added refactoring catalog, fixed context menu freezing tracklist

This commit is contained in:
2026-02-23 20:54:50 -05:00
parent d925e3d659
commit 153cd0eae0
7 changed files with 311 additions and 470 deletions
-175
View File
@@ -1,175 +0,0 @@
# Plan: Clear Queue Button
## Goal
Add a "Clear Queue" button (trash icon) next to the existing "Add queue to playlist" button in the queue panel header. The button clears all tracks from the queue, stops playback, and resets queue state.
## Architecture Overview
The backend already has a `Queue.Clear()` method (`backend/queue/queue.go:1540`) that handles everything — clearing tracks, stopping playback, resetting state, persisting, and emitting `QueueChanged`. The only missing piece is wiring it to the frontend via the event system and adding the UI button.
## Changes Required (5 files)
### 1. `backend/events/events.go` — Add new event constant
Add `RequestClearQueue` to the queue events const block.
```go
RequestMoveQueueTracks = "RequestMoveQueueTracks"
RequestClearQueue = "RequestClearQueue"
)
```
### 2. `frontend/src/events.ts` — Add matching TypeScript event constant
Add `RequestClearQueue` to the queue events section.
```typescript
RequestMoveQueueTracks: "RequestMoveQueueTracks",
RequestClearQueue: "RequestClearQueue",
```
### 3. `backend/queue/queue.go` — Wire event handler in `registerEventHandlers()`
Add a new `runtime.EventsOn` call at the end of `registerEventHandlers()` (after the existing `RequestMoveQueueTracks` handler around line 289):
```go
runtime.EventsOn(
q.ctx,
events.RequestClearQueue,
func(_ ...any) {
q.logger.Info("Received RequestClearQueue")
q.Clear()
},
)
```
### 4. `frontend/src/store/queue-store.ts` — Add `clearQueue()` action
Add after the existing `moveTracksInQueue()` method (around line 250):
```typescript
clearQueue(): void {
EventsEmit(Events.RequestClearQueue);
}
```
### 5. `frontend/src/components/queue-panel/queue-panel.ts` — Add UI button, styles, and handler
#### 5a. Add handler method
Add a new handler method near the other handlers (around line 518, near `handleAddToPlaylist`):
```typescript
private handleClearQueue = () => {
queueStore.clearQueue();
};
```
Note: Import `queueStore` — check if it's already imported (it likely is via the controller).
#### 5b. Add CSS styles for shared header button class
Add a `.header-actions` container style and refactor the button styles. Replace the existing `.add-to-playlist-button` styles:
**Replace:**
```css
.add-to-playlist-button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
}
.add-to-playlist-button:hover {
color: #ffd43b;
}
.add-to-playlist-button:disabled {
color: #555;
cursor: not-allowed;
}
```
**With:**
```css
.header-actions {
display: flex;
align-items: center;
gap: 4px;
}
.header-action-button {
background: none;
border: none;
color: inherit;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
}
.header-action-button:hover {
color: #ffd43b;
}
.header-action-button:disabled {
color: #555;
cursor: not-allowed;
}
```
#### 5c. Update the header HTML
Replace the header section (around lines 1183-1193):
**Replace:**
```html
<div class="header">
<h3>Queue</h3>
<button
class="add-to-playlist-button"
@click=${this.handleAddToPlaylist}
?disabled=${tracks.length === 0}
title="Add queue to playlist"
>
<wa-icon name="plus"></wa-icon>
</button>
</div>
```
**With:**
```html
<div class="header">
<h3>Queue</h3>
<div class="header-actions">
<button
class="header-action-button"
@click=${this.handleClearQueue}
?disabled=${tracks.length === 0}
title="Clear queue"
>
<wa-icon name="trash"></wa-icon>
</button>
<button
class="header-action-button add-to-playlist-button"
@click=${this.handleAddToPlaylist}
?disabled=${tracks.length === 0}
title="Add queue to playlist"
>
<wa-icon name="plus"></wa-icon>
</button>
</div>
</div>
```
**Important:** The `add-to-playlist-button` class must remain on the playlist button because it's referenced by `@query` selectors and the `closePickerHandler` (lines 84-85, 527). The new class `header-action-button` provides the shared visual style.
#### 5d. Update CSS selector references
Check that the `.add-to-playlist-button` query selector references still work. Since we're keeping `add-to-playlist-button` as a class on the playlist button, the existing `@query('.add-to-playlist-button')` and `querySelector('.add-to-playlist-button')` calls will continue to work unchanged.
### 6. Import check
Verify that `queueStore` is accessible in `queue-panel.ts`. The component uses a `QueueController` which wraps the store, but the `clearQueue()` call needs to go through the store directly. Check if `queueStore` is already imported; if not, add:
```typescript
import { queueStore } from '@store/queue-store';
```
## Testing
- Run `make lint` to verify Go code passes linting
- Run `cd frontend && pnpm exec tsc --noEmit` to verify TypeScript compiles
- Manual testing: click the trash button when queue has tracks → queue should clear, playback should stop, button should become disabled
-98
View File
@@ -1,98 +0,0 @@
# Fix Queue Panel Colors to Match Application
## Problem
The queue panel (`frontend/src/components/queue-panel/queue-panel.ts`) uses a blue-tinted dark background (`#1a1a2e`) and dimmer secondary text colors that don't match the rest of the application's Bootstrap-inspired neutral dark grey palette.
## Application Color Palette (established)
| Role | Color | Used by |
|------|-------|---------|
| Top bar / Bottom bar | `#343a40` | `index.css` |
| Sidebar / Main panel | `#212529` | `index.css`, `app-sidebar.ts` |
| Body background | `black` | `index.css` |
| Secondary text | `#b3b3b3` | `cover-grid.ts` (artist, empty state, loading) |
| Muted text | `#888` | various components |
| Accent | `#ffd43b` | all components (active/hover states) |
## Changes
All changes are in `frontend/src/components/queue-panel/queue-panel.ts`:
### 1. Background color (line 27)
```css
/* Before */
background-color: #1a1a2e;
/* After */
background-color: #212529;
```
**Reason**: `#1a1a2e` is blue-tinted (RGB 26,26,46). Should match sidebar & main panel neutral grey `#212529`.
### 2. `.track-position` color (line 119)
```css
/* Before */
color: #666;
/* After */
color: #888;
```
**Reason**: Slightly brighter to improve readability and match secondary text conventions.
### 3. `.track-artist` color (line 149)
```css
/* Before */
color: #888;
/* After */
color: #b3b3b3;
```
**Reason**: Match artist/secondary text color used in `cover-grid.ts`.
### 4. `.remove-button` color (line 158)
```css
/* Before */
color: #666;
/* After */
color: #888;
```
**Reason**: Slightly brighter for consistency with other muted interactive elements.
### 5. `.empty-state` color (line 181)
```css
/* Before */
color: #666;
/* After */
color: #b3b3b3;
```
**Reason**: Match empty-state color in `cover-grid.ts`.
## No changes needed
These properties already match the rest of the app:
- Border colors (`#333`) - used consistently
- Resize handle hover (`#6c757d`) - matches sidebar
- Accent color (`#ffd43b`) - consistent across all components
- Hover background (`rgba(255,255,255,0.05)`) - matches track-list
- Active background (`rgba(255,212,59,0.1)`) - matches track-list
- Danger hover (`#ff6b6b`) - standard for destructive actions
## Verification
After making changes, run:
```bash
cd frontend && pnpm exec tsc --noEmit
cd frontend && pnpm build
```
-169
View File
@@ -1,169 +0,0 @@
# Plan: Queue Click-to-Play
## Goal
When a track in the queue panel is clicked, that track should start playing.
## Architecture Overview
The app uses a unidirectional event system: Frontend emits request events -> Backend processes them -> Backend emits state-changed events -> Frontend stores update -> Lit components re-render. The queue backend (`backend/queue/queue.go`) drives playback via `playCurrentTrack()` which calls `player.LoadFile()` then `player.Play()`.
## Changes Required (6 files)
### 1. `backend/events/events.go` — Add new event constant
Add `RequestPlayQueueIndex = "RequestPlayQueueIndex"` to the queue events const block.
```go
RequestAddTracksToQueue = "RequestAddTracksToQueue"
RequestPlayTracksNext = "RequestPlayTracksNext"
RequestPlayQueueIndex = "RequestPlayQueueIndex"
```
### 2. `frontend/src/events.ts` — Add matching TypeScript event constant
Add `RequestPlayQueueIndex: "RequestPlayQueueIndex"` to the Events object.
```typescript
RequestAddTracksToQueue: "RequestAddTracksToQueue",
RequestPlayTracksNext: "RequestPlayTracksNext",
RequestPlayQueueIndex: "RequestPlayQueueIndex",
```
### 3. `backend/queue/queue.go` — Add PlayIndex method + event handler
**a) Add event handler registration** in `registerEventHandlers()`, after the `RequestPlayTracksNext` handler (around line 184):
```go
runtime.EventsOn(q.ctx, events.RequestPlayQueueIndex, func(data ...any) {
q.logger.Info("Received RequestPlayQueueIndex")
q.handlePlayQueueIndex(data...)
})
```
**b) Add handler function** (after `handlePlayTracksNext`, around line 329):
```go
// handlePlayQueueIndex processes the RequestPlayQueueIndex event payload.
// Expects data[0] = float64 index.
func (q *Queue) handlePlayQueueIndex(data ...any) {
if len(data) < 1 {
q.logger.Error("RequestPlayQueueIndex: missing data")
return
}
index, ok := data[0].(float64)
if !ok {
q.logger.Error("RequestPlayQueueIndex: invalid index type", "got", data[0])
return
}
q.PlayIndex(int(index))
}
```
**c) Add `PlayIndex` method** (after `Previous()`, around line 679):
```go
// PlayIndex jumps to and plays the track at the given index.
func (q *Queue) PlayIndex(index int) {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.tracks) == 0 {
return
}
if index < 0 || index >= len(q.tracks) {
q.logger.Warn("PlayIndex: index out of range", "index", index, "trackCount", len(q.tracks))
return
}
q.currentIndex = index
q.playCurrentTrack()
q.emitQueueChanged()
}
```
This is simple and consistent with how `SetQueue` works — it sets `currentIndex` directly and calls `playCurrentTrack()`. When shuffle is on, the current track changes but the shuffle order stays intact. Subsequent Next/Previous calls will navigate relative to the new position in the shuffle order.
### 4. `frontend/src/store/queue-store.ts` — Add `playAtIndex` + fix QueueTrack type
**a) Fix QueueTrack interface** (add title and artist fields that the backend sends):
```typescript
export interface QueueTrack {
id: number;
audioFileId: number;
filePath: string;
position: number;
title: string;
artist: string;
}
```
**b) Add `playAtIndex` action** (after `cycleRepeat()`, around line 108):
```typescript
playAtIndex(index: number): void {
EventsEmit(Events.RequestPlayQueueIndex, index);
}
```
### 5. `frontend/src/store/controllers/queue-controller.ts` — Expose `playAtIndex`
Add after `cycleRepeat()` (around line 112):
```typescript
playAtIndex(index: number): void {
queueStore.playAtIndex(index);
}
```
### 6. `frontend/src/components/queue-panel/queue-panel.ts` — Add click handler
**a) Add click handler method** (after `handleRemoveTrack`, around line 170):
```typescript
private handleTrackClick(index: number) {
this.queue.playAtIndex(index);
}
```
**b) Update the `<li>` element** to add a click handler and change cursor style. Update the `track-item` CSS from `cursor: default` to `cursor: pointer`:
```css
.track-item {
display: flex;
align-items: center;
padding: 8px 16px;
gap: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
}
```
**c) Add `@click` handler to the `<li>`** and **stop propagation on the remove button** so clicking remove doesn't also trigger playback:
```html
<li class="track-item ${index === currentIndex ? 'active' : ''}"
@click=${() => this.handleTrackClick(index)}>
<span class="track-position">${index + 1}</span>
<div class="track-details">
<span class="track-title">${this.getDisplayTitle(track)}</span>
<span class="track-artist">${track.artist || 'Unknown Artist'}</span>
</div>
<button
class="remove-button"
@click=${(e: Event) => { e.stopPropagation(); this.handleRemoveTrack(index); }}
title="Remove from queue"
>
<wa-icon name="xmark"></wa-icon>
</button>
</li>
```
## Verification
After making changes:
1. `make lint` — Go linting passes
2. `make test` — Go tests pass
3. `cd frontend && pnpm exec tsc --noEmit` — TypeScript type checking passes
+209
View File
@@ -0,0 +1,209 @@
# Refactoring Catalog
Prioritized list of architectural improvements identified during a full codebase audit (Feb 2026). Items are grouped by priority — tackle P1 before adding major new features, P2 as convenient, P3 opportunistically.
---
## P1 — Should fix before adding major features
### 1. Resolve `RequestPlay` dual-handler ambiguity
**Problem:** Both `player.registerEventHandlers()` (`player.go`) and `queue.registerEventHandlers()` (`queue.go:182`) listen for the `RequestPlay` event. The player calls `Play()` (resume audio), while the queue calls `PlayFromStart()` (play from beginning if `currentIndex == -1`). Both fire on every `RequestPlay` event since Wails delivers to all listeners.
**Why it matters:** This works by coincidence — `PlayFromStart` is a no-op when `currentIndex != -1`, so the two handlers don't conflict in the common case. But it's fragile and semantically confusing. A single event triggering two different actions in two packages is an anti-pattern that will cause bugs as the codebase grows.
**Approach:** Remove the `RequestPlay` handler from the player. The queue should be the sole handler — it already calls `player.Play()` internally when needed. If the queue needs to distinguish "resume" from "play from start", add a separate event or an argument to the existing one.
---
### 2. Remove player from Wails `FEBindings` (or remove event handlers)
**Problem:** The player is added to `FEBindings` in `app.go:163`, which generates JS bindings for all exported methods (`Play()`, `Pause()`, `LoadFile()`, `Seek()`, `SetVolume()`, etc.). However, the frontend exclusively uses events for player control. This creates two parallel APIs for the same operations.
**Why it matters:** It exposes internal lifecycle methods (`SetContext()`, `SaveState()`, `RestoreState()`) to the frontend as callable JS functions. New developers won't know which API to use. Any method added to the player automatically becomes a frontend-callable binding.
**Approach:** Remove the player from `FEBindings`. The frontend uses events exclusively and the player doesn't need direct bindings. If `GetCurrentTrackInfo()` is needed as a binding for some edge case, extract it to a separate small struct that only exposes that method.
---
### 3. Split `queue.go` (2254 lines)
**Problem:** The queue package is a single 2254-line file containing types, state management, ~300 lines of event handler boilerplate, persistence logic, shuffle algorithms, and emit helpers.
**Why it matters:** Hard to navigate, hard to review changes, easy to introduce bugs in unrelated sections.
**Approach:** Split into focused files:
- `queue.go` — Core types (`Track`, `State`, `Queue` struct), constructor, `SetContext`, `SetPlayer`
- `handlers.go``registerEventHandlers()` and all `handle*` methods
- `persistence.go``persistTracks`, `persistState`, `RestoreState`, `SaveState`, `lookupTrackMetaBatch`
- `shuffle.go` — Shuffle order generation and navigation
- `emit.go` — All `emit*` methods
---
### 4. Split `cover-grid.ts` (3740 lines)
**Problem:** The largest frontend component by far. It likely handles album grid rendering, context menus, drag-and-drop, selection, sorting, resizing, and more — all in a single file.
**Why it matters:** Difficult to understand, modify, or review. Changes to context menu logic risk breaking grid rendering and vice versa.
**Approach:** Extract logical sections into separate files/components:
- Context menu logic into a shared utility or sub-component
- Selection logic already uses a `SelectionController` — verify it's fully extracted
- Drag-and-drop setup into the existing `DragController` if not already
- Grid rendering as the core component, delegating to these helpers
---
## P2 — Fix when convenient
### 5. Delete `backend/models/` package (dead code)
**Problem:** The `models` package (`files.go`, `music.go`, `art.go`) defines `AudioFile`, `AudioFileType`, `Album`, `Track`, `Artist`, and `Art` types. No package imports it anywhere.
**Why it matters:** Dead code creates confusion — new contributors may think these are the canonical domain types, but the actual types are in `library/`, `queue/`, `playlist/`, and `sqlcgen/`.
**Approach:** Delete the entire `backend/models/` directory.
---
### 6. Extract `SizedFilename` to a shared utility package
**Problem:** `library.SizedFilename()` is a small string utility for generating thumbnail filenames. Both `player/player.go` and `playlist/playlist.go` import the entire `library` package solely for this function.
**Why it matters:** Creates unnecessary coupling — `player` -> `library` and `playlist` -> `library` dependencies exist only for one utility function.
**Approach:** Move `SizedFilename` to a shared package (e.g., `backend/coverart/` or `backend/fileutil/`). Update the three callers: `library/`, `player/`, and `playlist/`.
---
### 7. Consolidate `LibraryScanComplete` handling
**Problem:** `LibraryScanComplete` is listened to directly in 10+ components (`genres-view.ts`, `artists-view.ts`, `cover-grid.ts`, `track-list.ts`, `playlist-view.ts`, `genre-details.ts`, `artist-details.ts`, `playlist-picker.ts`, `config-page.ts`, `library-manager.ts`) in addition to `library-store.ts` and `playlist-store.ts`. Each component independently re-fetches its data.
**Why it matters:** The stores already invalidate their caches and notify subscribers on this event. Components that use the store controllers should get re-rendered automatically. The direct listeners exist because many components load data independently from the stores (calling Go bindings directly), which means the stores aren't serving their full purpose as centralized data sources.
**Approach:** For components that already use `LibraryController`/`PlaylistController`, the store subscription should handle cache invalidation. The controller's `hostConnected` subscribes and `requestUpdate` triggers a re-render, which calls the async data getter, which will re-fetch since the cache was invalidated. Remove the redundant direct `EventsOn(LibraryScanComplete)` from components that go through stores. For components like `playlist-picker.ts` that call Go bindings directly (bypassing stores), either route them through the store or accept the direct listener as intentional.
---
### 8. Type the WebAwesome popup interactions (eliminate 49x `as any`)
**Problem:** Every component with a context menu uses `(popup as any).anchor = ...` and `(popup as any).active = true`. This pattern appears 49 times across `track-list.ts`, `cover-grid.ts`, `queue-panel.ts`, `playlist-view.ts`, `genres-view.ts`, `artists-view.ts`.
**Why it matters:** Type safety is completely bypassed for a core interaction pattern. Typos in property names (`actve` instead of `active`) would silently fail.
**Approach:** Create a type declaration for the WebAwesome popup element (or find one in their package). Alternatively, write a small typed utility:
```typescript
function openPopup(popup: Element, anchor: Element | VirtualAnchor): void
function closePopup(popup: Element): void
```
Replace all 49 `as any` casts with calls to these utilities.
---
### 9. Replace `GetCurrentTrackInfo` `map[string]interface{}` with a struct
**Problem:** `player.GetCurrentTrackInfo()` returns `map[string]interface{}` with stringly-typed keys (`"fileName"`, `"filePath"`, `"state"`, `"title"`, etc.). The `emitTrackChanged()` method mutates this map by adding keys after the fact.
**Why it matters:** No compile-time safety — typos in key names are silent bugs. The Wails binding generator would produce typed TypeScript if given a struct.
**Approach:** Define a `TrackInfo` struct in the player package with all the fields. Return it from `GetCurrentTrackInfo`. Update `emitTrackChanged` to build the struct directly instead of mutating a map.
---
### 10. Move `FullRescan` orchestration from library to app
**Problem:** `library.Library` holds references to the queue (`queueClearer` interface) and playlist service (`playlistRestorer` interface), set via `SetQueue()` and `SetPlaylistRestorer()`. The `FullRescan` method in `rescan.go` orchestrates clearing the queue and restoring playlists — cross-cutting concerns that aren't really library responsibilities.
**Why it matters:** The library package shouldn't know about queue clearing or playlist restoration. This creates a dependency web (`app` -> `library` -> `queue`, `app` -> `library` -> `playlist`).
**Approach:** Move the `FullRescan` orchestration to the `app` level. The app already has references to all three packages. The library would only expose `Scan()` and a `ClearAndRescan()` that handles only library concerns (clear DB, walk files, extract metadata). The app's `FullRescan` handler would call `queue.Clear()`, `library.ClearAndRescan()`, then `playlist.RestoreAll()`.
---
### 11. Fix double `LibraryScanStarted` event during FullRescan
**Problem:** `rescan.go:22` emits `LibraryScanStarted`, then calls `Scan()` which emits `LibraryScanStarted` again at `library.go:191`. The frontend receives two `LibraryScanStarted` events for a single full rescan.
**Why it matters:** Frontend components may show duplicate "scanning" UI state transitions or start/reset loading indicators twice.
**Approach:** Remove the `LibraryScanStarted` emission from either `FullRescan` or `Scan`. Since `Scan` is also called independently, keep it in `Scan` and remove it from `FullRescan`.
---
### 12. Inconsistent communication patterns: queue (events) vs playlist (bindings)
**Problem:** Queue operations use 14+ `Request*` events with manual `data[0].(type)` casting in ~300 lines of handler boilerplate. Playlist operations use direct Wails bindings with type-safe Go function signatures.
**Why it matters:** Inconsistency makes the codebase harder to learn. The queue's event-only approach requires substantial boilerplate that the playlist avoids. New features on the queue require touching 4 files (Go event constant, TS event constant, Go handler, TS store method) vs 1-2 files for the playlist.
**Approach:** This is a larger refactor. Two options:
1. **Move queue to bindings** (recommended): Add the queue to `FEBindings`, expose typed methods, call them directly from the frontend store. Remove the event handlers and the `Request*` events. Keep the backend-to-frontend events (`QueueChanged`, etc.) for state push.
2. **Accept the inconsistency**: Document the rationale (queue existed before playlists, events were the original pattern, bindings were adopted later). Add a comment in AGENTS.md.
---
## P3 — Fix opportunistically
### 13. Dead player methods: `ChangeVolume`, `MuteToggle`, `CurrentPosition`
**Problem:** `ChangeVolume()` (`player.go`), `MuteToggle()` (`player.go`), and `CurrentPosition()` (percentage-based, `player.go`) have zero callers anywhere in the codebase.
**Approach:** Delete them, or keep them if you plan to add keyboard shortcuts / media key support soon.
---
### 14. Unused queue sentinels: `ErrEmptyQueue`, `ErrNoPlayer`
**Problem:** Defined in `queue.go` but never returned or checked.
**Approach:** Delete them, or wire them into the appropriate error paths if they were intended for future validation.
---
### 15. `SeekFailed` event emitted but never listened to
**Problem:** `player.go` emits `SeekFailed` when seeking fails, but no frontend code subscribes to it. Users get no feedback on seek failure.
**Approach:** Either add a frontend listener that shows a brief notification/toast, or remove the event emission if seek failure feedback isn't needed.
---
### 16. `path.Join` instead of `filepath.Join` in config
**Problem:** `config/config.go:41` uses `path.Join` (POSIX paths) instead of `filepath.Join` (OS-aware paths) for constructing the config file path.
**Approach:** Replace with `filepath.Join`. Single-line change.
---
### 17. Replace 200ms sleep with frontend-ready handshake
**Problem:** `app.go:205-216` uses `time.Sleep(200 * time.Millisecond)` before emitting state to the frontend, assuming it will be ready by then.
**Approach:** Have the frontend emit a "ready" event when its stores have initialized. The backend listens for this event and then emits the current state. Eliminates the timing assumption.
---
### 18. Custom `sortInts` in queue instead of `slices.Sort`
**Problem:** `queue.go` has a hand-written insertion sort for int slices, but `slices.Sort()` is already used elsewhere in the same file.
**Approach:** Replace the custom `sortInts` with `slices.Sort`. Single-line change.
---
### 19. `playlist-picker.ts` bypasses `playlistStore`
**Problem:** `playlist-picker.ts` calls `GetAllPlaylists()` directly from the Go binding instead of going through `playlistStore`. It fetches only summaries (not `WithTracks`), which is why it doesn't use the store.
**Approach:** Either add a `getSummaries()` method to the playlist store that caches just the summary list, or accept this as intentional since the picker only needs summaries and the full `WithTracks` fetch would be wasteful for this use case.
---
### 20. `library-manager.ts` and `config-page.ts` overlap
**Problem:** Both components exist (different nav routes: "libraries" vs "settings"). `config-page.ts` has a comment saying scan metrics were "carried over from library-manager". They may have diverging copies of similar logic.
**Approach:** Audit both components for duplicated logic. If the library manager's functionality is fully subsumed by the config page, consider removing it and redirecting the "libraries" nav route.
-15
View File
@@ -144,21 +144,6 @@ func (p *Player) registerEventHandlers() {
return
}
runtime.EventsOn(
ctx,
events.RequestPlay,
func(_ ...any) {
p.logger.Info("Received RequestPlayEvent")
if err := p.Play(); err != nil {
p.logger.Warn(
"Play requested but not ready",
"err", err,
)
}
},
)
runtime.EventsOn(
ctx,
events.RequestPause,
+44 -1
View File
@@ -181,7 +181,7 @@ func (q *Queue) registerEventHandlers() {
runtime.EventsOn(q.ctx, events.RequestPlay, func(_ ...any) {
q.logger.Info("Received RequestPlay")
q.PlayFromStart()
q.Play()
})
runtime.EventsOn(q.ctx, events.RequestNext, func(_ ...any) {
@@ -1481,6 +1481,43 @@ func (q *Queue) Previous() {
q.emitIndexChanged()
}
// Play handles a play request by either resuming the current track or
// starting playback from the beginning of the queue. When a track is
// already active (currentIndex != -1) the player is told to resume;
// otherwise playback starts from the first track (or a random one when
// shuffle is enabled).
func (q *Queue) Play() {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.tracks) == 0 {
return
}
// A track is already active — ask the player to resume.
if q.currentIndex != -1 {
if q.player == nil {
q.logger.Error(
"No player set, cannot resume",
)
return
}
if err := q.player.Play(); err != nil {
q.logger.Warn(
"Resume requested but player not ready",
"err", err,
)
}
return
}
// No active track — start from the beginning.
q.playFromStart()
}
// PlayFromStart restarts playback from the beginning of the queue.
// If shuffle is enabled, a new shuffle order is generated and playback
// starts from a random track. This is a no-op when a track is already
@@ -1489,6 +1526,12 @@ func (q *Queue) PlayFromStart() {
q.mu.Lock()
defer q.mu.Unlock()
q.playFromStart()
}
// playFromStart is the lock-free inner implementation of PlayFromStart.
// The caller must hold q.mu.
func (q *Queue) playFromStart() {
if q.currentIndex != -1 {
return
}
@@ -122,6 +122,16 @@ export class TrackList extends LitElement implements SelectionHost {
typeof setTimeout
> | null = null;
// -- Memoisation caches for filtered / sorted tracks --
private cachedFilteredTracks: library.Track[] = [];
private cachedSortedTracks: library.Track[] = [];
private prevFilterTracks: library.Track[] = [];
private prevFilterTerm = '';
private prevFilterColIds = '';
private prevSortFiltered: library.Track[] = [];
private prevSortField: string | null = null;
private prevSortDir: SortDirection = 'asc';
private closeHandler = () => this.closeContextMenu();
private mousedownCloseHandler = (
@@ -181,11 +191,49 @@ export class TrackList extends LitElement implements SelectionHost {
private hasRestoredScroll = false;
// =================================================================
// Filtered tracks (search)
// Filtered / sorted tracks (memoised)
// =================================================================
private get filteredTracks(): library.Track[] {
const term = this.searchCtrl.term.toLowerCase();
/**
* Recompute the filtered and sorted track caches when
* their inputs have changed. Called from willUpdate()
* so the caches are ready before render().
*/
private recomputeTrackCaches() {
const term = this.searchCtrl.term;
const colIds =
this.trackListCtrl.columnIds.join(',');
if (
this.tracks !== this.prevFilterTracks ||
term !== this.prevFilterTerm ||
colIds !== this.prevFilterColIds
) {
this.prevFilterTracks = this.tracks;
this.prevFilterTerm = term;
this.prevFilterColIds = colIds;
this.cachedFilteredTracks =
this.computeFilteredTracks();
}
if (
this.cachedFilteredTracks !==
this.prevSortFiltered ||
this.sortField !== this.prevSortField ||
this.sortDirection !== this.prevSortDir
) {
this.prevSortFiltered =
this.cachedFilteredTracks;
this.prevSortField = this.sortField;
this.prevSortDir = this.sortDirection;
this.cachedSortedTracks =
this.computeSortedTracks();
}
}
private computeFilteredTracks(): library.Track[] {
const term =
this.searchCtrl.term.toLowerCase();
if (!term) return this.tracks;
@@ -201,12 +249,8 @@ export class TrackList extends LitElement implements SelectionHost {
);
}
// =================================================================
// Sorted tracks
// =================================================================
private get sortedTracks(): library.Track[] {
const tracks = this.filteredTracks;
private computeSortedTracks(): library.Track[] {
const tracks = this.cachedFilteredTracks;
if (!this.sortField) return tracks;
@@ -227,11 +271,11 @@ export class TrackList extends LitElement implements SelectionHost {
// =================================================================
getItemKey(index: number): string | undefined {
return this.sortedTracks[index]?.FilePath;
return this.cachedSortedTracks[index]?.FilePath;
}
getItemCount(): number {
return this.sortedTracks.length;
return this.cachedSortedTracks.length;
}
onSelectionChanged(): void {
@@ -1003,6 +1047,8 @@ export class TrackList extends LitElement implements SelectionHost {
this.tracks = this.externalTracks;
this.selection.clear();
}
this.recomputeTrackCaches();
}
override firstUpdated() {
@@ -1664,7 +1710,7 @@ export class TrackList extends LitElement implements SelectionHost {
}
override render() {
const visibleTracks = this.sortedTracks;
const visibleTracks = this.cachedSortedTracks;
const cols = this.activeColumns;
return html`