diff --git a/.planning/quick/16-add-ctrl-a-hotkey-to-multi-select-views-/16-PLAN.md b/.planning/quick/16-add-ctrl-a-hotkey-to-multi-select-views-/16-PLAN.md new file mode 100644 index 0000000..2da2867 --- /dev/null +++ b/.planning/quick/16-add-ctrl-a-hotkey-to-multi-select-views-/16-PLAN.md @@ -0,0 +1,228 @@ +--- +phase: 16-add-ctrl-a-hotkey-to-multi-select-views- +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - frontend/src/utils/selection-controller.ts + - frontend/src/services/keyboard-shortcut-service.ts + - frontend/src/components/track-list/track-list.ts + - frontend/src/components/queue-panel/queue-panel.ts + - frontend/src/components/playlist-view/playlist-view.ts +autonomous: true +requirements: [QUICK-16] + +must_haves: + truths: + - "Pressing Ctrl+A in the track list selects all visible tracks" + - "Pressing Ctrl+A in the queue panel selects all queue items" + - "Pressing Ctrl+A in the playlist view selects all tracks in the expanded playlist" + - "Ctrl+A does NOT fire when a text input is focused" + - "Selection count updates immediately after Ctrl+A" + artifacts: + - path: "frontend/src/utils/selection-controller.ts" + provides: "selectAll() method on SelectionController" + contains: "selectAll" + - path: "frontend/src/services/keyboard-shortcut-service.ts" + provides: "app.selectAll dispatches custom event instead of document.execCommand" + contains: "shortcut:select-all" + key_links: + - from: "frontend/src/services/keyboard-shortcut-service.ts" + to: "track-list, queue-panel, playlist-view" + via: "CustomEvent('shortcut:select-all') on document" + pattern: "shortcut:select-all" +--- + + +Wire Ctrl+A to select all items in multi-select views (track-list, queue-panel, playlist-view). + +Purpose: Currently `app.selectAll` calls `document.execCommand('selectAll')` which is the browser's text selection — useless for the app's track/queue selection. This needs to trigger the SelectionController's select-all in whichever panel is focused. + +Output: Ctrl+A selects all items in the active multi-select view using the existing shortcut infrastructure and SelectionController. + + + +@/home/caleb/.config/Claude/get-shit-done/workflows/execute-plan.md +@/home/caleb/.config/Claude/get-shit-done/templates/summary.md + + + +@frontend/src/utils/selection-controller.ts +@frontend/src/services/keyboard-shortcut-service.ts +@frontend/src/components/track-list/track-list.ts +@frontend/src/components/queue-panel/queue-panel.ts +@frontend/src/components/playlist-view/playlist-view.ts +@backend/shortcuts/config.go + + + + +From frontend/src/utils/selection-controller.ts: +```typescript +export interface SelectionHost extends ReactiveControllerHost { + getItemKey(index: number): string | undefined; + getItemCount(): number; + onSelectionChanged?(): void; +} + +export class SelectionController implements ReactiveController { + get selectedItems(): ReadonlySet; + get hasSelection(): boolean; + get selectionCount(): number; + isSelected(key: string): boolean; + handleItemClick(e: MouseEvent, key: string, index: number): void; + handleContextMenu(key: string): void; + clear(): void; + getSelectedKeysOrdered(): string[]; + getSelectedIndices(): number[]; +} +``` + +From frontend/src/services/keyboard-shortcut-service.ts (dispatch function): +```typescript +// Existing pattern for panel-specific shortcuts: +case 'tracklist.play': + document.dispatchEvent(new CustomEvent('shortcut:tracklist-play')); + break; +case 'tracklist.delete': + document.dispatchEvent(new CustomEvent('shortcut:tracklist-delete')); + break; +``` + +From backend/shortcuts/config.go (existing binding): +```go +"app.selectAll": "Ctrl+A", // Already bound — just need to change the dispatch action +``` + + + + + + + Task 1: Add selectAll() to SelectionController and change app.selectAll dispatch + + frontend/src/utils/selection-controller.ts + frontend/src/services/keyboard-shortcut-service.ts + + + **SelectionController** (`frontend/src/utils/selection-controller.ts`): + Add a `selectAll()` method in the ACTIONS section (after `clear()`). It should: + 1. Get the total count via `this.host.getItemCount()` + 2. Build a new Set of all item keys by iterating `0..count-1` calling `this.host.getItemKey(i)` + 3. Skip any `undefined` keys (same pattern as `getSelectedKeysOrdered`) + 4. If the resulting set has the same size as `_selectedItems`, return early (already all selected) + 5. Set `this._selectedItems = newSet` + 6. Set `this.lastSelectedIndex = count - 1` (anchor at end for subsequent shift-click) + 7. Call `this.host.requestUpdate()` and `this.host.onSelectionChanged?.()` + + ```typescript + /** Select all items. */ + selectAll(): void { + const count = this.host.getItemCount(); + const next = new Set(); + + for (let i = 0; i < count; i++) { + const key = this.host.getItemKey(i); + if (key !== undefined) next.add(key); + } + + if (next.size === this._selectedItems.size) return; + + this._selectedItems = next; + this.lastSelectedIndex = count > 0 ? count - 1 : null; + this.host.requestUpdate(); + this.host.onSelectionChanged?.(); + } + ``` + + **keyboard-shortcut-service.ts** (`frontend/src/services/keyboard-shortcut-service.ts`): + In the `dispatch()` function, change the `app.selectAll` case from: + ```typescript + case 'app.selectAll': + document.execCommand('selectAll'); + break; + ``` + To: + ```typescript + case 'app.selectAll': + document.dispatchEvent( + new CustomEvent('shortcut:select-all'), + ); + break; + ``` + + This follows the exact same pattern used by `tracklist.play` and `tracklist.delete`. + + + cd frontend && npx tsc --noEmit 2>&1 | head -30 + + SelectionController has a selectAll() method, and Ctrl+A dispatches a 'shortcut:select-all' CustomEvent instead of document.execCommand('selectAll') + + + + Task 2: Wire select-all event listener in track-list, queue-panel, and playlist-view + + frontend/src/components/track-list/track-list.ts + frontend/src/components/queue-panel/queue-panel.ts + frontend/src/components/playlist-view/playlist-view.ts + + + Add a `shortcut:select-all` event listener to each of the three components that use SelectionController. Use the same lifecycle pattern — add in `connectedCallback`, remove in `disconnectedCallback` with a bound handler. + + **For each component (track-list, queue-panel, playlist-view):** + + 1. Add a bound handler method that calls `this.selection.selectAll()`: + ```typescript + private handleSelectAll = (): void => { + this.selection.selectAll(); + }; + ``` + + 2. In `connectedCallback()` (create one if it doesn't exist, calling `super.connectedCallback()`): + ```typescript + document.addEventListener('shortcut:select-all', this.handleSelectAll); + ``` + + 3. In `disconnectedCallback()` (create one if it doesn't exist, calling `super.disconnectedCallback()`): + ```typescript + document.removeEventListener('shortcut:select-all', this.handleSelectAll); + ``` + + **IMPORTANT consideration for playlist-view**: The playlist-view has a dual-mode UI (playlist list vs expanded playlist tracks). `getItemCount()` and `getItemKey()` already handle this — when a playlist is expanded, they return the tracks for that playlist; when no playlist is expanded, they return playlist entries. So `selectAll()` will naturally select all items in whatever mode is active. + + **IMPORTANT consideration for multiple listeners**: All three components may be connected simultaneously (track-list in the main panel, queue-panel as a sidebar, playlist-view as a panel). This is correct behavior — `selectAll()` on a component that has 0 items (e.g., queue-panel when it's closed or has no items) is harmless since `getItemCount()` returns 0 and the early-return guard in `selectAll()` triggers (0 === 0). The user's focused panel receives the visual feedback. This matches how `tracklist.play` and `tracklist.delete` already broadcast to all listeners. + + **Check if connectedCallback/disconnectedCallback already exist** in each component before adding. If they exist, add the addEventListener/removeEventListener lines to the existing methods. If they don't exist, create them. + + + cd frontend && npx tsc --noEmit 2>&1 | head -30 && echo "--- lint ---" && cd .. && make lint 2>&1 | tail -20 + + All three multi-select components (track-list, queue-panel, playlist-view) listen for 'shortcut:select-all' and call selection.selectAll(). Pressing Ctrl+A selects all items in all connected views. + + + + + +1. `cd frontend && npx tsc --noEmit` — TypeScript compiles without errors +2. `make lint` — passes golangci-lint and any frontend linting +3. `make build` or `wails build` — full build succeeds +4. Manual: Open app → navigate to track list → press Ctrl+A → all tracks highlight as selected +5. Manual: Open queue panel with items → press Ctrl+A → all queue items highlight as selected +6. Manual: Open playlist view, expand a playlist → press Ctrl+A → all playlist tracks highlight as selected +7. Manual: Focus a text input (search bar) → press Ctrl+A → text is selected (browser default), NOT track selection + + + +- Ctrl+A selects all items in track-list, queue-panel, and playlist-view +- Selection count badge/indicator updates to show total count +- Text input focus is not affected (shortcut service's text-input scope suppression handles this) +- No TypeScript compilation errors +- Linting passes + + + +After completion, create `.planning/quick/16-add-ctrl-a-hotkey-to-multi-select-views-/16-SUMMARY.md` +