fixed player behavior after final queue track completes, added click to play from queue window
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
# 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
|
||||
@@ -56,6 +56,65 @@ golangci-lint run --build-tags webkit2_41 ./... # With build tags expl
|
||||
|
||||
Frontend type checking: `cd frontend && pnpm exec tsc --noEmit`
|
||||
|
||||
### Avoiding Common Linting Errors
|
||||
|
||||
Always run `make lint` before considering a task complete. Below are the most common linting violations and how to avoid them.
|
||||
|
||||
**Line length (`golines`)**: Keep lines under 100 characters. Break long function calls, especially `slog` calls, across multiple lines:
|
||||
```go
|
||||
// Bad — over 100 characters:
|
||||
q.logger.Warn("Current index out of range", "index", q.currentIndex, "trackCount", len(q.tracks))
|
||||
|
||||
// Good — broken across lines:
|
||||
q.logger.Warn(
|
||||
"Current index out of range",
|
||||
"index", q.currentIndex, "trackCount", len(q.tracks),
|
||||
)
|
||||
```
|
||||
|
||||
**Stuttering type names (`revive`)**: Exported types must not repeat the package name. Consumers would write `queue.Track`, not `queue.QueueTrack`:
|
||||
```go
|
||||
// Bad — stutters as queue.QueueTrack:
|
||||
type QueueTrack struct { ... }
|
||||
|
||||
// Good:
|
||||
type Track struct { ... }
|
||||
```
|
||||
|
||||
**Cuddled declarations (`wsl`)**: `var` and `const` declarations must be separated from the preceding statement by a blank line:
|
||||
```go
|
||||
// Bad:
|
||||
wasEmpty := len(q.tracks) == 0
|
||||
var newTracks []Track
|
||||
|
||||
// Good:
|
||||
wasEmpty := len(q.tracks) == 0
|
||||
|
||||
var newTracks []Track
|
||||
```
|
||||
|
||||
**Blank line after early returns (`nlreturn`)**: An `if` block that ends with `return`, `continue`, or `break` must be followed by a blank line:
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doNextThing()
|
||||
```
|
||||
|
||||
**Error sentinels (`err113`)**: Never use `errors.New(...)` or `fmt.Errorf("...")` inline in return statements. Define package-level sentinel errors instead:
|
||||
```go
|
||||
var errNotFound = errors.New("not found")
|
||||
```
|
||||
|
||||
**Doc comments (`godot`)**: All doc comments on exported types and functions must end with a period:
|
||||
```go
|
||||
// Track represents a track in the queue with its metadata.
|
||||
type Track struct { ... }
|
||||
```
|
||||
|
||||
**Import order (`gci`)**: Three groups separated by blank lines — stdlib, third-party, internal (`yellowjacket/...`). Let the formatter handle this, but be aware of the expected grouping.
|
||||
|
||||
## Code Generation
|
||||
|
||||
`go:generate` directives live in `backend/app.go` (templ) and `backend/database/database.go` (sqlc). After modifying `.templ` files or SQL in `backend/database/sql/`, run `make generate`. **Never edit files in `backend/database/sql/sqlcgen/` or `*_templ.go` — they are generated.**
|
||||
|
||||
@@ -29,7 +29,8 @@ func (c *Config) handle(w http.ResponseWriter, r *http.Request) {
|
||||
if err := c.handleConfigPost(r); err != nil {
|
||||
c.logger.Error("problem handling config post request", "err", err.Error())
|
||||
|
||||
if renderErr := c.formSubmitError(err.Error()).Render(r.Context(), w); renderErr != nil {
|
||||
renderErr := c.formSubmitError(err.Error()).Render(r.Context(), w)
|
||||
if renderErr != nil {
|
||||
c.logger.Error("problem rendering error response", "err", renderErr.Error())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// sqlc v1.30.0
|
||||
// source: playlists.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// sqlc v1.30.0
|
||||
// source: queue.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
@@ -42,6 +42,7 @@ const (
|
||||
RequestCycleRepeat = "RequestCycleRepeat"
|
||||
RequestAddTracksToQueue = "RequestAddTracksToQueue"
|
||||
RequestPlayTracksNext = "RequestPlayTracksNext"
|
||||
RequestPlayQueueIndex = "RequestPlayQueueIndex"
|
||||
)
|
||||
|
||||
// Config events.
|
||||
|
||||
@@ -191,7 +191,10 @@ func (l *Library) generateMissingThumbnails() error {
|
||||
|
||||
imgData, err := os.ReadFile(filepath.Join(coverDir, name))
|
||||
if err != nil {
|
||||
l.logger.Warn("could not read cover art for thumbnail generation", "file", name, "err", err)
|
||||
l.logger.Warn(
|
||||
"could not read cover art for thumbnail generation",
|
||||
"file", name, "err", err,
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -292,9 +292,11 @@ func (p *Player) startPaused() {
|
||||
p.emitPlaybackFinished()
|
||||
p.logger.Info("Playback finished naturally")
|
||||
|
||||
// Notify queue for auto-advance.
|
||||
// Notify queue for auto-advance. This must be dispatched to a new
|
||||
// goroutine because beep.Callback runs with the speaker mutex held
|
||||
// and the handler will call LoadFile/Play which acquire that same lock.
|
||||
if p.playbackFinishedHandler != nil {
|
||||
p.playbackFinishedHandler()
|
||||
go p.playbackFinishedHandler()
|
||||
}
|
||||
})))
|
||||
|
||||
@@ -432,6 +434,43 @@ func (p *Player) Pause() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnloadTrack tears down the current track, releasing the file and streamer
|
||||
// chain. The player returns to the initial "no track loaded" state and emits
|
||||
// events so the frontend clears its current-track display.
|
||||
func (p *Player) UnloadTrack() {
|
||||
// Stop audio output.
|
||||
if p.control != nil {
|
||||
speaker.Lock()
|
||||
p.control.Paused = true
|
||||
speaker.Unlock()
|
||||
}
|
||||
|
||||
// Close the open audio file.
|
||||
if p.currentFile != nil {
|
||||
if err := p.currentFile.Close(); err != nil {
|
||||
p.logger.Warn("Failed to close audio file during unload", "err", err)
|
||||
}
|
||||
|
||||
p.currentFile = nil
|
||||
}
|
||||
|
||||
// Release streamer chain. Volume is intentionally kept so the user's
|
||||
// volume setting persists across tracks.
|
||||
p.baseStreamer = nil
|
||||
p.seeker = nil
|
||||
p.resampled = nil
|
||||
p.control = nil
|
||||
p.speakerStreamer = nil
|
||||
|
||||
p.state = Stopped
|
||||
|
||||
// Notify frontend that there is no longer a current track.
|
||||
p.emitPlaybackStateChanged(p.state)
|
||||
runtime.EventsEmit(p.ctx, events.TrackChanged, nil)
|
||||
|
||||
p.logger.Info("Track unloaded")
|
||||
}
|
||||
|
||||
// SetVolume sets the playback volume (0-100).
|
||||
func (p *Player) SetVolume(desiredVolume UserVolume) error {
|
||||
speaker.Lock()
|
||||
@@ -571,7 +610,10 @@ func (p *Player) GetCurrentTrackInfo() (map[string]interface{}, error) {
|
||||
coverArtThumbnail = "/covers/" + name + "_thumb.jpg"
|
||||
}
|
||||
} else {
|
||||
p.logger.Debug("Could not get track metadata from database", "path", filePath, "err", err)
|
||||
p.logger.Debug(
|
||||
"Could not get track metadata from database",
|
||||
"path", filePath, "err", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const (
|
||||
|
||||
// Internal volume range bounds.
|
||||
const (
|
||||
MinVol Volume = -4
|
||||
MinVol Volume = -6
|
||||
MaxVol Volume = 0
|
||||
)
|
||||
|
||||
|
||||
+101
-35
@@ -36,10 +36,11 @@ type TrackLoader interface {
|
||||
LoadFile(filePath string) error
|
||||
Play() error
|
||||
CurrentPositionSeconds() (int, error)
|
||||
UnloadTrack()
|
||||
}
|
||||
|
||||
// QueueTrack represents a track in the queue with its metadata.
|
||||
type QueueTrack struct {
|
||||
// Track represents a track in the queue with its metadata.
|
||||
type Track struct {
|
||||
ID int64 `json:"id"`
|
||||
AudioFileID int64 `json:"audioFileId"`
|
||||
FilePath string `json:"filePath"`
|
||||
@@ -48,13 +49,13 @@ type QueueTrack struct {
|
||||
Artist string `json:"artist"`
|
||||
}
|
||||
|
||||
// QueueState is the full state emitted to the frontend.
|
||||
type QueueState struct {
|
||||
Tracks []QueueTrack `json:"tracks"`
|
||||
CurrentIndex int `json:"currentIndex"`
|
||||
ShuffleMode bool `json:"shuffleMode"`
|
||||
RepeatMode RepeatMode `json:"repeatMode"`
|
||||
SourcePlaylistID int64 `json:"sourcePlaylistId"`
|
||||
// State is the full state emitted to the frontend.
|
||||
type State struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
CurrentIndex int `json:"currentIndex"`
|
||||
ShuffleMode bool `json:"shuffleMode"`
|
||||
RepeatMode RepeatMode `json:"repeatMode"`
|
||||
SourcePlaylistID int64 `json:"sourcePlaylistId"`
|
||||
}
|
||||
|
||||
// Queue manages an ordered list of tracks for playback.
|
||||
@@ -65,7 +66,7 @@ type Queue struct {
|
||||
player TrackLoader
|
||||
|
||||
mu sync.Mutex
|
||||
tracks []QueueTrack
|
||||
tracks []Track
|
||||
currentIndex int
|
||||
shuffleMode bool
|
||||
repeatMode RepeatMode
|
||||
@@ -106,6 +107,7 @@ func (q *Queue) OnPlaybackFinished() {
|
||||
// Repeat One: replay the current track.
|
||||
if q.repeatMode == RepeatOne {
|
||||
q.playCurrentTrack()
|
||||
q.emitQueueChanged()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -120,6 +122,7 @@ func (q *Queue) OnPlaybackFinished() {
|
||||
|
||||
q.currentIndex = nextIdx
|
||||
q.playCurrentTrack()
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
// registerEventHandlers sets up Wails event listeners for queue commands.
|
||||
@@ -179,6 +182,11 @@ func (q *Queue) registerEventHandlers() {
|
||||
q.logger.Info("Received RequestPlayTracksNext")
|
||||
q.handlePlayTracksNext(data...)
|
||||
})
|
||||
|
||||
runtime.EventsOn(q.ctx, events.RequestPlayQueueIndex, func(data ...any) {
|
||||
q.logger.Info("Received RequestPlayQueueIndex")
|
||||
q.handlePlayQueueIndex(data...)
|
||||
})
|
||||
}
|
||||
|
||||
// handleSetQueue processes the RequestSetQueue event payload.
|
||||
@@ -298,6 +306,25 @@ func (q *Queue) handleAddTracksToQueue(data ...any) {
|
||||
q.AddTracks(filePaths)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// handlePlayTracksNext processes the RequestPlayTracksNext event payload.
|
||||
// Expects data[0] = []interface{} of file path strings.
|
||||
func (q *Queue) handlePlayTracksNext(data ...any) {
|
||||
@@ -331,7 +358,7 @@ func (q *Queue) SetQueue(filePaths []string, startIndex int) {
|
||||
defer q.mu.Unlock()
|
||||
|
||||
// Look up audio file IDs and metadata for all paths.
|
||||
tracks := make([]QueueTrack, 0, len(filePaths))
|
||||
tracks := make([]Track, 0, len(filePaths))
|
||||
|
||||
for i, fp := range filePaths {
|
||||
af, err := q.db.Queries.GetAudioFileByPath(q.db.Ctx, fp)
|
||||
@@ -341,7 +368,7 @@ func (q *Queue) SetQueue(filePaths []string, startIndex int) {
|
||||
continue
|
||||
}
|
||||
|
||||
track := QueueTrack{
|
||||
track := Track{
|
||||
AudioFileID: af.ID,
|
||||
FilePath: fp,
|
||||
Position: int64(i),
|
||||
@@ -395,7 +422,7 @@ func (q *Queue) AddTrack(filePath string) {
|
||||
|
||||
wasEmpty := len(q.tracks) == 0
|
||||
|
||||
track := QueueTrack{
|
||||
track := Track{
|
||||
AudioFileID: af.ID,
|
||||
FilePath: filePath,
|
||||
Position: int64(len(q.tracks)),
|
||||
@@ -449,7 +476,7 @@ func (q *Queue) AddTracks(filePaths []string) {
|
||||
continue
|
||||
}
|
||||
|
||||
track := QueueTrack{
|
||||
track := Track{
|
||||
AudioFileID: af.ID,
|
||||
FilePath: fp,
|
||||
Position: int64(len(q.tracks)),
|
||||
@@ -490,7 +517,8 @@ func (q *Queue) InsertNextTracks(filePaths []string) {
|
||||
}
|
||||
|
||||
wasEmpty := len(q.tracks) == 0
|
||||
var newTracks []QueueTrack
|
||||
|
||||
var newTracks []Track
|
||||
|
||||
for _, fp := range filePaths {
|
||||
af, err := q.db.Queries.GetAudioFileByPath(q.db.Ctx, fp)
|
||||
@@ -500,7 +528,7 @@ func (q *Queue) InsertNextTracks(filePaths []string) {
|
||||
continue
|
||||
}
|
||||
|
||||
track := QueueTrack{
|
||||
track := Track{
|
||||
AudioFileID: af.ID,
|
||||
FilePath: fp,
|
||||
}
|
||||
@@ -519,7 +547,7 @@ func (q *Queue) InsertNextTracks(filePaths []string) {
|
||||
}
|
||||
|
||||
// Insert the block into the slice at insertPos.
|
||||
tail := make([]QueueTrack, len(q.tracks[insertPos:]))
|
||||
tail := make([]Track, len(q.tracks[insertPos:]))
|
||||
copy(tail, q.tracks[insertPos:])
|
||||
q.tracks = append(q.tracks[:insertPos], newTracks...)
|
||||
q.tracks = append(q.tracks, tail...)
|
||||
@@ -558,7 +586,7 @@ func (q *Queue) InsertNext(filePath string) {
|
||||
insertPos = len(q.tracks)
|
||||
}
|
||||
|
||||
track := QueueTrack{
|
||||
track := Track{
|
||||
AudioFileID: af.ID,
|
||||
FilePath: filePath,
|
||||
Position: int64(insertPos),
|
||||
@@ -572,7 +600,7 @@ func (q *Queue) InsertNext(filePath string) {
|
||||
}
|
||||
|
||||
// Insert into slice.
|
||||
q.tracks = append(q.tracks, QueueTrack{})
|
||||
q.tracks = append(q.tracks, Track{})
|
||||
copy(q.tracks[insertPos+1:], q.tracks[insertPos:])
|
||||
q.tracks[insertPos] = track
|
||||
|
||||
@@ -601,8 +629,9 @@ func (q *Queue) RemoveTrack(position int) {
|
||||
|
||||
q.tracks = append(q.tracks[:position], q.tracks[position+1:]...)
|
||||
|
||||
// Adjust current index if needed.
|
||||
if position < q.currentIndex {
|
||||
// Adjust current index if needed. A currentIndex of -1 means no track
|
||||
// is loaded, so only shift when a valid track is selected.
|
||||
if q.currentIndex >= 0 && position < q.currentIndex {
|
||||
q.currentIndex--
|
||||
} else if position == q.currentIndex && q.currentIndex >= len(q.tracks) && len(q.tracks) > 0 {
|
||||
q.currentIndex = len(q.tracks) - 1
|
||||
@@ -645,7 +674,7 @@ func (q *Queue) Previous() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if len(q.tracks) == 0 {
|
||||
if len(q.tracks) == 0 || q.currentIndex < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -674,6 +703,26 @@ func (q *Queue) Previous() {
|
||||
q.emitQueueChanged()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// ToggleShuffle toggles shuffle mode on/off.
|
||||
func (q *Queue) ToggleShuffle() {
|
||||
q.mu.Lock()
|
||||
@@ -710,14 +759,14 @@ func (q *Queue) CycleRepeat() {
|
||||
}
|
||||
|
||||
// GetState returns the current queue state for the frontend.
|
||||
func (q *Queue) GetState() QueueState {
|
||||
func (q *Queue) GetState() State {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
tracks := make([]QueueTrack, len(q.tracks))
|
||||
tracks := make([]Track, len(q.tracks))
|
||||
copy(tracks, q.tracks)
|
||||
|
||||
return QueueState{
|
||||
return State{
|
||||
Tracks: tracks,
|
||||
CurrentIndex: q.currentIndex,
|
||||
ShuffleMode: q.shuffleMode,
|
||||
@@ -790,10 +839,10 @@ func (q *Queue) RestoreState() {
|
||||
return
|
||||
}
|
||||
|
||||
q.tracks = make([]QueueTrack, 0, len(rows))
|
||||
q.tracks = make([]Track, 0, len(rows))
|
||||
|
||||
for _, row := range rows {
|
||||
q.tracks = append(q.tracks, QueueTrack{
|
||||
q.tracks = append(q.tracks, Track{
|
||||
ID: row.ID,
|
||||
AudioFileID: row.AudioFileID,
|
||||
FilePath: row.FilePath,
|
||||
@@ -803,7 +852,9 @@ func (q *Queue) RestoreState() {
|
||||
})
|
||||
}
|
||||
|
||||
// Clamp current index.
|
||||
// Clamp current index. A value of -1 is valid and means "no current
|
||||
// track" (e.g. the queue was exhausted before shutdown). Only clamp
|
||||
// when the index exceeds the restored track count.
|
||||
if q.currentIndex >= len(q.tracks) && len(q.tracks) > 0 {
|
||||
q.currentIndex = len(q.tracks) - 1
|
||||
}
|
||||
@@ -954,13 +1005,19 @@ func (q *Queue) playCurrentTrack() {
|
||||
}
|
||||
|
||||
if q.currentIndex < 0 || q.currentIndex >= len(q.tracks) {
|
||||
q.logger.Warn("Current index out of range", "index", q.currentIndex, "trackCount", len(q.tracks))
|
||||
q.logger.Warn(
|
||||
"Current index out of range",
|
||||
"index", q.currentIndex, "trackCount", len(q.tracks),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
track := q.tracks[q.currentIndex]
|
||||
q.logger.Info("Playing track from queue", "filePath", track.FilePath, "position", q.currentIndex)
|
||||
q.logger.Info(
|
||||
"Playing track from queue",
|
||||
"filePath", track.FilePath, "position", q.currentIndex,
|
||||
)
|
||||
|
||||
err := q.player.LoadFile(track.FilePath)
|
||||
if err != nil {
|
||||
@@ -978,10 +1035,19 @@ func (q *Queue) playCurrentTrack() {
|
||||
}
|
||||
|
||||
// onQueueExhausted is called when there are no more tracks to play.
|
||||
// This is the extension point for a future fallback playlist feature.
|
||||
// It unloads the current track, resets the index to -1 (no current track),
|
||||
// and notifies the frontend.
|
||||
func (q *Queue) onQueueExhausted() {
|
||||
q.logger.Info("Queue exhausted, stopping playback")
|
||||
// Future: load fallback playlist here.
|
||||
q.logger.Info("Queue exhausted, unloading track")
|
||||
|
||||
q.currentIndex = -1
|
||||
|
||||
if q.player != nil {
|
||||
q.player.UnloadTrack()
|
||||
}
|
||||
|
||||
q.emitQueueChanged()
|
||||
q.persistState()
|
||||
}
|
||||
|
||||
// reindexPositions updates the Position field of all tracks to match slice index.
|
||||
@@ -1047,7 +1113,7 @@ func (q *Queue) emitQueueChanged() {
|
||||
return
|
||||
}
|
||||
|
||||
state := QueueState{
|
||||
state := State{
|
||||
Tracks: q.tracks,
|
||||
CurrentIndex: q.currentIndex,
|
||||
ShuffleMode: q.shuffleMode,
|
||||
@@ -1057,7 +1123,7 @@ func (q *Queue) emitQueueChanged() {
|
||||
|
||||
// Ensure tracks is never nil in JSON.
|
||||
if state.Tracks == nil {
|
||||
state.Tracks = []QueueTrack{}
|
||||
state.Tracks = []Track{}
|
||||
}
|
||||
|
||||
runtime.EventsEmit(q.ctx, events.QueueChanged, state)
|
||||
|
||||
@@ -1 +1 @@
|
||||
02c7eb24a50fc8301be7488868be5860
|
||||
74e25cdcdccb20fc50b40dc29ec5f6f9
|
||||
@@ -75,7 +75,7 @@ export class QueuePanel extends LitElement {
|
||||
padding: 8px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
@@ -165,10 +165,15 @@ export class QueuePanel extends LitElement {
|
||||
this.dispatchEvent(new CustomEvent('queue-panel-close', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
private handleRemoveTrack(position: number) {
|
||||
private handleRemoveTrack(e: Event, position: number) {
|
||||
e.stopPropagation();
|
||||
this.queue.removeFromQueue(position);
|
||||
}
|
||||
|
||||
private handleTrackClick(index: number) {
|
||||
this.queue.playAtIndex(index);
|
||||
}
|
||||
|
||||
private getDisplayTitle(track: { title: string; filePath: string }): string {
|
||||
if (track.title) return track.title;
|
||||
|
||||
@@ -203,7 +208,8 @@ export class QueuePanel extends LitElement {
|
||||
<ul class="track-list">
|
||||
${tracks.map(
|
||||
(track, index) => html`
|
||||
<li class="track-item ${index === currentIndex ? 'active' : ''}">
|
||||
<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>
|
||||
@@ -211,7 +217,7 @@ export class QueuePanel extends LitElement {
|
||||
</div>
|
||||
<button
|
||||
class="remove-button"
|
||||
@click=${() => this.handleRemoveTrack(index)}
|
||||
@click=${(e: Event) => this.handleRemoveTrack(e, index)}
|
||||
title="Remove from queue"
|
||||
>
|
||||
<wa-icon name="xmark"></wa-icon>
|
||||
|
||||
@@ -32,6 +32,7 @@ export const Events = {
|
||||
RequestCycleRepeat: "RequestCycleRepeat",
|
||||
RequestAddTracksToQueue: "RequestAddTracksToQueue",
|
||||
RequestPlayTracksNext: "RequestPlayTracksNext",
|
||||
RequestPlayQueueIndex: "RequestPlayQueueIndex",
|
||||
} as const;
|
||||
|
||||
export type EventName = (typeof Events)[keyof typeof Events];
|
||||
|
||||
@@ -110,4 +110,8 @@ export class QueueController implements ReactiveController {
|
||||
cycleRepeat(): void {
|
||||
queueStore.cycleRepeat();
|
||||
}
|
||||
|
||||
playAtIndex(index: number): void {
|
||||
queueStore.playAtIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ class PlayerStore {
|
||||
this.update({ isPlaying: data.state === 'playing' });
|
||||
});
|
||||
|
||||
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo) => {
|
||||
this.update({ currentTrack: trackInfo });
|
||||
EventsOn(Events.TrackChanged, (trackInfo: TrackInfo | null) => {
|
||||
this.update({ currentTrack: trackInfo ?? null });
|
||||
});
|
||||
|
||||
EventsOn(Events.PlaybackFinished, () => {
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface QueueTrack {
|
||||
audioFileId: number;
|
||||
filePath: string;
|
||||
position: number;
|
||||
title: string;
|
||||
artist: string;
|
||||
}
|
||||
|
||||
export type RepeatMode = 'off' | 'all' | 'one';
|
||||
@@ -24,7 +26,7 @@ type Subscriber = () => void;
|
||||
class QueueStore {
|
||||
private state: QueueState = {
|
||||
tracks: [],
|
||||
currentIndex: 0,
|
||||
currentIndex: -1,
|
||||
shuffleMode: false,
|
||||
repeatMode: 'off',
|
||||
sourcePlaylistId: 0,
|
||||
@@ -107,6 +109,10 @@ class QueueStore {
|
||||
EventsEmit(Events.RequestCycleRepeat);
|
||||
}
|
||||
|
||||
playAtIndex(index: number): void {
|
||||
EventsEmit(Events.RequestPlayQueueIndex, index);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// SUBSCRIPTION SYSTEM
|
||||
// ===================================================================
|
||||
|
||||
@@ -9,7 +9,8 @@ require (
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/golang-cz/devslog v0.0.15
|
||||
github.com/gorilla/schema v1.4.1
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
github.com/wailsapp/wails/v2 v2.10.2
|
||||
golang.org/x/image v0.12.0
|
||||
golang.org/x/sync v0.19.0
|
||||
modernc.org/sqlite v1.45.0
|
||||
)
|
||||
@@ -32,13 +33,14 @@ require (
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/creack/pty v1.1.24 // indirect
|
||||
github.com/cubicdaiya/gonp v1.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/oto/v3 v3.3.3 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/evilmartians/lefthook/v2 v2.1.1 // indirect
|
||||
github.com/evilmartians/lefthook v1.13.6 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -48,7 +50,6 @@ require (
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/cel-go v0.26.1 // indirect
|
||||
@@ -66,6 +67,7 @@ require (
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/kaptinlin/go-i18n v0.2.2 // indirect
|
||||
github.com/kaptinlin/jsonpointer v0.4.8 // indirect
|
||||
github.com/kaptinlin/jsonschema v0.6.5 // indirect
|
||||
github.com/kaptinlin/messageformat-go v0.4.7 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
|
||||
@@ -42,6 +42,8 @@ github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7m
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
@@ -62,18 +64,15 @@ github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0o
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/evilmartians/lefthook v1.13.6 h1:uzuFWpgmqCUg3FoLz0CBkiOHUS/vU3nhB92zReyR09U=
|
||||
github.com/evilmartians/lefthook v1.13.6/go.mod h1:rZdqvPtTVFe+3syrRiY10tG3L6O5+4dz9ZuAMQ5JYn0=
|
||||
github.com/evilmartians/lefthook/v2 v2.1.1/go.mod h1:vm4cjx1xvQNrAMFkRpmAqnKscxZXm1bcLmXRKUFBAy8=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10-rc1 h1:dlx6t2dnKnMZgsUQf8wr7GP7xtLjE5FxBS2EstWHPfY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10-rc1/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
|
||||
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
|
||||
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -87,10 +86,7 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@@ -140,14 +136,13 @@ github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3T
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
|
||||
github.com/kaptinlin/go-i18n v0.1.7 h1:CYt6NGHFrje1dMufhxKGooCmKFJKDfhWVznYSODPjo8=
|
||||
github.com/kaptinlin/go-i18n v0.1.7/go.mod h1:Lq3ZGBq/JKUuxbH4bL0aQYeBM3Fk6JRuo637EfvxO6U=
|
||||
github.com/kaptinlin/go-i18n v0.2.2 h1:kebVCZme/BrCTqonh/J+VYCl1+Of5C18bvyn3DRPl5M=
|
||||
github.com/kaptinlin/go-i18n v0.2.2/go.mod h1:MiwkeHryBopAhC/M3zEwIM/2IN8TvTqJQswPw6kceqM=
|
||||
github.com/kaptinlin/jsonschema v0.4.14 h1:56HclkbBr/ZQypxqRzzeFERNFMK7kroloqlZbXLhJNM=
|
||||
github.com/kaptinlin/jsonschema v0.4.14/go.mod h1:KVvnDL8OUOhNQ51/PPFjITD7qe8M6nsuBuO0076oVHQ=
|
||||
github.com/kaptinlin/jsonpointer v0.4.8 h1:HocHcXrOBfP/nUJw0YYjed/TlQvuCAY6uRs3Qok7F6g=
|
||||
github.com/kaptinlin/jsonpointer v0.4.8/go.mod h1:9y0LgXavlmVE5FSHShY5LRlURJJVhbyVJSRWkilrTqA=
|
||||
github.com/kaptinlin/jsonschema v0.6.5 h1:hC7upwWlvamWqeTVQ3ab20F4w0XKNKR1drY9apoqGOU=
|
||||
github.com/kaptinlin/jsonschema v0.6.5/go.mod h1:EbhSbdxZ4QjzIORdMWOrRXJeCHrLTJqXDA8JzNaeFc8=
|
||||
github.com/kaptinlin/messageformat-go v0.4.0 h1:L5wPgwQZkV1Rvs19htUT2RGx8N1GCq3uQG5nB6VHRcM=
|
||||
github.com/kaptinlin/messageformat-go v0.4.0/go.mod h1:LrLCV49C5ms/BZlOpFPihou+cPvhOQSvVJHj2wOe6w8=
|
||||
github.com/kaptinlin/messageformat-go v0.4.7 h1:HQ/OvFUSU7+fAHWkZnP2ug9y+A/ZyTE8j33jfWr8O3Q=
|
||||
github.com/kaptinlin/messageformat-go v0.4.7/go.mod h1:DusKpv8CIybczGvwIVn3j13hbR3psr5mOwhFudkiq1c=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
@@ -191,8 +186,7 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||
@@ -249,8 +243,7 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
@@ -281,8 +274,8 @@ github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6N
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
github.com/wailsapp/wails/v2 v2.10.2 h1:29U+c5PI4K4hbx8yFbFvwpCuvqK9VgNv8WGobIlKlXk=
|
||||
github.com/wailsapp/wails/v2 v2.10.2/go.mod h1:XuN4IUOPpzBrHUkEd7sCU5ln4T/p1wQedfxP7fKik+4=
|
||||
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo=
|
||||
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM=
|
||||
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
|
||||
@@ -321,30 +314,30 @@ go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
|
||||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -358,23 +351,19 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -383,8 +372,8 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
Reference in New Issue
Block a user