12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17-single-track-edit | 01 | execute | 1 |
|
true |
|
|
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.
<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>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-single-track-edit/17-CONTEXT.mdFrom backend/tagwriter/pipeline.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:
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:
func (q *Queries) GetAudioFileByPath(ctx context.Context, filePath string) (AudioFile, error)
From backend/frontendutil/frontendutil.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:
class LibraryStore {
private invalidate(): void { ... }
// Currently listens for: LibraryScanComplete, LibraryRemoved, LibraryAdded, LibraryRenamed
// Does NOT listen for TrackMetadataChanged
}
From frontend/src/events.ts:
export const Events = {
TrackMetadataChanged: "TrackMetadataChanged",
// ...
} as const;
Context menu pattern (track-list.ts line ~1966):
${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.
Task 1: Add WriteTrackTagsByPath and ImageFilePicker backend methods backend/tagwriter/pipeline.go, backend/frontendutil/frontendutil.go **In `backend/tagwriter/pipeline.go`**, add a new method `WriteTrackTagsByPath` directly below the existing `WriteTrackTags` method:// 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:
// 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.
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.
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.
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:
${this.selection.selectionCount === 1
? html`<wa-dropdown-item @click=${() => this.onContextMenuAction('track-details')}>
With:
${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.
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).
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.
<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>
After completion, create `.planning/phases/17-single-track-edit/17-01-SUMMARY.md`