286 lines
12 KiB
Markdown
286 lines
12 KiB
Markdown
---
|
|
phase: 17-single-track-edit
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- backend/tagwriter/pipeline.go
|
|
- backend/frontendutil/frontendutil.go
|
|
- frontend/src/store/library-store.ts
|
|
- frontend/src/components/track-list/track-list.ts
|
|
- frontend/src/components/queue-panel/queue-panel.ts
|
|
- frontend/src/components/cover-grid/cover-grid.ts
|
|
- frontend/src/components/playlist-details/playlist-details.ts
|
|
autonomous: true
|
|
requirements:
|
|
- EDIT-01
|
|
- EDIT-04
|
|
|
|
must_haves:
|
|
truths:
|
|
- "WriteTrackTagsByPath accepts a file path string and TagChanges, resolves the track ID internally, and delegates to WriteTrackTags"
|
|
- "ImageFilePicker opens a native file dialog filtered to JPEG/PNG and returns the selected file path"
|
|
- "After a successful tag write, the library store invalidates all caches and re-fetches data so all views reflect the new metadata"
|
|
- "Right-clicking any single track in track-list, queue-panel, cover-grid, or playlist-details shows 'Track Details' in the context menu regardless of selection state"
|
|
artifacts:
|
|
- path: "backend/tagwriter/pipeline.go"
|
|
provides: "WriteTrackTagsByPath method on TagWriter"
|
|
contains: "func (tw *TagWriter) WriteTrackTagsByPath"
|
|
- path: "backend/frontendutil/frontendutil.go"
|
|
provides: "ImageFilePicker method for cover art selection"
|
|
contains: "func (fe *FrontendUtil) ImageFilePicker"
|
|
- path: "frontend/src/store/library-store.ts"
|
|
provides: "TrackMetadataChanged event handler calling invalidate()"
|
|
contains: "TrackMetadataChanged"
|
|
- path: "frontend/src/components/track-list/track-list.ts"
|
|
provides: "Track Details context menu item visible for any right-clicked track"
|
|
- path: "frontend/src/components/queue-panel/queue-panel.ts"
|
|
provides: "Track Details context menu item visible for any right-clicked track"
|
|
key_links:
|
|
- from: "frontend/src/store/library-store.ts"
|
|
to: "backend events"
|
|
via: "EventsOn(Events.TrackMetadataChanged)"
|
|
pattern: "EventsOn.*TrackMetadataChanged"
|
|
- from: "backend/tagwriter/pipeline.go"
|
|
to: "backend/database"
|
|
via: "GetAudioFileByPath query"
|
|
pattern: "GetAudioFileByPath"
|
|
---
|
|
|
|
<objective>
|
|
Wire the backend bridge methods and frontend plumbing needed for single-track tag editing.
|
|
|
|
Purpose: Phase 17 builds on the WriteTrackTags pipeline from Phase 16. The frontend identifies tracks by `FilePath` but WriteTrackTags requires `trackID int64`. This plan adds a path-based wrapper, a cover art file picker, the library store event handler that refreshes views after edits, and removes the selection-count gate on the "Track Details" context menu item.
|
|
|
|
Output: Backend methods ready for frontend consumption, library store reacts to tag write events, context menu accessible from any track context.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
|
|
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/17-single-track-edit/17-CONTEXT.md
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. -->
|
|
|
|
From backend/tagwriter/pipeline.go:
|
|
```go
|
|
type TagWriter struct {
|
|
logger *slog.Logger
|
|
db *database.DB
|
|
ctx context.Context
|
|
player PlayerStopper
|
|
library PipelineLocker
|
|
}
|
|
|
|
func (tw *TagWriter) WriteTrackTags(trackID int64, changes TagChanges) error
|
|
```
|
|
|
|
From backend/tagwriter/tagwriter.go:
|
|
```go
|
|
type TagChanges map[string]any
|
|
|
|
const (
|
|
FieldTitle = "title"
|
|
FieldArtist = "artist"
|
|
FieldAlbum = "album"
|
|
FieldAlbumArtist = "album_artist"
|
|
FieldGenre = "genre"
|
|
FieldYear = "year"
|
|
FieldTrackNumber = "track_number"
|
|
FieldDiscNumber = "disc_number"
|
|
FieldComposer = "composer"
|
|
FieldCoverArt = "cover_art"
|
|
)
|
|
```
|
|
|
|
From backend/database/sql/sqlcgen/audio_files.sql.go:
|
|
```go
|
|
func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (AudioFile, error)
|
|
```
|
|
|
|
From backend/frontendutil/frontendutil.go:
|
|
```go
|
|
type FrontendUtil struct {
|
|
ctx context.Context
|
|
}
|
|
func (fe *FrontendUtil) DirectoryPicker() (string, error)
|
|
func (fe *FrontendUtil) PlaylistFilePicker() ([]string, error)
|
|
```
|
|
|
|
From frontend/src/store/library-store.ts:
|
|
```typescript
|
|
class LibraryStore {
|
|
private invalidate(): void { ... }
|
|
// Currently listens for: LibraryScanComplete, LibraryRemoved, LibraryAdded, LibraryRenamed
|
|
// Does NOT listen for TrackMetadataChanged
|
|
}
|
|
```
|
|
|
|
From frontend/src/events.ts:
|
|
```typescript
|
|
export const Events = {
|
|
TrackMetadataChanged: "TrackMetadataChanged",
|
|
// ...
|
|
} as const;
|
|
```
|
|
|
|
Context menu pattern (track-list.ts line ~1966):
|
|
```typescript
|
|
${this.selection.selectionCount === 1
|
|
? html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>
|
|
<wa-icon slot="icon" name="circle-info"></wa-icon>
|
|
Track Details
|
|
</wa-dropdown-item>` : nothing}
|
|
```
|
|
|
|
queue-panel.ts uses the same pattern at line ~1553.
|
|
cover-grid.ts and playlist-details.ts conditionally show Track Details only for single track context.
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add WriteTrackTagsByPath and ImageFilePicker backend methods</name>
|
|
<files>backend/tagwriter/pipeline.go, backend/frontendutil/frontendutil.go</files>
|
|
<action>
|
|
**In `backend/tagwriter/pipeline.go`**, add a new method `WriteTrackTagsByPath` directly below the existing `WriteTrackTags` method:
|
|
|
|
```go
|
|
// WriteTrackTagsByPath resolves a file path to its audio_file.id and
|
|
// delegates to WriteTrackTags. This is the frontend-facing entry
|
|
// point since the frontend identifies tracks by FilePath.
|
|
func (tw *TagWriter) WriteTrackTagsByPath(filePath string, changes TagChanges) error {
|
|
ctx := context.Background()
|
|
|
|
audioFile, err := tw.db.Queries.GetAudioFileByPath(ctx, filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve track by path %q: %w", filePath, err)
|
|
}
|
|
|
|
return tw.WriteTrackTags(audioFile.ID, changes)
|
|
}
|
|
```
|
|
|
|
This uses the existing `GetAudioFileByPath` sqlc query (already generated) to look up the `audio_files.id` from `file_path`, then delegates to the existing `WriteTrackTags` pipeline.
|
|
|
|
**In `backend/frontendutil/frontendutil.go`**, add a new method `ImageFilePicker` below `PlaylistFilePicker`:
|
|
|
|
```go
|
|
// ImageFilePicker opens a file selection dialog filtered to image
|
|
// files (JPEG, PNG). Returns the selected file path, or empty
|
|
// string if the user cancelled.
|
|
func (fe *FrontendUtil) ImageFilePicker() (string, error) {
|
|
file, err := runtime.OpenFileDialog(
|
|
fe.ctx,
|
|
runtime.OpenDialogOptions{
|
|
Title: "Select Cover Art",
|
|
Filters: []runtime.FileFilter{
|
|
{
|
|
DisplayName: "Image Files (*.jpg, *.jpeg, *.png)",
|
|
Pattern: "*.jpg;*.jpeg;*.png",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not open file dialog: %w", err)
|
|
}
|
|
|
|
return file, nil
|
|
}
|
|
```
|
|
|
|
After adding both methods, run `make generate` to regenerate Wails TypeScript bindings (this will create the `WriteTrackTagsByPath` and `ImageFilePicker` bindings in `frontend/wailsjs/go/`).
|
|
|
|
Ensure both methods follow codebase conventions: doc comments ending with periods, error wrapping with `%w`, `fmt.Errorf` context.
|
|
</action>
|
|
<verify>
|
|
`go build -tags webkit2_41 ./...` compiles without errors.
|
|
`make generate` succeeds and creates new TypeScript bindings.
|
|
`rg "WriteTrackTagsByPath" frontend/wailsjs/go/tagwriter/` shows the generated binding.
|
|
`rg "ImageFilePicker" frontend/wailsjs/go/frontendutil/` shows the generated binding.
|
|
</verify>
|
|
<done>
|
|
WriteTrackTagsByPath method exists on TagWriter, resolves filePath→trackID via GetAudioFileByPath, delegates to WriteTrackTags.
|
|
ImageFilePicker method exists on FrontendUtil, opens native file dialog filtered to JPEG/PNG, returns selected path.
|
|
Both have TypeScript bindings generated.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Add TrackMetadataChanged handler and fix context menu conditions</name>
|
|
<files>frontend/src/store/library-store.ts, frontend/src/components/track-list/track-list.ts, frontend/src/components/queue-panel/queue-panel.ts, frontend/src/components/cover-grid/cover-grid.ts, frontend/src/components/playlist-details/playlist-details.ts</files>
|
|
<action>
|
|
**In `frontend/src/store/library-store.ts`**, add a `TrackMetadataChanged` event listener in the constructor, after the existing `LibraryRenamed` listener:
|
|
|
|
```typescript
|
|
EventsOn(Events.TrackMetadataChanged, () => {
|
|
this.invalidate();
|
|
});
|
|
```
|
|
|
|
This causes a full cache invalidation + re-fetch of all library data (tracks, albums, artists, genres) whenever any track's tags are written. Full reload is acceptable per the CONTEXT.md decision: "Full reload is acceptable because editing is a low-frequency operation."
|
|
|
|
**In `frontend/src/components/track-list/track-list.ts`**, find the context menu rendering where "Track Details" is conditionally shown (around line 1966). Change the condition from `this.selection.selectionCount === 1` to always show the item. The item should appear when right-clicking any track. Per CONTEXT.md: "'Track Details' should appear in the context menu when right-clicking any track, regardless of selection state."
|
|
|
|
Replace:
|
|
```typescript
|
|
${this.selection.selectionCount === 1
|
|
? html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>
|
|
```
|
|
With:
|
|
```typescript
|
|
${html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>
|
|
```
|
|
Remove the corresponding `: nothing}` closing.
|
|
|
|
When `track-details` action is triggered with multiple selections, use the first selected track (or the right-clicked track). Check how `onContextMenuAction` resolves the target — it should use the context menu target row's `FilePath`, not require exactly 1 selection.
|
|
|
|
**In `frontend/src/components/queue-panel/queue-panel.ts`**, apply the same fix: remove the `selectionCount === 1` condition around the "Track Details" context menu item (around line 1553). The queue always has a specific right-click target (the clicked track row), so Track Details should always be available.
|
|
|
|
**In `frontend/src/components/cover-grid/cover-grid.ts`**, find the Track Details context menu item condition (it checks `contextMenuTarget.kind === 'track' && selectedTracks.size === 1`). Change to only check `contextMenuTarget.kind === 'track'` — the dialog opens for the right-clicked track regardless of multi-selection.
|
|
|
|
**In `frontend/src/components/playlist-details/playlist-details.ts`**, apply the same fix: remove the `selectionCount === 1` condition for the "Track Details" menu item.
|
|
|
|
For all 4 components: when Track Details is activated with multiple tracks selected, the `openTrackDetails` method should open details for the first selected track (or the context-menu-target track). Review each component's `openTrackDetails` to ensure it works with the right-clicked track, not the full selection.
|
|
</action>
|
|
<verify>
|
|
`pnpm run typecheck` in frontend/ passes.
|
|
`rg "TrackMetadataChanged" frontend/src/store/library-store.ts` shows the new event handler.
|
|
`rg "selectionCount === 1" frontend/src/components/track-list/track-list.ts frontend/src/components/queue-panel/queue-panel.ts` returns no matches (condition removed).
|
|
</verify>
|
|
<done>
|
|
TrackMetadataChanged event handler added to LibraryStore — after a tag write, all caches are invalidated and views refresh.
|
|
"Track Details" context menu item appears when right-clicking any track in all 4 views (track-list, queue-panel, cover-grid, playlist-details) regardless of how many tracks are selected.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `go build -tags webkit2_41 ./...` compiles
|
|
- `pnpm run typecheck` (in frontend/) passes
|
|
- `make generate` succeeds
|
|
- WriteTrackTagsByPath binding exists in `frontend/wailsjs/go/tagwriter/`
|
|
- ImageFilePicker binding exists in `frontend/wailsjs/go/frontendutil/`
|
|
- LibraryStore listens for TrackMetadataChanged
|
|
- "Track Details" context menu item no longer gated on single selection
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
Backend methods ready for Plan 02 to call from the track-details dialog. Library store will automatically refresh all views when tag writes complete. Context menu shows "Track Details" for any right-clicked track.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/17-single-track-edit/17-01-SUMMARY.md`
|
|
</output>
|