Repository navigation
fix(feeds): show missing private addresses while offline - #482
Merged
Merged
Conversation
Deploying podnotes with
|
| Latest commit: |
c0ac05c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://12d7a79f.podnotes.pages.dev |
| Branch Preview URL: | https://fix-offline-missing-address.podnotes.pages.dev |
This was referenced Oct 14, 2026
chhoumann
added a commit
that referenced
this pull request
Oct 15, 2026
* fix(ui): remove the previous bundle's style sheets when PodNotes loads Obsidian updates a plugin in place without reloading its windows, and Svelte only ever appends a component's sheet, so the sheets of the version before an update kept applying until a restart. PodNotes now removes its earlier pn- sheets from the main window and every popout at the start of onload, before registerView mounts the restored views. tests/e2e/update-styles.test.ts installs the 2.21.1 release over this build, plants stand-in stale sheets, then updates back the way Obsidian's updater does, with the view in the main window and in a popout. * refactor(ui): share one button reset, one focus ring and one playing check - .pn-bare in the shell carries the reset of Obsidian's button skin that fifteen rules repeated, together with the shell's own .pn-back, .pn-iconbtn and .pn-navitem. Declarations equal to Obsidian's own button rule (border, cursor, font size) go. - One button:focus-visible rule replaces the list of thirteen selectors. - isPlaying(status) replaces four copies of the same derivation. - tests/tooling/componentStyles.test.ts fails when two components scope the same class, so a component's sheet only styles what it renders. - The queue list drops its unused pn-queue class; the e2e targets select it by its accessible name. * fix(ui): polish fallback covers, the address banner, disabled controls and tooltips - A cover without artwork shows the first three lines of its name, at 10 px or more, so a long name keeps its beginning on a row's 40 px cover. - The feed banner keeps monospace for a host only; "Address not on this device" wraps in the UI font instead of truncating at 320 px. - Every disabled button uses Obsidian's 0.7 opacity: the 0.35 and 0.45 overrides go, and an unavailable row dims through its aria-disabled. - The mini player draws its cover with the rows' Cover and tileOf, so an episode without artwork shows its podcast's name as rows do. - No control has both an aria-label, which Obsidian shows as a tooltip, and a title; the tooling test enforces it. - The transcript's Follow chip no longer appears when a resize clamps the list's scroll position, and Find folds a dotted capital I without moving its marks. * fix(ui): name settings fields, widen Edit, and stop all motion on request - Each settings field is labelled by its row's name through aria-labelledby, which Obsidian does not show as a tooltip. Toggles keep Obsidian's native 30 px size, as every toggle in Obsidian's settings has. - Up next's Edit, and every other panel head control, is at least 44 px wide on a phone, not only 44 px tall. - One shell rule stops every transition and animation in the view when the user prefers reduced motion; the grid card's press had none before. It replaces the play button's and the playing bars' own rules. - styles.test.ts cascades the components' compiled CSS in jsdom: motion under the reduced-motion query, and the video play button's focus ring drawn inside the frame that clips it. * fix(ui): give the phone player room, and fix hover and menu Escape on phones - On a phone the deck keeps the desktop's 12 px gaps and 8 px top padding, so the stage, and the artwork that fills its height, grow by 20 px under Obsidian's header and navbar. A panel already fills the stage. - Hover on the shell's controls, rows and the deck applies only on a device that can hover, so a tapped control does not stay lit on a phone. - A menu's closing Escape is handled, and focus returns to its control, as the menu starts to hide; on a phone the hide callback runs 150 ms later, when the Escape is gone. * refactor(ui): draw the full player's artwork with Cover, as rows and the mini player do The full player kept its own image, failure state and tile. It now uses the rows' Cover and tileOf: a remote episode without artwork, or whose artwork fails, shows its podcast's name as rows and the grid do, and a local file its tile. This saves 111 gzip bytes. * refactor(ui): keep master's wrapping feed banner detail #482 made the banner's detail wrap, so "Address not on this device" no longer truncates at 320 px. The split that set a sentence in the UI font and only a host in monospace cost 52 gzip bytes for a font change, so it goes back to master's single rule; the e2e keeps the no-truncation check. * fix(ui): leave video fullscreen on the first Escape, and open a saved transcript note - In a video's fullscreen the view's Escape now leaves fullscreen and keeps the player open; the next Escape closes the player. Obsidian's window does not leave element fullscreen on Escape by itself, and closing the player only ended fullscreen by removing the frame. The fullscreen button stays on the frame in fullscreen as Exit fullscreen. - The transcript panel offers Open note, offline too, when the episode's transcript note already exists at the transcript path, instead of Save as note, which the command refuses for an existing note. It follows a note made, moved or deleted, and a new transcript path. - The podcast menu's Set feed address, and Edit feed URL, are disabled offline, as the feed banner's button is: an address is checked by loading its feed. * test(e2e): place the transcript journeys' view instead of assuming the right sidebar The journeys opened PodNotes wherever the vault's layout left it. After the verify skill's screens.mjs had moved the view to the main area or sized the window, the transcript panel was shorter, the click helper scrolled line 7 into view, and the Follow chip that scroll shows covered it. Each journey now puts PodNotes in the right sidebar 820 px wide, widening the window if it has to. * test(e2e): a private feed with its address here lists its cached episodes offline Beside #482's journey for a private feed whose address is missing: one whose address is on this device, opened offline after its feed was cached, lists the cached episodes and shows no feed banner and no missing address. * refactor(ui): anchor each component's styles on its root Svelte writes its scoping class into every selector written at a sheet's top level, but only once for rules nested in a root-anchored `.root :global { ... }` block. Each component now keeps its root's own declarations and its state, width and platform variants (`&.is-wide`, `.is-phone &`) in that one block. The flattened rules select the same elements with the same specificity, except two that rise by one class and change nothing: the wide header's `.pn-head` and `.pn-head-title` rules, which nothing else styles, and the queue's row-hover rule for Remove, whose only rival in between is its own focus rule with the same opacity. * refactor(ui): keep the row equalizer's keyframes global A scoped keyframes rule makes Svelte write its scoping class into every element of the component. EpisodeRow's `pn-eq` bars are the only keyframes in the view, and put the class on all 17 of the row's elements. A global `pn-eq`, inside `:global { }`, leaves it on the root. * refactor(ui): style a chapter's name with Panel's rows ChaptersPanel's sheet held one rule, the weight of a chapter's name. Panel already styles its rows and their time, so it styles the name beside it, and ChaptersPanel no longer ships a sheet of its own. * fix(ui): open a saved transcript note when the feed transcript is missing With a transcript note saved before, the Transcript panel offered Open note only once the feed's transcript loaded. When it did not load (an offline restart) the panel showed only Retry, and when the feed has none (the note came from a paid transcription) it offered to transcribe again. Both now offer Open note, in place of the paid transcription, through the same button as the panel's head, which keeps its label outside the head. * fix(ui): name each settings toggle where it takes focus Obsidian 1.14 focuses a toggle's `label.checkbox-container`, and gives its checkbox tabindex -1, so the row's name on the checkbox never reached a screen reader. The label is named after the row too. The unit mock builds Obsidian's toggle markup, so the test can tell. Each Setting has a fresh name element, so it always takes a new id. * fix(ui): light up hovered controls only on a device that can hover A touch screen keeps the last tapped element hovered. Eleven `:hover` rules outside `@media (hover: hover)` stayed lit after a tap, and the panel switch's hover rule outranked its pressed look, so a tapped switch on a phone looked hovered rather than on. Every component's hover rules now sit in that query, and the test walks every component under src/ui instead of a hand-kept list. * fix(ui): clip a text cover's name after its third line The clamped name had a 10cqw padding inside its overflow clip, so the line after the third painted into that padding: on the 93 px card of a 320 px view, four lines had their middle inside the clip. A margin puts the clip edge under the third line and keeps the name where it was. The e2e test now counts those lines. * test(e2e): on a phone, the Escape that closes a row menu keeps focus on its button A phone hides Obsidian's menu after a 150 ms animation, and Obsidian sends focus from a sidebar to the main area on an Escape nothing handled. The test opens a row's menu in the right drawer, where PodNotes opens on a phone, presses Escape with trusted input, and reads focus in the task after the keydown and 200 ms later: on the row's More button both times. * test(e2e): restore the plugin's links after an update test that failed part way The update-styles test swaps PodNotes' artifact symlinks for files and puts them back in its finally. An install that failed after removing main.js made that finally throw ENOENT before relinking it, which left the vault's PodNotes without a bundle. It now removes with force. * refactor(ui): drop declarations that restate Obsidian's defaults Every element the button rules style is a button, and Obsidian's `button` rule already sets the same display, alignment, height, radius, font size, cursor and white-space. No other Obsidian rule that reaches a button in the view sets those properties, so the cascade resolves to the same values. Obsidian also hides a search field's cancel button. * refactor(ui): draw a transcript that failed or is missing as one empty state The two states shared their frame, icon and note button, and differ in the icon, the sentence and whether Retry or the paid transcription shows. * refactor(ui): count items with a class of the view's own, not Obsidian's flair The counts used Obsidian's `.flair` and undid its tag look with five declarations. A `pn-flair` class needs none of them. What Obsidian's rule still set does not show here: a radius without a background, a relative position without an offset, and vertical-align on a flex item. * test(e2e): offline, a private feed shows no failure from before it went offline Offline, a load is not attempted, so the journey that restarts offline records no failure, and showing every recorded failure offline passed it. The journey now fails a refresh online first, and checks that going offline hides that failure behind the Offline strip. * fix(ui): name the Local files page as it is spelled in its menu item The episode menu said "Remove from Local Files" while the page is "Local files". The stored playlist keeps its name. * fix(ui): say that a second download to a shared path fails Without {{title}} in the download path, episodes resolve to the same file. The download code then refuses the second one, with "A file already exists" when another episode's file is there and "A download is already in progress" while the first is still writing, so nothing is overwritten. The warning said downloads overwrite each other. * fix(ui): fold the transcript's Save into its icon wherever the head is narrow The label folded away on the view's compact desktop layout, so in a phone's right drawer, about 330 px wide, "Save as note" ran past the panel's edge. The panel head is now a size container, and Save folds when the head has less than the 340 px the phone's label needs: in a 320 px sidebar and in the drawer, not in a 390 px phone's main area. The CSS mock applies a container query the way it applies a media query. * fix(ui): follow a transcript line with the line before it in full view Following kept the line playing now a third of the way down the list. In a phone's right drawer, where the list is 248 px tall, that left the line before it 7 px above the list and under its faded top edge. The list declares that 12 px edge as scroll padding, and following now puts the line a third down or under the whole line before it and that edge, whichever is lower, but no lower than half way, so a long line before it does not push it out of view. The e2e helpers for the phone layout and the right drawer serve the transcript journeys' three phone tests. * test(e2e): start each suite online, whatever the instance was left in view-polish timed out once waiting for a Latest row. That run reused an instance on which the verify skill's `screens.mjs offline on` had left the renderer offline through CDP: no feed loaded, the Latest page stayed empty, and the next run passed because the suite's offline test turns the network back on as it ends. Reproduced by the same commands, then a run of view-polish: "Timed out waiting for episode First Steps shown". The harness now brings the renderer online before the suite starts, and the reused-vault lifecycle test leaves it offline to check that. * fix(ui): follow a line or chapter between the panel's faded edges Following put the transcript's line a third of the way down, lower to show the whole line before it, but no lower than half way. A tall line could then end in the faded bottom edge, the half-way cap clipped a line before it that would have fit, and a third of the way down could leave the line above the one before it cut under the top fade, as in a 320 px sidebar. Chapters kept the line a third of the way down and ignored both fades. One reveal beside Panel now serves both panels. It puts the row under as many whole rows as fit in the list's top third, starting where the top fade ends, so no row shows cut there, and under the whole row before it even when that one is taller. The row never reaches into the bottom fade and never starts under the top fade. Each fade's height is one custom property on the scroller, which the mask and the scroll padding use and reveal reads. * test(e2e): share the phone layout and the right drawer placement view-polish and journey-transcript each pasted the CDP phone metrics and the steps that open PodNotes in a phone's right drawer. asPhone now sits in the journey actions, and placePodNotes takes a "right drawer" place that waits until PodNotes shows there. The Escape focus test removes the global it records focus in. * test(e2e): check the name Chromium gives each settings field Tab reaches The test read each field's aria-labelledby and looked the label up itself, so it passed on markup that names nothing. It now turns on diarization, whose rows take focus only then, tabs through PodNotes' settings with trusted key presses, and reads the name Chromium computes for each field that takes focus, the name a screen reader announces, and what Chromium took it from. Each must be its row's name, through aria-labelledby: a focused toggle's label would otherwise take the name of the checkbox inside it. * fix(ui): name the transcript's note button by its label, with no tooltip The note button carried an aria-label equal to its label, so Obsidian showed a tooltip repeating "Open note" in the empty panel, where the label is in sight. The aria-label kept the button named in a narrow head, which folded the label away with display: none. A narrow head now clips the label out of sight and keeps it in the accessibility tree, so the label names the button everywhere and the aria-label is gone. * fix(ui): keep the rail's add button padding on a tablet The rail's add button was the one control whose rule sat outside its component's root, at the specificity of two classes. On a tablet, Obsidian's .is-tablet button:not(.clickable-icon) outranks that and gave it 4 px by 20 px of padding. The rail's styles now nest under its root like every other component's. * fix(ui): keep the stage's artwork look off a panel's row covers The stage's artwork rules matched every cover in the stage, so where a narrow player shows Up next in the stage, its rows' covers took the artwork's drop shadow, and a local file's row tile took the artwork's icon size. The rules now match only the stage's own child. * fix(ui): dim every disabled control to 0.4, the look of Obsidian's disabled icons Disabled buttons took Obsidian's 0.7 for buttons, which reads as enabled, most of all on icon buttons and on an offline row that needs a connection. One rule in the view now dims a disabled or aria-disabled button to 0.4, as Obsidian dims a disabled icon. Hover no longer lights a disabled icon button or call to action, so the icon button's own disabled rule is gone. Nothing inside a disabled button dims again, and the scrubber keeps its own 0.5. * fix(ui): end a phone's search text where its clear button starts On a phone the clear button is 44 px wide, but the field kept the desktop's 28 px of room on its right, so a pasted link ran into the X. A phone's field now keeps 44 px on its right. * docs(ui): say what fits the transcript head's 340 px The container rule that folds Save's label gave no reason for 340 px. Measured on a phone, the head's icon, title, Free tag, 44 px Find and labelled Save take 339.8 px. * fix(ui): keep a pressed icon button dimmed when it is disabled The one disabled rule dims every disabled control to 0.4, but the pressed icon button's full opacity outranked it. reveal now says "at least the row before" with the position it has already taken instead of reading the DOM again. * test(e2e): turn diarization on only when it is off, and name what the head check reads The settings test clicked the diarization toggle, which turned it off on a vault where it was already on, so the diarization rows never took focus. The transcript head check reports whether Save has an aria-label; it now says so.
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.
Summary
A synced private podcast now shows "Address not on this device" and Set feed address on its first offline open. The repository checks the local address before gating network access. The error text wraps so it stays readable in a narrow sidebar.
Fixes #476
Scope
Local private-address diagnosis and the episodes-page error description. Online fetch behavior and fresh-cache lookup rules stay unchanged.
Verification
npm run lint,npm run format:check,npm run typecheck,npm run build,npm run testwith 2,614 tests,npm run test:toolingwith 13 tests, andnpm run docs:build. Commands used Node v22.23.2 throughfnm exec --using 22 --.npm run test:e2eon a fresh isolated vault in Obsidian 1.14.4 on Linux, 199 tests across 27 files, exit 0. This repeated a full run whose tests passed but whose terminal session returned 143 after cleanup. The isolated instance is stopped.podnotes-private-feed:Privatewith an absent device secret and no cache. The journey opens it through Library and checks the message, offline-disabled action, enabled action after reconnect, persisted subscription, and readable banner width.http://podnotes.test:<port>/feed2-alpha/feed.xmland/feed2-beta/feed.xml.npm run sizepassed at 321,686 raw bytes and 101,871 gzip bytes. The delta is +46 raw bytes and +34 gzip bytes.Three wrong alternatives each failed a named test:
a private feed whose URL is not on this device is unavailable with online falseand the real-Obsidian journey failed.offline, a feed with no document waits idle without a requestfailed.offline, a private feed with its address on this device waits without an error or requestfailed.The rendered-width assertion also failed before the error-description wrapping change.
Verification logs, mutation driver, and independent reviews.
Before. After the fix:
Release impact
No settings, storage format, public API, or URI changes. No migration.
Docs at release
Update
docs/src/content/docs/podcasts.md, underWhen a feed cannot be reached, to say that a missing private-feed address is reported offline too, and Set feed address needs a connection. Published docs remain unchanged.Note
Fix offline resolution of private feed addresses in
FeedRepositoryensureAllskipped all per-feed work. It now evaluates every subscription through the cached request path.onlineflag. When false, it resolves the feed address but does not start a fetch; an unresolved address still records an unavailable error, and fresh cached feeds are reused as-is.Macroscope summarized c0ac05c.