Skip to content

feat(ui): rebuild searchable settings and add headset mappings - #468

Merged
chhoumann merged 11 commits into
masterfrom
feat/ui-settings
Oct 14, 2026
Merged

chhoumann merged 11 commits into
masterfrom
feat/ui-settings

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

Rebuild Settings into seven native searchable sections: Playback, Headset and lock screen, Notes, Transcripts, Downloads, Library, and Import and export. A 22-character path edit now writes data.json once after a 500 ms typing pause, or immediately on completion, down from 22 writes.

  • Remove the volume and three cache controls. The player still remembers volume. Feeds always cache for six hours, including for users who previously disabled caching; Refresh forces a fetch.
  • Add Previous track and Next track mappings for capture, skip lengths, chapters, and advancing the queue.
  • Offer the player's speed menu values from one shared constant, including 1.25 and 1.75. An existing unusual speed appears as an extra option without saving anything merely by opening Settings.
  • Keep Timestamp offset directly after Timestamp template. Keep dependent diarization rows visible and searchable while disabled. Refresh the auto-queue toggle when the Queue menu changes it.

Each section returns native SettingDefinitionGroup items, with one named SettingDefinitionRender per row. Obsidian indexes those rows directly. This deletes the synthetic alias list. The headset rows retain topical aliases because Obsidian does not index group headings. Custom renderers keep writes on the existing persistence writer; native control setters call saveData per keystroke.

Existing controls that SPEC 9 did not name explicitly remain:

Control Placement
Timestamp offset (s) Notes, immediately after Timestamp template
Diarization provider Transcripts, after Speaker diarization
Deepgram API key Transcripts, after Diarization provider
Speaker label format Transcripts, after Deepgram API key
OPML export filename Library, in Export OPML
Settings export filename Import and export, in Export settings

Search podcasts and Playlists had already moved out of Settings on the base branch. The duplicated result-card tooltip and missing artwork alt text belonged to that removed search UI.

Verification

Node 22.23.2. Commands use TMPDIR=/data/home/tmp/podnotes-ui3 fnm exec --using 22 -- ....

npm run lint, npm run format:check, npm run typecheck, npm run build, npm run test, npm run test:tooling, npm run docs:build, and npm run size all passed. Unit suite after merging #466: 121 files / 2,525 tests. Tooling: 2 files / 13 tests. Docs: 18 pages; all 112 legacy fragments resolve.

Real Obsidian 1.14.4 on headless Linux, isolated vault podnotes-feat-ui-settings, linked to this checkout's build. Final fresh-vault full suite after merging #466: 22 files and 166 tests passed in 577.39 seconds on 17b5bca50ec18c8a8ee82e607804dbb61cce5e03. Capture Undo/refusal and player journeys pass alongside the Settings and focused-edit regression tests. The isolated instance was stopped and the final dry run found no processes. Runtime coverage includes search in every section, focused text edits across whole-app quit/restart and CLI renderer reload, text-edit writes and completion, headset capture and remapping, cache retirement, volume across restart/import, and the existing journeys. Fixtures include http://podnotes.test:<fixture-port> feeds and Fixtures/Local Fixture.wav.

The write recorder targets PodNotes' exact data.json. Before: 22 completed writes for 22 typed characters. After: one write for the 22-character path V468/My podcastnote.md after the debounce, with zero extra writes on Tab. Memory already held the full draft while disk still held the old value. The normal-session sequence remains four writes, with identical counts after every action. Recorder integrity and plugin identity remain intact, with zero failed writes.

The V-468 focused-edit failure is reproduced by two real E2E tests at c0a6d3d. Both retain Initial/{{title}}.md instead of the focused edit after a 750 ms pause. Both pass after this correction. A further regression proves that reverting a saved field to its original text still saves immediately on blur. Focused-edit correction report and rerunnable evidence.

Twenty-four unit mutations and two runtime mutations failed on their named assertions. Eight refreshed/new mutation runs also caught their planted defects after review. Review regressions were reproduced in Obsidian before fixing them: import-lock bypass on a store emission, a current episode at the queue head, delayed chapters after switching away and back, and unavailable Previous chapter seeking to zero.

Whole-tab before/after comparison, desktop and phone 390, light and dark. These are native viewport slices; fixed headers repeat and the final slice overlaps.

Desktop, light Phone, dark
Desktop settings Phone settings

Verification report | Review resolutions and dispatch ledger | Raw evidence and rerunnable probes

Bundle size

Production main.js, gzip level 9, measured after each step with npm run build and npm run size.

Step Raw bytes Gzip bytes Gzip delta vs base
Base b14edf1 311,774 98,336 0
Native rows, removals, completed edits, speed menu 304,724 96,928 -1,408
Cache retirement 304,085 96,774 -1,562
Complete headset mapping 305,783 97,366 -970
Layout polish 305,847 97,402 -934
Review fixes and full-width path fields 306,171 97,536 -800
Focused-edit debounce and explicit completion 306,423 97,629 -707
Merge #466 and share playback speeds 310,775 98,183 -153

The mapping initially added 592 gzip bytes after the cache cleanup. The complete unit remains net-negative and below the 100,000-byte gate. Compared with the new master 35461cd0 rebuilt using the same dependencies and Node 22, UI3 removes 5,373 raw bytes and 744 gzip bytes (master: 316,148 raw / 98,927 gzip). The final bundle leaves 1,817 gzip bytes under the gate.

Release impact

feedCache.enabled and feedCache.ttlHours are retired. Old data decodes without warnings, no startup rewrite occurs, and the next normal save drops the retired key. Tests cover old enabled:false data, #388 three-way merge preservation, settings import/export, and a frozen 2.21.1-shaped reader falling back to caching on with a six-hour TTL. All other settings and library data remain preserved.

Two additive headset fields default to previous-track capture and next-track forward skip. Missing fields in old data get those defaults. Next track now follows the configured forward skip length (15 seconds by default), replacing its old fixed 30 seconds. defaultVolume keeps its stored shape and means the player's last volume. No API or URI changes.

Each keystroke updates settings in memory. The normal writer persists after 500 ms without another input, or immediately on blur, Enter in a single-line field, Tab, closing Settings, or closing its popout. Enter in a template remains a newline. The main-window timer follows QuickAdd 0142db3d. Completion never replays stale text over a later synced value.

Quitting or reloading inside the debounce window can still lose the unsaved typing burst. This is the accepted residual. The quit hooks and write-refusal protections are unchanged; the verifier's experimental quit-task patch was not adopted. Background visibility is covered by a unit test, not a real mobile suspension test.

Integration and follow-ups

Merge head: 17b5bca50ec18c8a8ee82e607804dbb61cce5e03. Integration report and logs, merge patches, and rerunnable checks.

#466 landed as 35461cd0. This PR merges that master without rebasing, with parents 23217bb1 and 35461cd0. The player's rate menu now imports PLAYBACK_RATES from src/utility/playbackRate.ts, as Default speed already does: 0.75, 1, 1.25, 1.5, 1.75, 2. Its inline list is deleted. A saved speed outside that list still appears as an extra Settings option without changing data.json on open.

CaptureService keeps #466's snapshot-equality Undo, success notice, refusal wording, and disposal exactly. The only difference from #466 is UI3's existing two-line diff in the missing-template notice, one deletion and one insertion: Capture timestamp format becomes Timestamp template, matching the renamed Settings row. The file otherwise matches #466 byte-for-byte.

The four merge conflicts preserve #466's verification instructions and hundredths normalization while retaining UI3's new Settings labels and constant. The keyboard helper keeps #466's Space handling and UI3's Tab handling. The complete isolated E2E suite passed (166 tests). It was run in full because the merge also touches the shared harness and runtime tests.

UI4 follow-ups: give queue advancement a controller action, and remove FeedRepository's ignored optional cache constructor member when the frozen view tests stop supplying it. Cache policy does not read that member. These seams avoid concurrent edits to UI2's files.

Docs at release

  • Rewrite the settings guide for the seven sections and new row names; retain Timestamp offset.
  • List the removed Search podcasts, Playlists, Default volume, Cache podcast feeds, Cache duration, and Clear cached feeds rows, with their replacement workflows in the view/player and Refresh.
  • Document unconditional six-hour feed caching and retirement of the cache preferences.
  • Document both headset mappings, all choices, and the change from fixed 30-second next-track skips to the configured forward skip length.
  • Document the speed dropdown, quarter-step values, and preserved custom speeds.
  • Explain the immediate in-memory update, the 500 ms trailing save, and immediate save on completion. Document that quitting or reloading inside the debounce window can lose the unsaved typing burst.
  • Move podcast-note template tag help into the guide and regenerate settings screenshots with docs/scripts/screenshots.mjs.

No published docs content is changed before release.

Note

Rebuild settings tab with grouped rows, debounced saves, and headset mappings

  • Rebuilds PodNotesSettingsTab.getSettingDefinitions in PodNotesSettingsTab.ts as grouped sections (playback, headset, notes, transcripts, downloads, library, import/export) with shared row renderers for text, dropdown, secret, and conditional controls. Text and textarea settings now save drafts after a 500 ms typing pause and flush on change, blur, pagehide, or tab hide.
  • Adds configurable headset media-button mappings (headsetPreviousTrack, headsetNextTrack) in constants.ts and Headset.ts. The media session in mediaSession.ts reads mappings at press time and supports chapter navigation and next-in-queue playback, with stale chapter requests rejected on episode switch.
  • Removes the feed cache setting: feedCache is retired in podNotesData.ts and settingsCodec.ts, and FeedRepository now always caches with a fixed six-hour TTL in FeedRepository.ts.
  • Replaces the Obsidian mock's Setting and PluginSettingTab with a structured settings-definition model including cleanup, disabled-state propagation, and inline errors, in obsidian.ts.
  • Updates OPML import progress notices to show counts instead of percentages, and reworks settings search in the mock/E2E flows.
  • Risk: FeedRepository.clear is removed and the constructor's cache property is ignored; stale persisted feedCache values are dropped on next save. Settings labels are renamed (Default Playback Rate → Default speed, Episode notes → Notes), which breaks any flow targeting the old names. Default headsetPreviousTrack is captureTimestamp and headsetNextTrack is skipForward; invalid persisted values fall back to defaults with warnings.

Macroscope summarized 17b5bca.

Each SPEC 9 section is a native settings group, and every row is its own
render definition, so Obsidian's settings search finds each row by name.
The 27-entry alias list and the single "Podcast preferences" wrapper go.

- Remove Default volume (the player remembers volume) and the feed cache
  rows (Cache podcast feeds, Cache duration, Clear cache).
- Text fields write data.json once per completed edit instead of once per
  keystroke. Typing changes only the field; the edit applies on change,
  when the row or tab closes, or when its window hides or unloads. A field
  with no edit never writes, so it cannot undo a synced or imported value.
- Default speed is a dropdown of the player's speed menu. A saved speed it
  does not offer is shown as an extra option and is not rewritten. Rates
  keep hundredths so 1.25 and 1.75 survive a reload.
- The auto-queue toggle is "Queue episodes I switch away from" and follows
  changes from the Queue page while Settings is open.
- Diarization rows stay listed and searchable, disabled while they do not
  apply.
- Path previews and the download warning use the row's description and
  native error message. Restating descriptions are removed.
- Plain-word import notice and OPML progress, and no long dashes in the
  settings migration docstrings.
Feeds are cached for six hours whatever the saved preference said, and
Refresh still fetches at once. Users who turned caching off get it back.

feedCache leaves the settings type, defaults, codec and settings transfer.
A saved feedCache, including enabled:false, decodes without a warning or a
startup rewrite, is not kept as an unknown field, and the next save drops
it, also when an older device syncs it back. Releases up to 2.21.1 read the
missing field as caching on for six hours. Clear cache had no caller left,
so FeedRepository.clear goes too.
Settings has a Headset and lock screen section with Previous track
(Capture timestamp, Back by the skip length, Previous chapter) and Next
track (Forward by the skip length, Next in queue, Next chapter, Capture
timestamp). Each press reads the mapping, so a change made in Settings or
synced in applies at once.

The defaults keep today's buttons, except that next track now skips by
the skip-forward length (15 s by default) through the API's skip, which
stops short of the end and never goes back, instead of a fixed 30 s.

Next in queue plays the queue's first episode. With auto-queue on, the
episode switched away from goes to the end of the queue, so repeated
presses go through it instead of switching between two. Chapter actions
load the episode's chapters once per URL, ask again after a failed or
empty load, and move nothing when the episode changed meanwhile.

The two settings are validated where data.json and imports are read; old
data without them gets the defaults with no rewrite.
Obsidian dims only disabled action rows, so a disabled diarization dropdown or key button looked usable. The timestamp template field sits under its name at full width, as on a phone it otherwise stopped short of the other fields.
… mapping

- Settings search finds a row of each section by name, a diarization row
  while diarization is off, and none of the removed rows.
- Typing 22 characters into Episode note path writes data.json nothing
  until Tab completes the edit, then once. An edit still in its field is
  saved when Settings closes.
- The queue toggle follows the Queue page menu while Settings is open.
- A saved feedCache enabled:false still caches feeds, and the next save
  drops it.
- Previous track follows a remap in Settings with no reload, and next track
  defaults to the skip-forward length.
- A restarted player starts at the volume saved last.
Each completion test now reads data.json before any other path could complete the edit, and the six-hour boundary no longer waits on a fetch it does not expect, so a planted defect fails on its assertion instead of a timeout.
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 17b5bca
Status: ✅  Deploy successful!
Preview URL: https://b00adc3b.podnotes.pages.dev
Branch Preview URL: https://feat-ui-settings.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit 7642765 into master Oct 14, 2026
17 checks passed
@chhoumann
chhoumann deleted the feat/ui-settings branch October 14, 2026 04:57
chhoumann added a commit that referenced this pull request Oct 14, 2026
… of the full player (#472)

* refactor(feeds): drop the ignored FeedRepository cache option

Feeds are always cached since #468. The constructor still accepted a
cache option that nothing read, and eight view tests still passed it.

* refactor(player): move Next in queue into PlayerController.playNext

The headset's Next in queue read the queue store and called play inline in
bindMediaSession. PlayerController now owns it, beside play and the natural
end's queue advance.

* fix(settings): say an import that could not save yet saves by itself

Since #470, PodNotes saves on its own once it can write data.json again.
The Notice after an import whose final save failed still told the user to
change a setting to save again.

* refactor(ui): co-locate each component's styles and split Deck and Panel out of Player

PodcastView.svelte held 1,300 lines of page and component CSS in one global
block. Each component now carries its own styles, anchored on its own root
class with the `.root :global {}` pattern the shell already used. The shell
keeps its tokens, its layout, the Obsidian DOM rules and the classes several
components share.

Player.svelte loses the deck (title, scrubber, transport, capture bar) to
Deck.svelte, and ChaptersPanel and UpNextPanel share their chrome through
Panel.svelte. Cover draws a local file's tile, and SearchField takes the
first run's large variant, so no page reaches into them for it.

Variants of the shared .pn-iconbtn and .pn-btn classes win by specificity
instead of by stylesheet order.

* test(player): format the playNext tests

* feat(ui): move between the wide player's tabs with arrow keys

The Chapters and Up next tabs follow the ARIA tabs pattern: Left and Right
move focus to the previous or next tab and wrap, Home and End go to the
first and last, and the focused tab shows its panel. Only the selected tab
is in the Tab order.

* fix(ui): hide the fullscreen button in fullscreen and let play respect reduced motion

In fullscreen the video frame's fullscreen button stayed visible and did
nothing. It now hides there; Escape leaves fullscreen.

The play button's 120 ms press transition ran even when the system asks for
reduced motion. It now presses without a transition then.

* refactor(verify): split screens.mjs by concern

screens.mjs grew past 1,100 lines. It keeps its usage header, the verb
dispatch and set; the instance calls, the seeded scenes and the contact sheet
move to screens/instance.mjs, screens/scenes.mjs and screens/sheet.mjs. The
code moved verbatim apart from imports, exports and the paths that moved one
directory down.

* refactor(ui): keep each button's touch cascade and move queue rows into EpisodeRow

On a touch device Obsidian adds .mobile-tap to a tapped button, and its
button:not(.clickable-icon).mobile-tap rule paints a fill. Search results,
playlist rows, chapter rows and Up next's Edit button keep the specificity
that resisted it, and the mini player's open button and the rail's add
button keep the plain scoped rules that let it through.

Queue rows are an EpisodeRow variant (queue, dragging) instead of rules
QueueList laid over EpisodeRow and Cover. Deck no longer takes `wide`: the
player passes no onchapters when the chapter name cannot open a panel.
.pn-alt moves whole into AddPodcastPage, and the shell's head and back
button rules move from the shared block to its layout.
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.

1 participant