diff --git a/.planning/STATE.md b/.planning/STATE.md index a52190d..84ee2ae 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -83,14 +83,15 @@ None currently. | 004 | Add "set as default playlist" context menu option for single playlist selection | 2026-02-28 | 9971b63 | [4-add-set-as-default-playlist-context-menu](./quick/4-add-set-as-default-playlist-context-menu/) | | 005 | Add sort dropdown to playlist view | 2026-03-01 | 5c07485 | [5-add-sort-dropdown-to-playlist-view](./quick/5-add-sort-dropdown-to-playlist-view/) | | 006 | Remove list icon from playlist names, add favorites icon to default | 2026-03-01 | 3c19766 | [6-remove-list-icon-from-playlist-names-and](./quick/6-remove-list-icon-from-playlist-names-and/) | +| 007 | Pin default playlist to top of playlist view | 2026-03-01 | e6378e1 | [7-pin-default-playlist-to-top-of-playlist-](./quick/7-pin-default-playlist-to-top-of-playlist-/) | ## Session Continuity ### Last Session **Date:** 2026-03-01 -**What happened:** Executed quick task 006 — removed list icon from playlist names, added favorites icon (heart/star) to default playlist only -**Where we stopped:** Completed quick task 006 (all tasks, verification passed) +**What happened:** Executed quick task 007 — pin default playlist to top of playlist view with config toggle +**Where we stopped:** Completed quick task 007 (all tasks, verification passed) **Next action:** `/gsd-plan-phase 2` to create execution plan for Backend Correctness ### Context for Next Session @@ -101,8 +102,9 @@ None currently. - Ready for Phase 2 (Backend Correctness) — error handling, config permissions, MPRIS errors - Quick task 005: Playlist view now has sort dropdown (Recent, Name, Date Created, Track Count) with persistent preferences - Quick task 006: Playlist list icon removed; default playlist shows favorites icon (heart/star per config), others show no icon +- Quick task 007: Default playlist pinned to top of playlist list (configurable toggle in Settings > Favorites) --- *State initialized: 2026-02-27* -Last activity: 2026-03-01 - Completed quick task 006: Remove list icon from playlist names, add favorites icon to default +Last activity: 2026-03-01 - Completed quick task 007: Pin default playlist to top of playlist view *Last updated: 2026-03-01* diff --git a/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-PLAN.md b/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-PLAN.md new file mode 100644 index 0000000..7d7d290 --- /dev/null +++ b/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-PLAN.md @@ -0,0 +1,291 @@ +--- +phase: quick-7 +plan: 1 +type: execute +wave: 1 +depends_on: [] +files_modified: + - backend/favorites/config.go + - backend/config/config.go + - frontend/src/store/favorites-store.ts + - frontend/src/store/controllers/favorites-controller.ts + - frontend/src/components/playlist-view/playlist-view.ts + - frontend/src/components/config-page/config-page.ts +autonomous: true +requirements: [PIN-DEFAULT-01] + +must_haves: + truths: + - "When pin is enabled, the default/favorites playlist always appears first in the playlist list regardless of sort field or direction" + - "When pin is disabled, the default playlist sorts normally with all other playlists" + - "The pin setting is toggleable from the config/settings page under the Favorites section" + - "The pin preference persists across app restarts via config.toml" + artifacts: + - path: "backend/favorites/config.go" + provides: "PinDefault bool field on Config struct" + contains: "PinDefault" + - path: "backend/config/config.go" + provides: "GetPinDefaultPlaylist and SetPinDefaultPlaylist methods" + exports: ["GetPinDefaultPlaylist", "SetPinDefaultPlaylist"] + - path: "frontend/src/store/favorites-store.ts" + provides: "pinDefault state, getter, setter, and event reactivity" + - path: "frontend/src/components/playlist-view/playlist-view.ts" + provides: "sortedEntries getter pins default playlist to top when enabled" + key_links: + - from: "frontend/src/components/playlist-view/playlist-view.ts" + to: "frontend/src/store/controllers/favorites-controller.ts" + via: "favCtrl.pinDefault and favCtrl.playlistId in sortedEntries" + pattern: "this\\.favCtrl\\.pinDefault" + - from: "frontend/src/store/favorites-store.ts" + to: "backend/config/config.go" + via: "GetPinDefaultPlaylist/SetPinDefaultPlaylist Wails bindings" + pattern: "(Get|Set)PinDefaultPlaylist" + - from: "backend/config/config.go" + to: "frontend/src/store/favorites-store.ts" + via: "FavoritesConfigChanged event includes PinDefault field" + pattern: "PinDefault" +--- + + +Pin the default/favorites playlist to the top of the playlist view regardless of sort order, controlled by a toggleable config setting. + +Purpose: Users who rely on a favorites playlist want instant access without scrolling/sorting to find it. +Output: Full-stack feature — config field, backend getter/setter, frontend store/controller, sort logic, and settings toggle. + + + +@/home/caleb/.config/Claude/get-shit-done/workflows/execute-plan.md +@/home/caleb/.config/Claude/get-shit-done/templates/summary.md + + + +@backend/favorites/config.go +@backend/config/config.go +@frontend/src/store/favorites-store.ts +@frontend/src/store/controllers/favorites-controller.ts +@frontend/src/components/playlist-view/playlist-view.ts +@frontend/src/components/config-page/config-page.ts + + + + +From backend/favorites/config.go: +```go +type Config struct { + PlaylistID int64 `toml:"PlaylistID"` + IconStyle IconStyle `toml:"IconStyle"` +} +``` + +From backend/config/config.go: +```go +// Pattern for getter/setter — follow GetFavoritesPlaylistID / SetFavoritesPlaylistID exactly +func (c *Config) GetFavoritesPlaylistID() int64 { ... } +func (c *Config) SetFavoritesPlaylistID(id int64) error { ... } +func (c *Config) emitFavoritesChanged() { + runtime.EventsEmit(c.ctx, events.FavoritesConfigChanged, map[string]any{ + "PlaylistID": c.Favorites.PlaylistID, + "IconStyle": string(c.Favorites.IconStyle), + }) +} +``` + +From frontend/src/store/favorites-store.ts: +```typescript +// Event handler in constructor: +EventsOn(Events.FavoritesConfigChanged, (data: { + PlaylistID: number; + IconStyle: string; +}) => { ... }); + +// loadConfig pattern: +private async loadConfig(): Promise { + const [id, style] = await Promise.all([ + GetFavoritesPlaylistID(), + GetFavoritesIconStyle(), + ]); + ... +} +``` + +From frontend/src/components/playlist-view/playlist-view.ts: +```typescript +private get sortedEntries(): PlaylistEntry[] { + const entries = this.filteredEntries; + const dir = this.sortDirection === 'asc' ? 1 : -1; + return [...entries].sort((a, b) => { ... }); +} +``` + +From frontend/src/components/config-page/config-page.ts: +```typescript +// Favorites section uses config-field with type: 'select' +// Pattern for toggle: use type: 'toggle' with boolean value +private renderFavoritesSection() { ... } +``` + + + + + + + Task 1: Add PinDefault to backend config and expose getter/setter + + backend/favorites/config.go + backend/config/config.go + + +1. In `backend/favorites/config.go`, add `PinDefault bool` field to the `Config` struct with TOML tag `"PinDefault"`. Default should be `true` (pin enabled by default). Update `ApplyDefaults()` — since Go zero-value for bool is false, add a separate mechanism: add a `pinDefaultSet bool` unexported field (no toml tag) to track if PinDefault was explicitly set, OR simpler: just document that the default is applied in `config.go`'s `applyDefaults`. Actually, simplest approach: since `bool` zero-value is `false`, and we want default `true`, handle this in `config.go`'s `applyDefaults()` method by setting `c.Favorites.PinDefault = true` when initializing a new Favorites config. No validation needed for a bool field. + +2. In `backend/config/config.go`: + - Add `GetPinDefaultPlaylist() bool` method following the exact pattern of `GetFavoritesPlaylistID()`: + ```go + func (c *Config) GetPinDefaultPlaylist() bool { + if c.Favorites == nil { + return true // default: pinned + } + return c.Favorites.PinDefault + } + ``` + - Add `SetPinDefaultPlaylist(pin bool) error` method following the pattern of `SetFavoritesPlaylistID()`: + - Ensure `c.Favorites` is initialized (same nil guard pattern) + - Set `c.Favorites.PinDefault = pin` + - Call `c.Save()`, return error if save fails + - Call `c.emitFavoritesChanged()` + - Log the change + - Update `emitFavoritesChanged()` to include `"PinDefault": c.Favorites.PinDefault` in the event payload map + - In the `applyDefaults()` method, ensure when creating a new `favorites.Config{}`, `PinDefault` is set to `true` + +NOTE: The Wails bindings (`frontend/wailsjs/go/config/Config.js` and `.d.ts`) are auto-generated by `wails generate module`. Run `wails generate module` after making Go changes, or if not available, manually add the binding stubs to match the pattern of existing bindings. + + + Run `go build ./...` from the backend directory to verify compilation. Grep for `PinDefault` in `backend/` to confirm it appears in both files. + + + - `favorites.Config` has `PinDefault bool` field with TOML tag + - `config.Config` has `GetPinDefaultPlaylist()` and `SetPinDefaultPlaylist()` methods + - `emitFavoritesChanged` includes `PinDefault` in event payload + - Default value is `true` (pin enabled) + - Code compiles without errors + + + + + Task 2: Wire frontend store, controller, playlist-view sort logic, and config page toggle + + frontend/src/store/favorites-store.ts + frontend/src/store/controllers/favorites-controller.ts + frontend/src/components/playlist-view/playlist-view.ts + frontend/src/components/config-page/config-page.ts + frontend/wailsjs/go/config/Config.js + frontend/wailsjs/go/config/Config.d.ts + + +1. **Wails bindings** — Add `GetPinDefaultPlaylist` and `SetPinDefaultPlaylist` to `frontend/wailsjs/go/config/Config.js` and `.d.ts` following the exact pattern of the existing exports (e.g. `GetFavoritesPlaylistID`/`SetFavoritesPlaylistID`): + - In `.d.ts`: `export function GetPinDefaultPlaylist():Promise;` and `export function SetPinDefaultPlaylist(arg1:boolean):Promise;` + - In `.js`: Follow the exact `window['go']['config']['Config']['MethodName']` pattern used by other exports + +2. **favorites-store.ts**: + - Import `GetPinDefaultPlaylist` and `SetPinDefaultPlaylist` from `@go/config/Config` + - Add `private pinDefault = true;` field (default true) + - Add `getPinDefault(): boolean` getter + - Add `async setPinDefault(pin: boolean): Promise` action (same pattern as `setIconStyle`) + - In `loadConfig()`: add `GetPinDefaultPlaylist()` to the `Promise.all` call, store result in `this.pinDefault` + - In the `FavoritesConfigChanged` event handler: read `data.PinDefault` (as `boolean`) and store in `this.pinDefault`, then notify + +3. **favorites-controller.ts**: + - Add `get pinDefault(): boolean` getter that delegates to `favoritesStore.getPinDefault()` + - Add `async setPinDefault(pin: boolean): Promise` that delegates to `favoritesStore.setPinDefault(pin)` + +4. **playlist-view.ts** — Update `sortedEntries` getter to pin default playlist when enabled: + ```typescript + private get sortedEntries(): PlaylistEntry[] { + const entries = this.filteredEntries; + const dir = this.sortDirection === 'asc' ? 1 : -1; + + const sorted = [...entries].sort((a, b) => { + // Pin default playlist to top when enabled + if (this.favCtrl.pinDefault) { + const aIsDefault = a.summary.ID === this.favCtrl.playlistId; + const bIsDefault = b.summary.ID === this.favCtrl.playlistId; + if (aIsDefault && !bIsDefault) return -1; + if (!aIsDefault && bIsDefault) return 1; + } + + let cmp = 0; + switch (this.sortField) { + // ... existing sort cases unchanged + } + return cmp * dir; + }); + + return sorted; + } + ``` + +5. **config-page.ts** — Add a toggle in `renderFavoritesSection()` AFTER the existing Icon Style field: + ```typescript + + ``` + - Add handler `private handlePinDefaultChange`: + ```typescript + private handlePinDefaultChange = ( + e: CustomEvent, + ): void => { + const pin = Boolean(e.detail.value); + this.favCtrl + .setPinDefault(pin) + .catch((err: unknown) => { + console.error('Failed to set pin default:', err); + }); + }; + ``` + +IMPORTANT: Check if `config-field` supports `type: 'toggle'`. If not, check what boolean toggle type it supports (could be `'switch'` or `'checkbox'`). Look at the config-field component to determine the correct type string. If `toggle` isn't supported, use whatever boolean field type the component supports. + + + Run `npm run build` (or the project's frontend build command) from the frontend directory to verify TypeScript compilation. Visually verify by launching the app that: (1) The favorites playlist appears at the top of the playlist list regardless of sort, (2) The setting toggle appears in Settings > Favorites, (3) Disabling the toggle causes the favorites playlist to sort normally. + + + - Favorites store exposes `pinDefault` state with getter/setter + - FavoritesController exposes `pinDefault` getter and `setPinDefault` action + - `sortedEntries` in playlist-view pins default playlist to index 0 when `pinDefault` is true + - Config page shows "Pin to Top" toggle in Favorites section + - Toggling the setting immediately updates the playlist view (reactive via store subscription) + - Setting persists across app restarts (saved to config.toml via backend) + - Frontend builds without TypeScript errors + + + + + + +1. `go build ./...` passes (backend compiles) +2. Frontend build passes (TypeScript compiles) +3. App launches; default playlist appears pinned to top regardless of sort field/direction +4. Settings > Favorites shows "Pin to Top" toggle (default: on) +5. Disabling the toggle causes the default playlist to sort normally +6. Re-enabling the toggle immediately pins the default playlist back to the top +7. Restarting the app preserves the pin preference + + + +- Default playlist pinned to top of playlist view when setting enabled (default: enabled) +- Toggle in Settings > Favorites controls the behavior +- Setting persists in config.toml across restarts +- All other sort functionality (field + direction) works normally for non-default playlists +- No regressions to existing playlist features (sorting, filtering, drag-drop, context menu) + + + +After completion, create `.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-SUMMARY.md` + diff --git a/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-SUMMARY.md b/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-SUMMARY.md new file mode 100644 index 0000000..3dd49b5 --- /dev/null +++ b/.planning/quick/7-pin-default-playlist-to-top-of-playlist-/7-SUMMARY.md @@ -0,0 +1,72 @@ +--- +phase: quick-7 +plan: 1 +subsystem: favorites +tags: [config, playlist, sort, favorites, full-stack] +dependency_graph: + requires: [] + provides: [pin-default-playlist] + affects: [playlist-view, config-page, favorites-store] +tech_stack: + added: [] + patterns: [toggle-config-field, sort-pinning] +key_files: + created: [] + modified: + - backend/favorites/config.go + - backend/config/config.go + - frontend/src/store/favorites-store.ts + - frontend/src/store/controllers/favorites-controller.ts + - frontend/src/components/playlist-view/playlist-view.ts + - frontend/src/components/config-page/config-page.ts + - frontend/wailsjs/go/config/Config.js + - frontend/wailsjs/go/config/Config.d.ts +decisions: + - "Default PinDefault to true for new installs; existing configs without the field get Go zero-value (false) from TOML" +metrics: + duration: 10 min + completed: "2026-03-01" +--- + +# Quick Task 7: Pin Default Playlist to Top of Playlist View + +Full-stack pin-to-top feature: PinDefault bool config field, Go getter/setter with event emission, frontend store/controller/view wiring, and config page toggle. + +## Completed Tasks + +| # | Task | Commit | Key Changes | +|---|------|--------|-------------| +| 1 | Add PinDefault to backend config and expose getter/setter | `6e123bd` | PinDefault field on favorites.Config, Get/SetPinDefaultPlaylist methods, event payload update, applyDefaults with true | +| 2 | Wire frontend store, controller, playlist-view sort logic, and config page toggle | `e6378e1` | favorites-store pinDefault state + getter/setter, controller delegation, sortedEntries pinning logic, config-page toggle, Wails bindings | + +## Implementation Details + +### Backend (Task 1) + +- Added `PinDefault bool` with `toml:"PinDefault"` tag to `favorites.Config` struct +- Added `GetPinDefaultPlaylist() bool` — returns `true` when `Favorites` is nil (safe default) +- Added `SetPinDefaultPlaylist(pin bool) error` — follows existing setter pattern (nil guard, save, emit, log) +- Updated `emitFavoritesChanged()` to include `"PinDefault"` in the event payload map +- In `applyDefaults()`, new `favorites.Config` structs are created with `PinDefault: true` + +### Frontend (Task 2) + +- **favorites-store.ts**: Added `pinDefault` private field (default `true`), `getPinDefault()` getter, `setPinDefault()` action (optimistic update + backend call), included in `loadConfig()` Promise.all, and event handler reads `data.PinDefault` +- **favorites-controller.ts**: Added `get pinDefault(): boolean` and `async setPinDefault(pin)` delegating to store +- **playlist-view.ts**: Updated `sortedEntries` getter — when `this.favCtrl.pinDefault` is true, the playlist matching `this.favCtrl.playlistId` always sorts to index 0, regardless of sort field/direction +- **config-page.ts**: Added `` for "Pin to Top" in the Favorites section with `handlePinDefaultChange` handler +- **Wails bindings**: `GetPinDefaultPlaylist():Promise` and `SetPinDefaultPlaylist(arg1:boolean):Promise` (pre-generated) + +## Deviations from Plan + +None - plan executed exactly as written. + +## Verification + +- [x] `go build ./...` passes (backend compiles) +- [x] `npx tsc --noEmit` passes (frontend TypeScript compiles) +- [x] `config-field` supports `type: 'toggle'` (confirmed in config-field.ts) + +## Self-Check: PASSED + +All 8 modified files verified on disk. Both task commits (6e123bd, e6378e1) found in git history.