Skip to content

feat(notes): find notes and links by episode GUID, capture where you write, and create episode notes without playing - #442

Merged
chhoumann merged 30 commits into
masterfrom
feat/note-identity
Oct 13, 2026
Merged

chhoumann merged 30 commits into
masterfrom
feat/note-identity

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes #349. Fixes #351. Refs #347: this is the logic half; the player buttons belong to UI2.

Notes and links survive a retitle (#349). Episodes now carry the feed item's GUID, which is a lookup hint, not a storage key. New links get an optional guid= parameter, through {{linktime}}, {{linksegment}} and {{episodelink}}. New episode notes get quoted guid and feedUrl frontmatter. A link, an episode note, or a published transcript is found by GUID first, within its own feed. A GUID that several items in one feed share is never attached to those episodes, and a link whose GUID matches several items is resolved by title. In both cases the existing title rules apply, so old links and notes keep working. Identity stays podcast name plus title (getEpisodeKey), with nothing rekeyed.

Create an episode note without playing (#351). With nothing loaded, Create episode note opens a podcast picker, then an episode picker, and creates the note from the template without touching the player. With an episode loaded it behaves as before.

Capture lands where you write (#347, logic). Capture Timestamp, Capture Last 10/20 Seconds and the headset's previous-track button now share one rule:

  • The text goes in at the cursor of the focused editor. If no editor has focus, it goes into the Markdown editor you used last, which can be in a popout. A note in Reading view is not an editor, and opening one does not replace the editor you used last.
  • With no editor, the text is appended to the episode note. If that note does not exist, PodNotes creates it from the template.
  • With nothing loaded, a notice says "Play an episode to capture".

The three commands now run from anywhere, including while the PodNotes view has focus. The mini player's Capture timestamp button from #434 goes through the same CaptureService and keeps its "Timestamp added to " notice. CaptureService returns { destination, path, text, created } for UI2's toast. Until UI2 lands, the commands show no notice on success, as today.

Also folded in from TRANS1: formatEpisodeTimestamp now formats segment links as well, so the API no longer builds its own link.

Master e3b90c1 This PR
After a feed renames an episode, its menu offers Create Note, and an old link shows "Episode not found" Open Note opens the original note, and a guid= link plays the renamed episode at its time
With the PodNotes view focused, the palette finds no Capture Timestamp; the headset capture goes to the episode note, not the note being written The capture goes in at the cursor of the note you used last
Create episode note is missing until something plays Podcast picker, then episode picker; nothing plays

#349, the episode menu after the feed renames the episode, master on the left:
Master offers Create Note for the renamed episode; this PR offers Open Note

#349, clicking a timestamp link captured before the rename, with another episode loaded:
Master shows Episode not found; this PR plays Renamed Opening Show from the link's time
screen recording

#347, the palette with the PodNotes view focused, master on the left:
Master finds no Capture Timestamp command; this PR lists it
The timestamp link inserted at the cursor of the note used last
screen recording

#351, nothing played, master on the left:
Master finds no Create episode note command; this PR shows the episode picker
The new note with guid and feedUrl properties

#347 on the v3 shell: the mini player's Timestamp button with focus in the PodNotes view puts the link at the caret of the note used last:
The link inserted in Verify/Mini.md and the notice Timestamp added to Mini

Verification

The checks ran at head d2ed715 (master 4cc2aeb merged in) with Node v22.23.2:

  • npm run lint: "Svelte policies: 0 errors, 0 warnings."
  • npm run format:check: "All matched files use the correct format."
  • npm run typecheck: exit 0.
  • npm run build: exit 0.
  • npm run test: 114 files, 2130 tests passed.
  • npm run size: 99,739 bytes gzip against 98,763 on master 4cc2aeb (+976), and 314,656 bytes raw (+2,379). 261 bytes of the 100,000 budget remain.
  • At 1448221, master 647c069 merged in; d2ed715 adds only perf: preserve plugin compilation caching across reloads #444's privateFeedUrl change:
    • npm run test:tooling: 11 tests passed.
    • npm run docs:build: "All 112 legacy fragments resolve."
    • npm run test:e2e: 15 files, 101 tests passed on Obsidian 1.14.4 (Linux, headless), on a fresh vault.
  • The first five NOTE1 journeys also passed on Obsidian 1.13.7, at 31398cd.

The new e2e journeys, each checked against a planted defect:

  • J-NOTE-09. A renamed episode keeps its note, its guid= timestamp link and its Resume link. An old link without a GUID still plays its own episode.
  • J-NOTE-10. Create episode note through both pickers with nothing loaded. Nothing plays and no media is requested.
  • J-TS-08. The capture lands at the cursor of the editor used last after the PodNotes view is clicked.
  • J-TS-09. The capture lands in a popout editor. After the popout closes, it goes to the episode note.
  • J-TS-10. With no editor open, the first capture creates the episode note from the template and the second appends to it.
  • J-NOTE-11. Two feed items share a GUID and only Alpha has a note. Beta's menu offers Create Note, and Beta gets its own note.
  • J-NOTE-12. A template whose frontmatter keys are indented gets guid and feedUrl in the same mapping, and Obsidian reads all its properties.
  • J-TS-11. After the editor is used, a note opened in a Reading tab does not take its place. The timestamp and a range land at the editor's caret.

Live proof in Obsidian 1.14.4, Linux, on an isolated vault, master and this PR:

  • Feed. The verify fixture at http://podnotes.test:20644/feed.xml, with GUIDs podnotes-fixture-1 and podnotes-fixture-2.
  • Rename. The new node .claude/skills/verify/fixtures.mjs retitle serves the two episodes as "Renamed Opening Show" and "Renamed Closing Show" with the same GUIDs. The cache was then cleared with Settings, Clear cache.
  • Steps. The feed was subscribed in Settings. Commands ran from the palette. Links were clicked in Reading view.
  • Reading view. With the last note in Reading view, a capture went to the episode note, and the note stayed byte-identical.
  • Headset. The previous-track action has no headset to press here. It calls the same capture.timestamp() that the unit tests cover through bindMediaSession.

Release impact

  • Storage. Persisted episode snapshots gain one optional string, guid: currentEpisode, queue, playlists, favorites, local files and downloads. Nothing migrates.
    • Released data still loads. The 2.21.1 fixture tests/fixtures/persistence/released-2.21.1.json decodes with no warnings in a unit test. Loaded into this build in real Obsidian, it left data.json byte-identical and showed no notice or error.
    • Older versions keep the field. 2.21.1's decodeEpisode copies unknown fields (copySafeObject), so guid survives a downgrade.
  • URIs. obsidian://podnotes gains an optional guid parameter. Old links resolve as before. 2.21.1 reads parameters by name and ignores guid.
  • API. The IAPI signatures are unchanged. getPodcastTimeFormatted and getPodcastSegmentFormatted links now carry guid= when the episode has one.
  • Note content. New episode notes get guid and feedUrl properties when the episode has a GUID.
    • Existing keys are read with Obsidian's own YAML parser, so a key the template already writes is never added again, however it is spelled.
    • New lines follow the mapping's indentation, and the user's lines are never rewritten.
    • If the result would not read back as the template's properties plus the new two, the note is created without them.
    • A private feed's feedUrl is its placeholder, never its address.
    • {{episodelink}} and {{url}} in the default template already write the feed URL into notes.
  • Dependencies. yaml 2.8.4, the parser Obsidian bundles and already in the lockfile through vite, is now an explicit devDependency so the unit tests parse frontmatter the way Obsidian does. It is not in main.js.
  • Commands. Capture Timestamp and the two range commands are plain commands now, not editor commands, so they run with no editor focused. Hotkeys and saved mobile toolbar entries keep working because both are stored by command id. Obsidian 1.13 and 1.14 list plain commands under the mobile toolbar's Add command.

Docs at release

Per standing order 28, these docs pages describe the released behavior and change at release:

  • docs/src/content/docs/commands.md:
    • Lines 22 to 25, the availability column. The capture commands are always available. Create episode note needs a loaded episode or a saved podcast.
    • Lines 79 to 80 and 84 to 85: capture works from anywhere, with the destination rule.
    • Lines 88 to 91: with nothing loaded, Create episode note asks for a podcast, then an episode.
  • docs/src/content/docs/timestamps.md:
    • Line 30: the destination rule now applies to every capture command.
    • Line 70: the notice is now "Play an episode to capture".
    • Line 74: a link with guid finds a renamed episode.
    • The parameter table near line 131: add guid, optional, the feed item's GUID, tried before episodeName.
  • docs/src/content/docs/notes.md:
    • Lines 10 to 13: the command works with nothing playing, through the two pickers.
    • Line 117: notes with guid and feedUrl properties are found after a rename or move.
    • Document the two properties.
  • docs/src/content/docs/mobile.md lines 27 to 32 and 47 to 50: no note needs to be open to add the commands, and the headset follows the same rule.
  • docs/src/content/docs/faq.md lines 21 and 69: capture no longer needs a note open, and it goes to the editor used last.
  • docs/src/content/docs/getting-started.md line 42: no episode needs to be loaded.

Review

An adversarial review ran in three rounds, with Claude Fable 5.1 and GPT 6.0 Astra.

  • Round 1 found 15 issues. The ones accepted were fixed:
    • A link's GUID only ever rules the loaded episode out.
    • Quoted frontmatter keys count as present.
    • A note in Reading view is not a capture target.
    • Transcript GUID matching is limited to the episode's own feed.
    • Capturing into the episode note scans the vault once, down from three times.
  • Round 2 made the GUID rule strict and closed two races in note creation.
  • Round 3: GPT found nothing, and Fable raised two nits.
  • Independent verifier (V-442) failed 31398cd on three defects reproduced in Obsidian, all fixed here with unit tests and the journeys above:
    • A GUID repeated across feed items opened another episode's note.
    • A Reading tab replaced the remembered editor.
    • Indented or escaped frontmatter keys were duplicated.

The verdicts with reasons, including what was dismissed, are in the thread's audit notes.

Note

Identify episodes and notes by GUID, capture to last focused editor, create notes without playing

  • Episodes now carry an optional GUID throughout persistence, feed parsing, and deep links. encodePodnotesURI and buildEpisodeResumeLink include the GUID, and podNotesURIHandler selects episodes by GUID (falling back to title) so links keep working after publishers rename episodes.
  • New CaptureService (CaptureService.ts) handles timestamp and segment capture. It writes to the focused Markdown editor, falls back to the last focused source-mode editor, and otherwise appends to the episode note. Capture commands are now plain callbacks and can run without an open editor; Reading view counts as no editor.
  • The episode-note command can run with no loaded episode: pickEpisodeForNote (commands.ts) opens a saved-feed picker, loads the feed, and offers an episode picker.
  • Note creation writes guid and feed URL frontmatter via addFrontmatterFields (noteFrontmatter.ts) and getPodcastNote resolves existing notes by GUID (scoped to the same feed) before title/path lookup. Duplicate GUIDs in a feed are stripped by FeedRepository so they cannot misidentify episodes.
  • Behavioral Change: capture commands no longer require an editor; episodeMetadata returns transcripts only and no longer matches GUIDs across feeds; FeedSuggestModal is replaced by the generic PickerModal; add-on lockfile now includes the yaml dependency.

Macroscope summarized 62c16e5.

Capture Timestamp, Capture Last 10/20 Seconds and the headphones'
previous-track button now share one CaptureService. A capture goes to the
focused editor, else to the markdown editor focused last (clicking in the
PodNotes view leaves no active editor), else to the episode note, which is
created from the note template when missing and appended to atomically.

The three commands become plain callbacks so they run with no editor
focused. Saved mobile toolbar entries are looked up by command id, so they
keep working.
…nd episode picker

Create episode note is now available with nothing loaded once a podcast is
saved. It asks for a podcast, loads its feed, asks for an episode in feed
order and creates that episode's note, without touching the player. With an
episode loaded it creates that episode's note as before.

FeedSuggestModal becomes the generic PickerModal, shared by the feed note
and episode note commands.
A guid can repeat within a feed, so the loaded-episode shortcut no longer
accepts the loaded episode on a guid match alone. A guid that differs from
the loaded episode's sends the link through the feed lookup, which refuses
duplicate guids and falls back to the title.
A template that writes "feedUrl": or 'guid': already has the key, so
adding it again would give the note a duplicate key that Obsidian rejects.
A MarkdownView in Reading view still returns its hidden editor, and text
written there is lost. A capture now treats such a note as no editor,
whether it is focused or was the last note focused, and goes to the
episode note.
A subscription of another feed that shares the episode's podcast name no
longer lends its transcript through a shared guid. Other feeds still
match by enclosure URL as before.
createPodcastNoteFileIfNotExists now returns the note and whether it was
created, and shows no notice. createPodcastNote says an existing note
exists, as before, and a capture into an existing note stays quiet. A
capture scans the vault once instead of three times.
The episode picker for Create episode note now loads a podcast by its
savedFeeds key, so a podcast saved under another name than its title
still lists its episodes. A private podcast whose address is not on this
device gets the usual private feed notice.
Capture commands now run from anywhere, Create episode note works with
nothing loaded once a podcast is saved, and fixtures.mjs has a retitle
verb. The verify skill said otherwise or did not list it.
…d matches

A loaded episode without a guid may be another feed episode that reused its title, so a link that carries a guid now goes through the feed lookup unless the loaded episode has the same guid.
Creating a note awaits the chapter fetch, during which sync or another capture can write the episode's note. Look the note up again after that wait and return it instead of writing a second note.
When vault.create fails because the note now exists at its path, the note is returned with created: false, so Create podcast note says it already exists and a capture does not report a new note.
… capture destinations

J-NOTE-09 retitles an episode on the fixture server and proves its note,
its guid= timestamp link and its Resume link still find it, while an old
link without a GUID still plays its episode. J-NOTE-10 creates an episode
note from the palette's podcast and episode pickers with nothing loaded.
J-TS-08, J-TS-09 and J-TS-10 prove Capture Timestamp lands at the caret of
the editor used last, in a popout too, and otherwise in the episode note.

The fixture server can now retitle an episode, and the journey actions can
run a command from the palette, open and click an editor in any window, and
close a popout with its tab. Closing a popout can leave Obsidian looping a
ResizeObserver in the main window, with PodNotes disabled too, so the popout
journey reloads at the end and accepts only that error.
@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: 62c16e5
Status: ✅  Deploy successful!
Preview URL: https://997fc06d.podnotes.pages.dev
Branch Preview URL: https://feat-note-identity.podnotes.pages.dev

View logs

suggestion: (text: string) => byCss(`suggestion ${text}`, ".prompt .suggestion-item", text),
paletteCommand: (name: string): UiTarget => ({
name: `palette command PodNotes: ${name}`,
elements: `[...document.querySelectorAll(".prompt .suggestion-item")].filter((el) => el.querySelector(".suggestion-prefix")?.textContent === "PodNotes" && el.querySelector(".suggestion-title")?.textContent === ${q(`PodNotes${name}`)})`,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a defect here. paletteCommand builds renderer code for the e2e harness, and its only input is a command name written as a literal in the test ("Create episode note", "Capture Timestamp"), quoted with JSON.stringify like every other ui target in this file. CodeQL classifies the location as test code, and master already has 12 open alerts of this rule for the same pattern in tests/e2e/journey/actions.ts, tests/e2e/harness.ts and tests/e2e/local-files-runtime.test.ts. Clearing the class means one escaping quote helper for the whole harness, which belongs in its own change rather than in one new target.

Two items of one feed can carry the same guid. Projected onto both
episodes, it let a note of one episode open for the other, and become
its capture destination. A repeated guid now identifies neither episode,
so their notes and links fall back to the path and title rules.
…akes focus

Every markdown tab that took focus became the remembered editor, a note
in Reading view too, so opening one sent the next capture to the episode
note while the editor stayed open. Obsidian fires no event when a note
switches between Reading view and editing, so a note now counts by its
mode when focus leaves it, and only an open note in Source or Live
Preview mode receives a capture.
Obsidian's parseYaml is eemeli/yaml 2. The obsidian mock now parses with
the same library and options, so unit tests see the YAML Obsidian sees.
… holds

A line pattern decided which keys a template's frontmatter already had,
so an indented key or an escaped one such as "gu\u0069d" got a second,
unindented guid or feedUrl, and Obsidian showed Invalid properties.
Existing keys now come from Obsidian's own YAML parser, new keys take
the indentation of the mapping's first key, and frontmatter that is not
a mapping, or would not read back as the same mapping plus the fields,
is left as it is.
The mini player's Timestamp now goes through CaptureService: ShellServices.captureTimestamp
returns the Capture result, and MiniPlayer names the note from its path. #434's
captureTimestampToEditorOrNote and its capture helpers in main.ts are gone, IPodNotes
exposes `capture` instead, and its two main.ts capture tests now drive capture.timestamp().
The NOTE1 journeys open the Library with showLibrary, as the shell replaced the grid tab.
Comment thread src/services/FeedRepository.ts Outdated
Comment thread src/commands.ts Outdated
@chhoumann
chhoumann merged commit e3243c4 into master Oct 13, 2026
14 checks passed
@chhoumann
chhoumann deleted the feat/note-identity branch October 13, 2026 07:23
chhoumann added a commit that referenced this pull request Oct 13, 2026
…s pages (#447)

* test(verify): capture the v3 pages at the design's widths and themes

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.

* refactor(ui): add library actions, episode row model and menus as data

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)

* feat(ui): first run and Add podcast pages in the view

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)

* feat(ui): Library grid with title tiles, error badges and the podcast 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)

* feat(ui): podcast episodes page and one episode row for every list

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)

* feat: open PodNotes once when the mobile app enables it for the first 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)

* refactor: drop the download check and unavailable reason no list reads

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)

* refactor(ui): share formatMinutes with the mini player

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)

* refactor(ui): play a vault file through the shared PickerModal

The merge brought PickerModal from #442, so "Play a vault file" offers
the vault's audio and video files through it instead of a second
FuzzySuggestModal subclass.

size: 99115 gzip (-38 against the merge, 99153; master e3243c4 is 99768)

* perf(notes): read the vault once for a list's episode notes

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)

* fix(ui): wrap long episode, result and card titles inside their buttons

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).

* fix(ui): report a podcast's feed failure only in its page's banner

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.

* fix(ui): keep Back on the wide Add podcast page reached from First run

From the rail the page has nothing before it and stays without Back.

* fix(ui): leave Copy episode link out when its target carries a feed secret

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).

* fix(ui): count an episode as current only when its media is the player's

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).

* fix(ui): offer only Mark as played for a Played record that left every 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).

* fix(ui): keep the row note icon current, and cheap enough for every row

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).

* test(e2e): drive the journeys through the new Library, Add podcast and 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.

* test(e2e): first run to playing, pasted feed and Apple links, and a feed 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.

* test(e2e): the episode menu's order, playlist submenu, Mark as played and Open player

* test(e2e): a new install on mobile opens PodNotes once, on First run

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.

* fix(ui): size the text cover's padding against the cover, not its row

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).

* docs(verify): drive feeds through the view's pages and add a new-install 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.

* fix(ui): list a Played search's best match first

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).

* fix(ui): leave played episodes out of the player's queue while hidden

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).

* test(ui): move a queued episode up from the player's queue menu

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.

* fix(services): say a private feed failed to load without its error's text

Set feed address showed "Could not load feed: " and the error's message,
which can carry the address being set, a secret. It now shows one fixed
line, "Could not load the feed at that address."

npm run size: 99,240 gzip (master e3243c4: 99,768).

* fix(main): log a failure to open the view on a new install's first mobile start

The first mobile enable opened the view with no rejection handler, so a
failure became an unhandled rejection. It is now logged as the desktop
startup path logs it. A new test starts on mobile with no data.json,
then starts again on the data.json that first start wrote, and proves
the view opens only the first time.

npm run size: 99,244 gzip (master e3243c4: 99,768).

* fix(ui): start First run over on Back, and open Add podcast only on text

Back from Add podcast to First run showed the old query, and a search
still in flight could publish its results. First run now clears the
search when it opens. Spaces alone on First run no longer open an idle
Add podcast page.

npm run size: 99,260 gzip (master e3243c4: 99,768).

* fix(ui): list nothing on a podcast or playlist page whose list is gone

A podcast page whose feed was removed, or a playlist page whose playlist
was, fell back to Latest episodes until the shell left the page. Each
page now lists only its own episodes, and none once they are gone.

npm run size: 99,269 gzip (master e3243c4: 99,768).

* fix(ui): keep the full player's artwork square on mobile

The artwork is the full player's play and pause button. It was a square
made of padding-bottom, which Obsidian's mobile button padding overrides,
so on a tablet or phone it collapsed to an 8 px strip. An aspect-ratio
keeps it square whatever the button's padding.

npm run size: 99,111 gzip (master e3243c4: 99,768).

* fix(ui): refresh a restored view's note icons at layout ready, once per burst

A view restored at startup lists its rows before Obsidian has indexed the
vault, and the rows only looked their notes up again on a later vault event,
so a note found by its GUID kept a missing icon for a quiet session. The view
now registers its vault listeners when it mounts, so closing it removes them
even before the layout is ready, ignores the files Obsidian reports while it
loads the vault, refreshes the rows once at layout ready, and collapses a
burst of changes into one lookup after 200 ms of quiet.

The test mock's Component and ItemView follow Obsidian's lifecycle: open
loads before onOpen, close unloads before onClose, and a callback registered
after unload never runs.

Size: main.js gzip 99,302 bytes (was 99,271), raw 312,785.

* docs(verify): wait for the Add result that a pasted feed shows

* test(e2e): J-LIST-04 waits for the Loading episodes line to leave both pages

* test(e2e): J-FEED-05 removes a podcast on another device while Latest Episodes stays open

* test(e2e): Open player on the playing episode keeps it playing and advancing

* test(ui): iterate the fake vault's listeners without copying them

* refactor: drop comments that restate the code they sit on

Keeps only the whys the code cannot show: security, races, platform
behavior and interface contracts.

* fix(ui): drop a pending note lookup when the view closes, and name the result's episode count

size: 99,317 gzip

* test(ui): a same-titled episode without a downloaded copy shows as not downloaded

* fix(ui): call an episode downloaded only when playback would play its 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.

* test(ui): listed episodes look their notes up again when the note path setting changes

* fix(ui): show a listed episode's note once the note path setting points 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.

* test(ui): the search takes focus when its view first shows, not while it is hidden

* fix(ui): focus the search field once its view is shown

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.

* test(ui): build the note-path test's note as Obsidian's TFile type allows

* test(e2e): onboarding types into First run without tapping the field first

* test(e2e): onboarding waits for the search field's focus before typing

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.

* test(e2e): write the mobile new-install eval's plugin values with jsLiteral

* test(e2e): find a Back button by its label anywhere in the view

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.
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.

"Create episode note" should offer an episode picker when nothing is loaded Match notes and timestamp links by episode GUID, not by title

2 participants