feat(queue): allow reordering the playback queue - #179
Conversation
The Queue previously had no reorder affordance. Add two complementary, cross-platform ways to resequence it, plus the supporting store ops. - store: pure reorderEpisodes() and queue.move/moveUp/moveDown/moveToTop/ moveToBottom; enforce a title-unique queue on every add and set so existing or synced queues with duplicate titles are normalized on load (no settings migration; order already persists via QueueController) - context menu: "Move to top/up/down/bottom of queue" verbs, gated to the player's queue list via viewState; index resolved live at click time - command + modal: "Reorder Queue" opens a button-list reorder window (QueueReorderModal + ReorderQueue.svelte) with per-row move/remove controls - docs and unit/component tests The Player/Episode-UI files frozen by #173 are untouched; the queue list there is reached only through the non-frozen context-menu helper.
Deploying podnotes with
|
| Latest commit: |
a1b230b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6e1d2055.podnotes.pages.dev |
| Branch Preview URL: | https://chhoumann-80-reorder-queue.podnotes.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a1b230bf36
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }), | ||
| add: (episode: Episode) => { | ||
| update((queue) => { | ||
| if (isAlreadyQueued(queue.episodes, episode)) return queue; |
There was a problem hiding this comment.
Preserve same-titled episodes from different podcasts
When the queue already contains an episode titled, for example, "Trailer" from one podcast, this title-only duplicate check makes adding a different podcast's "Trailer" silently no-op; the new dedupeEpisodesByTitle path applies the same identity on load/import and can drop previously queued episodes. Episodes carry podcastName and the repo already has getEpisodeKey specifically to avoid same-title collisions, so the queue should dedupe/reorder by a composite episode key rather than title alone.
Useful? React with 👍 / 👎.
# [2.17.0](2.16.0...2.17.0) (2026-06-22) ### Bug Fixes * behavioral-audit logic and robustness fixes (back-end, 1/2) ([#213](#213)) ([4e2845d](4e2845d)) * **download:** default per-episode download path and migrate empty default ([#183](#183)) ([#186](#186)) ([46a6486](46a6486)) * **download:** prevent Android crash and create missing folders on download ([#178](#178)) ([ecd09d5](ecd09d5)), closes [#113](#113) [#86](#86) [#113](#113) [#86](#86) * **lifecycle:** defer mobile podcast view startup ([#208](#208)) ([8bf9ce4](8bf9ce4)) * **notes:** cap note path length and harden folder creation ([#22](#22), [#87](#87)) ([#192](#192)) ([858e280](858e280)), closes [#87-class](#87) * **playback:** persist listened time during playback ([#33](#33)) ([#190](#190)) ([e3433c2](e3433c2)), closes [#191](#191) [#108](#108) [#163](#163) [#183](#183) * **playback:** play local files and downloads on iOS via resource path ([#100](#100)) ([#184](#184)) ([12c503a](12c503a)) * **player:** clear progress on episode switch to stop end-of-playback glitch ([#94](#94)) ([#194](#194)) ([59dccb3](59dccb3)) * **player:** reveal PodNotes view on Play with PodNotes so local files play ([#84](#84)) ([#198](#198)) ([5953625](5953625)) * **settings:** show labelled Add/Remove buttons in podcast search ([#109](#109)) ([#195](#195)) ([33399f5](33399f5)) * show downloaded episodes in the Local Files playlist ([#176](#176)) ([#177](#177)) ([184188c](184188c)) * **timestamps:** capture into the cursor's table cell without breaking the row ([#165](#165)) ([#203](#203)) ([964e342](964e342)) * **transcription:** always transcribe the currently playing episode ([#182](#182)) ([62b488a](62b488a)), closes [#107](#107) * **uri:** preserve '+' in episode titles and paths for timestamp links ([#181](#181)) ([8ad7aa5](8ad7aa5)), closes [#164](#164) * **view:** reliably reveal PodNotes view via command + ribbon icon ([#55](#55)) ([#199](#199)) ([fa1d708](fa1d708)) ### Features * add podcast segment links ([#205](#205)) ([d97e59e](d97e59e)) * **api:** expose generated episode transcripts ([e264465](e264465)), closes [#105](#105) * behavioral-audit UI and interaction fixes (front-end, 2/2) ([#215](#215)) ([894d93d](894d93d)) * **commands:** add playback rate and media timestamp controls ([#206](#206)) ([a8bb44a](a8bb44a)) * **devx:** isolated per-worktree Obsidian E2E vault wrapper ([#188](#188)) ([a8b7a4a](a8b7a4a)) * **episodes:** add a setting to control the Latest Episodes list length ([#114](#114)) ([#200](#200)) ([7b3e3c6](7b3e3c6)) * **notes:** add {{episodelink}} template tag to resume an episode from its note ([#35](#35)) ([#193](#193)) ([8c1ddd6](8c1ddd6)) * **notes:** add podcast feed-level notes ([#163](#163)) ([#187](#187)) ([db0de47](db0de47)), closes [#161](#161) [#160](#160) * **notes:** ship a Bases-friendly default episode note template ([#160](#160)) ([#201](#201)) ([209431d](209431d)), closes [#163](#163) [#183](#183) * **player:** scale episode title font size to its length ([#81](#81)) ([#202](#202)) ([659b6b8](659b6b8)) * **player:** support video episode playback ([#209](#209)) ([f92a91f](f92a91f)) * **queue:** add setting to disable queue auto-population and auto-advance ([#108](#108)) ([#185](#185)) ([cf3d73c](cf3d73c)) * **queue:** allow reordering the playback queue ([#80](#80)) ([#179](#179)) ([d994d61](d994d61)), closes [#173](#173) * **settings:** import/export settings & templates ([#180](#180)) ([a27d23d](a27d23d)), closes [#162](#162) [#162](#162) * **templates:** add {{currentDate}}, {{episodeNumber}}, {{duration}} template variables ([#189](#189)) ([ec573eb](ec573eb)), closes [#75](#75) [#34](#34) [#88](#88) [163/#186](#186) * **templates:** add episode chapters tag ([#207](#207)) ([9c98863](9c98863)) * **transcripts:** opt-in speaker diarization for transcripts ([#168](#168)) ([#204](#204)) ([a96e12f](a96e12f))
|
🎉 This PR is included in version 2.17.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Adds the ability to reorder the playback Queue (requested in #80). Until now the Queue had no reorder affordance — its order was fixed by insertion. You can now resequence it two complementary ways, both working on desktop and mobile:
Closes #80.
What changed
src/store/index.ts) — purereorderEpisodes(from, to)(defensively clamped so a-1lookup miss can never reachsplice) andqueue.move/moveUp/moveDown/moveToTop/moveToBottom. Order already persists automatically viaQueueController→settings.queue, so no settings migration. The queue is now kept title-unique on every add and set, so persisted/synced queues that contain duplicate titles are normalized on load.spawnEpisodeContextMenu.ts+ new purequeueReorderMenu.ts) — move verbs gated toViewState.Player+ in-queue + length > 1 (so they never leak into the Latest/feed/playlist menus, which share this helper). Index resolved from the live store at click time.main.ts, newQueueReorderModal.ts+ReorderQueue.svelte) — a button-list reorder window (deliberately not drag-and-drop, which is non-functional on touch). Live index resolution; Svelte 5mount/unmount.docs/docs/podcasts.md"Reordering the queue" section.moveToBottomlands last,-1/equal/out-of-range no-ops, dedup on add and set, end-to-end persistence viaQueueController), the context-menu gating helper, theReorderQueuecomponent (all four moves, disabled-control inertness, ordering/position assertions, Done-close), andQueueReorderModalmount/unmount.Constraint compliance (#173)
The Player/Episode-UI files frozen by draft #173 (
EpisodePlayer,EpisodeList,EpisodeListHeader,EpisodeListItem,PodcastView,TopBar) are untouched. The in-player queue list is reached only through the non-frozenspawnEpisodeContextMenuhelper and the new modal.Testing
Ran the full CI-equivalent suite locally, all green:
npm run test→ 186 passed (32 new).npm run docs:buildbuilds in CI (localmkdocsnot installed).Obsidian runtime verification (dev vault
dev, this branch's build, realAccidental Tech Podcastepisodes):PodNotes: Reorder Queueopened the modal — 3 numbered rows, correct per-row buttons, correct disabled states (top row's up-controls and bottom row's down-controls greyed).settings.queuebecame[689, 688, 681], confirming reorder auto-persists end-to-end. Move to top undid it; Done closed/unmounted cleanly. Vault restored afterwards (data.jsonbyte-for-byte identical to a pre-test backup).Release / behavior impact
settings.queuearray order.remove/played-cleanup were already title-keyed), and documented.