+60
-17
frontend/src/lib/components/Player.svelte
+60
-17
frontend/src/lib/components/Player.svelte
···
6
let formattedCurrentTime = $derived(formatTime(player.currentTime));
7
let formattedDuration = $derived(formatTime(player.duration));
8
9
// volume state indicators
10
let volumeState = $derived.by(() => {
11
if (player.volume === 0) return 'muted';
···
93
<div class="player-title" class:scrolling={player.currentTrack.title.length > 30}>
94
<span>{player.currentTrack.title}</span>
95
</div>
96
-
<a href="/u/{player.currentTrack.artist_handle}" class="player-artist-link">
97
-
{player.currentTrack.artist}
98
-
</a>
99
</div>
100
101
<div class="player-controls">
···
124
min="0"
125
max={player.duration || 0}
126
bind:value={player.currentTime}
127
/>
128
<span class="time">{formattedDuration}</span>
129
</div>
···
218
}
219
}
220
221
-
.player-artist-link {
222
color: #909090;
223
font-size: 0.9rem;
224
text-decoration: none;
225
-
display: block;
226
transition: color 0.2s;
227
-
white-space: nowrap;
228
-
overflow: hidden;
229
-
text-overflow: ellipsis;
230
}
231
232
.player-artist-link:hover {
233
color: var(--accent);
234
}
235
236
.player-controls {
237
flex: 1;
238
display: flex;
···
324
cursor: pointer;
325
}
326
327
-
input[type="range"]::-webkit-slider-track {
328
-
background: #333;
329
height: 4px;
330
border-radius: 2px;
331
}
···
342
}
343
344
input[type="range"]::-webkit-slider-thumb:hover {
345
-
background: #8ab3ff;
346
transform: scale(1.2);
347
-
box-shadow: 0 0 0 4px rgba(106, 159, 255, 0.2);
348
}
349
350
input[type="range"].muted::-webkit-slider-thumb {
···
353
354
input[type="range"].max::-webkit-slider-thumb {
355
background: var(--accent);
356
-
box-shadow: 0 0 0 4px rgba(106, 159, 255, 0.3);
357
}
358
359
input[type="range"]::-moz-range-track {
360
-
background: #333;
361
height: 4px;
362
border-radius: 2px;
363
}
···
372
}
373
374
input[type="range"]::-moz-range-thumb:hover {
375
-
background: #8ab3ff;
376
transform: scale(1.2);
377
-
box-shadow: 0 0 0 4px rgba(106, 159, 255, 0.2);
378
}
379
380
input[type="range"].muted::-moz-range-thumb {
···
383
384
input[type="range"].max::-moz-range-thumb {
385
background: var(--accent);
386
-
box-shadow: 0 0 0 4px rgba(106, 159, 255, 0.3);
387
}
388
389
@media (max-width: 768px) {
···
6
let formattedCurrentTime = $derived(formatTime(player.currentTime));
7
let formattedDuration = $derived(formatTime(player.duration));
8
9
+
// compute progress percentage for seek bar styling
10
+
let progressPercent = $derived.by(() => {
11
+
if (!player.duration || player.duration === 0) return 0;
12
+
return (player.currentTime / player.duration) * 100;
13
+
});
14
+
15
// volume state indicators
16
let volumeState = $derived.by(() => {
17
if (player.volume === 0) return 'muted';
···
99
<div class="player-title" class:scrolling={player.currentTrack.title.length > 30}>
100
<span>{player.currentTrack.title}</span>
101
</div>
102
+
<div class="player-metadata">
103
+
<a href="/u/{player.currentTrack.artist_handle}" class="player-artist-link">
104
+
{player.currentTrack.artist}
105
+
</a>
106
+
{#if player.currentTrack.album}
107
+
<span class="metadata-separator">•</span>
108
+
<span class="player-album">{player.currentTrack.album}</span>
109
+
{/if}
110
+
</div>
111
</div>
112
113
<div class="player-controls">
···
136
min="0"
137
max={player.duration || 0}
138
bind:value={player.currentTime}
139
+
style="--progress: {progressPercent}%"
140
/>
141
<span class="time">{formattedDuration}</span>
142
</div>
···
231
}
232
}
233
234
+
.player-metadata {
235
+
display: flex;
236
+
align-items: center;
237
+
gap: 0.5rem;
238
color: #909090;
239
font-size: 0.9rem;
240
+
white-space: nowrap;
241
+
overflow: hidden;
242
+
}
243
+
244
+
.player-artist-link {
245
+
color: #909090;
246
text-decoration: none;
247
transition: color 0.2s;
248
+
flex-shrink: 0;
249
}
250
251
.player-artist-link:hover {
252
color: var(--accent);
253
}
254
255
+
.metadata-separator {
256
+
color: #606060;
257
+
flex-shrink: 0;
258
+
}
259
+
260
+
.player-album {
261
+
color: #808080;
262
+
overflow: hidden;
263
+
text-overflow: ellipsis;
264
+
white-space: nowrap;
265
+
}
266
+
267
.player-controls {
268
flex: 1;
269
display: flex;
···
355
cursor: pointer;
356
}
357
358
+
input[type="range"]::-webkit-slider-runnable-track {
359
+
background: linear-gradient(
360
+
to right,
361
+
color-mix(in srgb, var(--accent) 60%, transparent) 0%,
362
+
color-mix(in srgb, var(--accent) 60%, transparent) var(--progress, 0%),
363
+
color-mix(in srgb, var(--accent) 20%, transparent) var(--progress, 0%),
364
+
color-mix(in srgb, var(--accent) 20%, transparent) 100%
365
+
);
366
height: 4px;
367
border-radius: 2px;
368
}
···
379
}
380
381
input[type="range"]::-webkit-slider-thumb:hover {
382
+
background: var(--accent-hover);
383
transform: scale(1.2);
384
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 20%, transparent);
385
}
386
387
input[type="range"].muted::-webkit-slider-thumb {
···
390
391
input[type="range"].max::-webkit-slider-thumb {
392
background: var(--accent);
393
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 30%, transparent);
394
}
395
396
input[type="range"]::-moz-range-track {
397
+
background: color-mix(in srgb, var(--accent) 20%, transparent);
398
+
height: 4px;
399
+
border-radius: 2px;
400
+
}
401
+
402
+
input[type="range"]::-moz-range-progress {
403
+
background: color-mix(in srgb, var(--accent) 60%, transparent);
404
height: 4px;
405
border-radius: 2px;
406
}
···
415
}
416
417
input[type="range"]::-moz-range-thumb:hover {
418
+
background: var(--accent-hover);
419
transform: scale(1.2);
420
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 20%, transparent);
421
}
422
423
input[type="range"].muted::-moz-range-thumb {
···
426
427
input[type="range"].max::-moz-range-thumb {
428
background: var(--accent);
429
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 30%, transparent);
430
}
431
432
@media (max-width: 768px) {