+2
-2
AGENTS.md
+2
-2
AGENTS.md
···
7
7
- check the justfiles. there's a root one, one for the backend, one for the frontend, and one for the transcoder etc
8
8
9
9
## ๐จ Critical Rules & Workflows
10
-
* **Read `STATUS.md` First:** Always check for active tasks and known issues. This file is NEVER tracked in git.
10
+
* **Read `STATUS.md` First:** Always check for active tasks and known issues.
11
11
* **Workflow:**
12
12
* Use **GitHub Issues** (not Linear).
13
13
* **PRs:** Always create for review; never push to main directly.
···
52
52
โ โโโ src/lib/ # Components & State (.svelte.ts)
53
53
โโโ scripts/ # Admin scripts (uv run scripts/...)
54
54
โโโ docs/ # Architecture & Guides
55
-
โโโ STATUS.md # Living status document (Untracked)
55
+
โโโ STATUS.md # Living status document
56
56
```
57
57
58
58
this file ("AGENTS.md") is symlinked to `CLAUDE.md` and `GEMINI.md` for maximal compatibility.
+33
-3
docs/frontend/keyboard-shortcuts.md
+33
-3
docs/frontend/keyboard-shortcuts.md
···
49
49
- `aria-label="toggle queue (Q)"` for screen readers
50
50
- tooltip shows keyboard hint
51
51
52
+
---
53
+
54
+
## playback shortcuts
55
+
56
+
all playback shortcuts require a track to be loaded and are disabled when the search modal is open.
57
+
58
+
### Space - play/pause
59
+
60
+
toggles playback of the current track.
61
+
62
+
### Left Arrow - seek backward
63
+
64
+
seeks backward 10 seconds in the current track.
65
+
66
+
### Right Arrow - seek forward
67
+
68
+
seeks forward 10 seconds in the current track.
69
+
70
+
### J - previous track
71
+
72
+
goes to the previous track in the queue. if more than 3 seconds into the current track, restarts it instead.
73
+
74
+
### L - next track
75
+
76
+
advances to the next track in the queue (if available).
77
+
78
+
### M - mute/unmute
79
+
80
+
toggles mute. restores previous volume level when unmuting.
81
+
82
+
---
83
+
52
84
## adding new shortcuts
53
85
54
86
when adding keyboard shortcuts:
···
95
127
## future candidates
96
128
97
129
potential shortcuts to consider:
98
-
- **space** - play/pause (when not focused on button)
99
-
- **arrow keys** - skip forward/back (context-aware)
100
-
- **shift + arrow** - navigate queue
101
130
- **/** - focus search (alternative to Cmd/Ctrl+K)
102
131
- **T** - cycle theme (dark/light/system)
132
+
- **S** - shuffle queue
103
133
104
134
## design principles
105
135
+93
-1
frontend/src/routes/+layout.svelte
+93
-1
frontend/src/routes/+layout.svelte
···
15
15
import { auth } from '$lib/auth.svelte';
16
16
import { preferences } from '$lib/preferences.svelte';
17
17
import { player } from '$lib/player.svelte';
18
+
import { queue } from '$lib/queue.svelte';
18
19
import { search } from '$lib/search.svelte';
19
20
import { browser } from '$app/environment';
20
21
import type { LayoutData } from './$types';
···
80
81
document.documentElement.style.setProperty('--queue-width', queueWidth);
81
82
});
82
83
84
+
const SEEK_AMOUNT = 10; // seconds
85
+
let previousVolume = 0.7; // for mute toggle
86
+
83
87
function handleKeyboardShortcuts(event: KeyboardEvent) {
84
88
// Cmd/Ctrl+K: toggle search
85
89
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
···
103
107
return;
104
108
}
105
109
110
+
// ignore playback shortcuts when search modal is open
111
+
if (search.isOpen) {
112
+
return;
113
+
}
114
+
115
+
const key = event.key.toLowerCase();
116
+
106
117
// toggle queue on 'q' key
107
-
if (event.key.toLowerCase() === 'q') {
118
+
if (key === 'q') {
108
119
event.preventDefault();
109
120
toggleQueue();
121
+
return;
122
+
}
123
+
124
+
// playback shortcuts - only when a track is loaded
125
+
if (!player.currentTrack) {
126
+
return;
127
+
}
128
+
129
+
switch (event.key) {
130
+
case ' ': // space - play/pause
131
+
event.preventDefault();
132
+
player.togglePlayPause();
133
+
break;
134
+
135
+
case 'ArrowLeft': // seek backward
136
+
event.preventDefault();
137
+
seekBy(-SEEK_AMOUNT);
138
+
break;
139
+
140
+
case 'ArrowRight': // seek forward
141
+
event.preventDefault();
142
+
seekBy(SEEK_AMOUNT);
143
+
break;
144
+
145
+
case 'j': // previous track (youtube-style)
146
+
case 'J':
147
+
event.preventDefault();
148
+
handlePreviousTrack();
149
+
break;
150
+
151
+
case 'l': // next track (youtube-style)
152
+
case 'L':
153
+
event.preventDefault();
154
+
if (queue.hasNext) {
155
+
queue.next();
156
+
}
157
+
break;
158
+
159
+
case 'm': // mute/unmute
160
+
case 'M':
161
+
event.preventDefault();
162
+
toggleMute();
163
+
break;
164
+
}
165
+
}
166
+
167
+
function seekBy(seconds: number) {
168
+
if (!player.audioElement || !player.duration) return;
169
+
170
+
const newTime = Math.max(0, Math.min(player.duration, player.currentTime + seconds));
171
+
player.currentTime = newTime;
172
+
player.audioElement.currentTime = newTime;
173
+
}
174
+
175
+
function handlePreviousTrack() {
176
+
const RESTART_THRESHOLD = 3; // restart if more than 3 seconds in
177
+
178
+
if (player.currentTime > RESTART_THRESHOLD) {
179
+
// restart current track
180
+
player.currentTime = 0;
181
+
if (player.audioElement) {
182
+
player.audioElement.currentTime = 0;
183
+
}
184
+
} else if (queue.hasPrevious) {
185
+
// go to previous track
186
+
queue.previous();
187
+
} else {
188
+
// restart from beginning
189
+
player.currentTime = 0;
190
+
if (player.audioElement) {
191
+
player.audioElement.currentTime = 0;
192
+
}
193
+
}
194
+
}
195
+
196
+
function toggleMute() {
197
+
if (player.volume > 0) {
198
+
previousVolume = player.volume;
199
+
player.volume = 0;
200
+
} else {
201
+
player.volume = previousVolume || 0.7;
110
202
}
111
203
}
112
204