ArtelTaleb commited on
Commit
69c7e8a
Β·
verified Β·
1 Parent(s): 62fea51

Add retro player HTML

Browse files
Files changed (1) hide show
  1. index.html +626 -18
index.html CHANGED
@@ -1,19 +1,627 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Retro iPod Player</title>
7
+ <style>
8
+ /* ─── FONTS & CSS RESET ──────────────────────────────────────────────────────── */
9
+ * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
10
+
11
+ /* A pixel font brings the old LCD vibe. Fallback to monospace if unavailable */
12
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap');
13
+
14
+ body {
15
+ background: #1a1a1a;
16
+ display: flex;
17
+ justify-content: center;
18
+ align-items: center;
19
+ min-height: 100vh;
20
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
21
+ }
22
+
23
+ /* ─── IPOD BODY ──────────────────────────────────────────────────────────────── */
24
+ .ipod-body {
25
+ width: 380px;
26
+ height: 700px;
27
+ background: #ffffff;
28
+ border-radius: 40px;
29
+ box-shadow:
30
+ 0 0 0 2px #d0d0d0 inset,
31
+ 0 20px 40px rgba(0,0,0,0.6),
32
+ 0 -5px 15px rgba(255,255,255,0.8) inset,
33
+ 0 5px 10px rgba(0,0,0,0.1);
34
+ position: relative;
35
+ padding: 30px;
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ }
40
+
41
+ /* Black rim around the screen */
42
+ .screen-bezel {
43
+ width: 100%;
44
+ height: 340px;
45
+ background: #000000;
46
+ border-radius: 12px;
47
+ padding: 10px;
48
+ box-shadow: 0 4px 8px rgba(0,0,0,0.2) inset;
49
+ margin-bottom: 40px;
50
+ }
51
+
52
+ /* ─── LCD SCREEN ─────────────────────────────────────────────────────────────── */
53
+ .screen {
54
+ width: 100%;
55
+ height: 100%;
56
+ background: #e8e8e8; /* Default white-ish grayscale background */
57
+ border-radius: 4px;
58
+ overflow: hidden;
59
+ position: relative;
60
+ font-family: 'VT323', monospace; /* Pixelated retro font */
61
+ color: #000;
62
+ display: flex;
63
+ flex-direction: column;
64
+ }
65
+
66
+ /* Screen Scanlines / Grayscale Filter effect */
67
+ .screen::after {
68
+ content: '';
69
+ position: absolute;
70
+ inset: 0;
71
+ background: linear-gradient(rgba(0,0,0,0.03) 50%, transparent 50%);
72
+ background-size: 100% 4px;
73
+ pointer-events: none;
74
+ z-index: 100;
75
+ }
76
+
77
+ /* Status Bar */
78
+ .status-bar {
79
+ display: flex;
80
+ justify-content: space-between;
81
+ align-items: center;
82
+ padding: 4px 8px;
83
+ border-bottom: 2px solid #000;
84
+ font-size: 18px;
85
+ font-weight: bold;
86
+ letter-spacing: 1px;
87
+ background: #fff; /* Slight contrast in the B&W motif */
88
+ }
89
+
90
+ /* ─── NOW PLAYING BLOCK ──────────────────────────────────────────────────────── */
91
+ .now-playing {
92
+ padding: 8px;
93
+ border-bottom: 2px solid #000;
94
+ background: #fff;
95
+ }
96
+ .np-label { font-size: 14px; text-transform: uppercase; font-family: 'VT323', monospace; }
97
+ .np-title {
98
+ font-size: 20px;
99
+ font-weight: bold;
100
+ white-space: nowrap;
101
+ overflow: hidden;
102
+ text-overflow: ellipsis;
103
+ font-family: 'VT323', monospace;
104
+ margin-top: 2px;
105
+ }
106
+ .np-artist { font-size: 16px; font-family: 'VT323', monospace; color: #333; }
107
+
108
+ /* Progress bar */
109
+ .progress-wrap {
110
+ margin-top: 6px;
111
+ height: 6px;
112
+ background: #ccc;
113
+ border: 1px solid #000;
114
+ cursor: pointer;
115
+ position: relative;
116
+ }
117
+ .progress-fill { height: 100%; background: #000; width: 0%; pointer-events: none; }
118
+ .np-time {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ font-size: 14px;
122
+ font-family: 'VT323', monospace;
123
+ margin-top: 2px;
124
+ }
125
+
126
+ /* ─── PLAYLIST ────────────────────────────────────────────────────────────────── */
127
+ .playlist {
128
+ flex: 1;
129
+ overflow-y: auto;
130
+ font-family: 'VT323', monospace;
131
+ background: #e8e8e8;
132
+ }
133
+ .pl-item {
134
+ display: flex;
135
+ align-items: center;
136
+ padding: 3px 6px;
137
+ font-size: 16px;
138
+ border-bottom: 1px solid #ccc;
139
+ cursor: pointer;
140
+ overflow: hidden;
141
+ }
142
+ .pl-item.active {
143
+ background: #000;
144
+ color: #fff;
145
+ }
146
+ .pl-index { width: 22px; flex-shrink: 0; font-size: 13px; opacity: 0.6; }
147
+ .pl-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
148
+
149
+ /* ─── SCREEN FOOTER ──────────────────────────────────────────────────────────── */
150
+ .screen-footer {
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ padding: 4px 6px;
155
+ font-size: 16px;
156
+ font-family: 'VT323', monospace;
157
+ background: #fff;
158
+ border-top: 2px solid #000;
159
+ flex-shrink: 0;
160
+ }
161
+ .footer-btn {
162
+ background: #000;
163
+ color: #fff;
164
+ padding: 1px 7px;
165
+ border-radius: 6px;
166
+ font-size: 14px;
167
+ cursor: pointer;
168
+ }
169
+
170
+ /* ─── DRAG OVERLAY ───────────────────────────────────────────────────────────── */
171
+ .drop-overlay {
172
+ display: none;
173
+ position: absolute;
174
+ inset: 0;
175
+ background: rgba(0,0,0,0.55);
176
+ border-radius: 40px;
177
+ z-index: 200;
178
+ align-items: center;
179
+ justify-content: center;
180
+ flex-direction: column;
181
+ color: #fff;
182
+ font-family: 'VT323', monospace;
183
+ font-size: 28px;
184
+ pointer-events: none;
185
+ }
186
+ .drop-overlay.active { display: flex; }
187
+
188
+ /* Drop Overlay */
189
+ .screen-overlay {
190
+ position: absolute;
191
+ inset: 0;
192
+ background: rgba(255,255,255,0.9);
193
+ display: flex;
194
+ flex-direction: column;
195
+ align-items: center;
196
+ justify-content: center;
197
+ text-align: center;
198
+ z-index: 50;
199
+ cursor: pointer;
200
+ }
201
+ .screen-overlay h2 { font-size: 24px; margin-bottom: 10px; }
202
+ .screen-overlay p { font-size: 16px; }
203
+ #file-input { display: none; }
204
+
205
+
206
+ /* ─── CLICK WHEEL ────────────────────────────────────────────────────────────── */
207
+ .wheel-container {
208
+ width: 250px;
209
+ height: 250px;
210
+ border-radius: 50%;
211
+ background: #111;
212
+ box-shadow:
213
+ 0 2px 5px rgba(0,0,0,0.5),
214
+ 0 0 0 1px #333 inset;
215
+ position: relative;
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ }
220
+
221
+ .wheel-label {
222
+ position: absolute;
223
+ color: #fff;
224
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
225
+ font-weight: bold;
226
+ font-size: 14px;
227
+ letter-spacing: 1px;
228
+ pointer-events: none;
229
+ }
230
+
231
+ .w-top { top: 25px; }
232
+ .w-bottom { bottom: 25px; font-size: 16px; } /* Play/Pause icon */
233
+ .w-left { left: 25px; font-size: 18px; } /* Rewind */
234
+ .w-right { right: 25px; font-size: 18px; } /* FF */
235
+
236
+ .wheel-center {
237
+ width: 90px;
238
+ height: 90px;
239
+ background: #fff;
240
+ border-radius: 50%;
241
+ box-shadow:
242
+ 0 2px 4px rgba(0,0,0,0.3) inset,
243
+ 0 0 0 1px #d0d0d0;
244
+ cursor: pointer;
245
+ display: flex;
246
+ align-items: center;
247
+ justify-content: center;
248
+ transition: transform 0.1s;
249
+ }
250
+ .wheel-center:active {
251
+ transform: scale(0.95);
252
+ background: #f0f0f0;
253
+ }
254
+
255
+ /* Clickable zones over the black wheel */
256
+ .zone {
257
+ position: absolute;
258
+ width: 60px;
259
+ height: 60px;
260
+ cursor: pointer;
261
+ border-radius: 50%;
262
+ z-index: 10;
263
+ }
264
+ .z-top { top: 0; left: 50%; transform: translateX(-50%); }
265
+ .z-bottom { bottom: 0; left: 50%; transform: translateX(-50%); }
266
+ .z-left { left: 0; top: 50%; transform: translateY(-50%); }
267
+ .z-right { right: 0; top: 50%; transform: translateY(-50%); }
268
+
269
+ </style>
270
+ </head>
271
+ <body>
272
+
273
+ <div class="ipod-body">
274
+
275
+ <!-- SCREEN -->
276
+ <div class="screen-bezel">
277
+ <div class="screen">
278
+
279
+ <!-- Drop overlay (visible quand drag sur iPod body) -->
280
+ <div class="screen-overlay" id="overlay" onclick="document.getElementById('file-input').click()">
281
+ <h2>RETRO PLAYER</h2>
282
+ <p>Click Center Button<br>or Drop Audio</p>
283
+ </div>
284
+ <input type="file" id="file-input" accept="audio/*" multiple onchange="importFiles(event)">
285
+
286
+ <!-- Status bar -->
287
+ <div class="status-bar">
288
+ <span id="clock">00:00</span>
289
+ <span>PLAYER</span>
290
+ <span id="shuffle-icon"></span>
291
+ </div>
292
+
293
+ <!-- Now Playing -->
294
+ <div class="now-playing">
295
+ <div class="np-label">NOW PLAYING:</div>
296
+ <div class="np-title" id="np-title">β€”</div>
297
+ <div class="np-artist" id="np-artist"></div>
298
+ <div class="progress-wrap" id="progress-wrap">
299
+ <div class="progress-fill" id="progress-fill"></div>
300
+ </div>
301
+ <div class="np-time">
302
+ <span id="np-elapsed">0:00</span>
303
+ <span id="np-position">1 / 0</span>
304
+ <span id="np-duration">0:00</span>
305
+ </div>
306
+ </div>
307
+
308
+ <!-- Playlist -->
309
+ <div class="playlist" id="playlist"></div>
310
+
311
+ <!-- Footer -->
312
+ <div class="screen-footer">
313
+ <span class="footer-btn" id="btn-menu" onclick="toggleShuffle()">MENU</span>
314
+ <span id="footer-count">0 / 0</span>
315
+ <span class="footer-btn" onclick="document.getElementById('file-input').click()">+ADD</span>
316
+ </div>
317
+
318
+ </div>
319
+ </div>
320
+
321
+ <!-- CLICK WHEEL -->
322
+ <div class="wheel-container">
323
+ <span class="wheel-label w-top">MENU</span>
324
+ <span class="wheel-label w-bottom">β–Ά/II</span>
325
+ <span class="wheel-label w-left">Iβ—€β—€</span>
326
+ <span class="wheel-label w-right">β–Άβ–ΆI</span>
327
+
328
+ <!-- Interaction Zones -->
329
+ <div class="zone z-top" onclick="toggleShuffle()"></div> <!-- Top: Shuffle -->
330
+ <div class="zone z-bottom" onclick="togglePlay()"></div> <!-- Bottom: Play/Pause -->
331
+ <div class="zone z-left" onclick="prevTrack()"></div> <!-- Left: Previous -->
332
+ <div class="zone z-right" onclick="nextTrack()"></div> <!-- Right: Next -->
333
+
334
+ <div class="wheel-center" onclick="document.getElementById('file-input').click()"></div>
335
+ </div>
336
+
337
+ <div class="drop-overlay" id="drop-overlay">
338
+ <div>DROP HERE</div>
339
+ <div style="font-size:18px;margin-top:8px;">+ ajouter Γ  la playlist</div>
340
+ </div>
341
+
342
+ </div>
343
+
344
+ <script>
345
+ // ─── STATE ────────────────────────────────────────────────────────────────────
346
+ const state = {
347
+ tracks: [], // [{ name, artist, title, buffer }]
348
+ currentIndex: -1,
349
+ isPlaying: false,
350
+ shuffle: false,
351
+ shuffleOrder: [], // indices mΓ©langΓ©s
352
+ shufflePos: 0, // position dans shuffleOrder
353
+ };
354
+
355
+ let audioCtx = null;
356
+ let sourceNode = null;
357
+ let startTime = 0;
358
+ let pauseOffset = 0;
359
+ let animFrame = null;
360
+
361
+ // ─── UTILITAIRES ──────────────────────────────────────────────────────────────
362
+ function parseName(filename) {
363
+ const base = filename.replace(/\.[^/.]+$/, '');
364
+ const sep = base.indexOf(' - ');
365
+ if (sep !== -1) {
366
+ return { artist: base.slice(0, sep).trim(), title: base.slice(sep + 3).trim() };
367
+ }
368
+ return { artist: '', title: base.trim() };
369
+ }
370
+
371
+ function formatTime(sec) {
372
+ if (!isFinite(sec)) return '0:00';
373
+ const m = Math.floor(sec / 60);
374
+ const s = Math.floor(sec % 60).toString().padStart(2, '0');
375
+ return `${m}:${s}`;
376
+ }
377
+
378
+ // Horloge
379
+ setInterval(() => {
380
+ const d = new Date();
381
+ document.getElementById('clock').textContent =
382
+ d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
383
+ }, 1000);
384
+
385
+ // ─── IMPORT ───────────────────────────────────────────────────────────────────
386
+ async function addTracksToPlaylist(files) {
387
+ if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
388
+ for (const file of files) {
389
+ const arrayBuffer = await file.arrayBuffer();
390
+ const buffer = await audioCtx.decodeAudioData(arrayBuffer);
391
+ const { artist, title } = parseName(file.name);
392
+ state.tracks.push({ name: file.name, artist, title, buffer });
393
+ }
394
+ document.getElementById('overlay').style.display = 'none';
395
+ renderPlaylist();
396
+ if (state.currentIndex === -1) { loadTrack(0); audioPlay(); }
397
+ if (state.shuffle) state.shuffleOrder = buildShuffleOrder();
398
+ }
399
+
400
+ async function importFiles(event) {
401
+ const files = Array.from(event.target.files);
402
+ if (!files.length) return;
403
+ await addTracksToPlaylist(files);
404
+ event.target.value = '';
405
+ }
406
+
407
+ // ─── AUDIO ENGINE ─────────────────────────────────────────────────────────────
408
+ function loadTrack(index) {
409
+ if (index < 0 || index >= state.tracks.length) return;
410
+ audioStop();
411
+ state.currentIndex = index;
412
+ pauseOffset = 0;
413
+ updateNowPlaying();
414
+ renderPlaylist();
415
+ }
416
+
417
+ function audioPlay() {
418
+ if (!audioCtx || state.currentIndex < 0) return;
419
+ if (audioCtx.state === 'suspended') audioCtx.resume();
420
+ cancelAnimationFrame(animFrame);
421
+
422
+ if (sourceNode) { try { sourceNode.disconnect(); } catch(e){} }
423
+ sourceNode = audioCtx.createBufferSource();
424
+ sourceNode.buffer = state.tracks[state.currentIndex].buffer;
425
+ sourceNode.connect(audioCtx.destination);
426
+ startTime = audioCtx.currentTime - pauseOffset;
427
+ sourceNode.start(0, pauseOffset);
428
+ state.isPlaying = true;
429
+ sourceNode.onended = onTrackEnded;
430
+ tickProgress();
431
+ }
432
+
433
+ function audioPause() {
434
+ if (!state.isPlaying) return;
435
+ pauseOffset = audioCtx.currentTime - startTime;
436
+ if (sourceNode) {
437
+ sourceNode.onended = null;
438
+ try { sourceNode.stop(); } catch(e){}
439
+ }
440
+ state.isPlaying = false;
441
+ cancelAnimationFrame(animFrame);
442
+ }
443
+
444
+ function audioStop() {
445
+ if (sourceNode) {
446
+ sourceNode.onended = null;
447
+ try { sourceNode.stop(); } catch(e){}
448
+ }
449
+ state.isPlaying = false;
450
+ pauseOffset = 0;
451
+ cancelAnimationFrame(animFrame);
452
+ document.getElementById('progress-fill').style.width = '0%';
453
+ document.getElementById('np-elapsed').textContent = '0:00';
454
+ }
455
+
456
+ function togglePlay() {
457
+ if (state.currentIndex < 0) return;
458
+ if (state.isPlaying) audioPause(); else audioPlay();
459
+ }
460
+
461
+ // ─── NAVIGATION ───────────────────────────────────────────────────────────────
462
+ function onTrackEnded() {
463
+ if (!state.isPlaying) return;
464
+ state.isPlaying = false;
465
+ nextTrack();
466
+ }
467
+
468
+ function nextTrack() {
469
+ if (!state.tracks.length) return;
470
+ if (state.shuffle) {
471
+ state.shufflePos++;
472
+ if (state.shufflePos >= state.shuffleOrder.length) {
473
+ state.currentIndex = state.shuffleOrder[state.shuffleOrder.length - 1];
474
+ audioStop();
475
+ updateNowPlaying();
476
+ renderPlaylist();
477
+ return;
478
+ }
479
+ loadTrack(state.shuffleOrder[state.shufflePos]);
480
+ } else {
481
+ if (state.currentIndex >= state.tracks.length - 1) {
482
+ audioStop();
483
+ return;
484
+ }
485
+ loadTrack(state.currentIndex + 1);
486
+ }
487
+ audioPlay();
488
+ }
489
+
490
+ function prevTrack() {
491
+ if (!state.tracks.length) return;
492
+ const elapsed = state.isPlaying ? audioCtx.currentTime - startTime : pauseOffset;
493
+ if (elapsed > 3) {
494
+ pauseOffset = 0;
495
+ if (state.isPlaying) audioPause();
496
+ audioPlay();
497
+ return;
498
+ }
499
+ if (state.shuffle) {
500
+ state.shufflePos = Math.max(0, state.shufflePos - 1);
501
+ loadTrack(state.shuffleOrder[state.shufflePos]);
502
+ } else {
503
+ loadTrack(Math.max(0, state.currentIndex - 1));
504
+ }
505
+ audioPlay();
506
+ }
507
+
508
+ // ─── UI ───────────────────────────────────────────────────────────────────────
509
+ function renderPlaylist() {
510
+ const el = document.getElementById('playlist');
511
+ el.innerHTML = '';
512
+ state.tracks.forEach((t, i) => {
513
+ const row = document.createElement('div');
514
+ row.className = 'pl-item' + (i === state.currentIndex ? ' active' : '');
515
+ const idx = document.createElement('span');
516
+ idx.className = 'pl-index';
517
+ idx.textContent = String(i + 1).padStart(2, '0');
518
+ const nm = document.createElement('span');
519
+ nm.className = 'pl-name';
520
+ nm.textContent = (t.title || t.name) + (t.artist ? ' β€” ' + t.artist : '');
521
+ row.appendChild(idx);
522
+ row.appendChild(nm);
523
+ row.onclick = () => { loadTrack(i); audioPlay(); };
524
+ el.appendChild(row);
525
+ });
526
+
527
+ // Scroll la piste active dans la vue
528
+ const active = el.querySelector('.active');
529
+ if (active) active.scrollIntoView({ block: 'nearest' });
530
+
531
+ // Footer count
532
+ document.getElementById('footer-count').textContent =
533
+ state.tracks.length ? `${state.currentIndex + 1} / ${state.tracks.length}` : '0 / 0';
534
+ }
535
+
536
+ function updateNowPlaying() {
537
+ if (state.currentIndex < 0 || state.currentIndex >= state.tracks.length) return;
538
+ const t = state.tracks[state.currentIndex];
539
+ document.getElementById('np-title').textContent = t.title || t.name;
540
+ document.getElementById('np-artist').textContent = t.artist || '';
541
+ document.getElementById('np-duration').textContent = formatTime(t.buffer.duration);
542
+ document.getElementById('np-position').textContent =
543
+ `${state.currentIndex + 1} / ${state.tracks.length}`;
544
+ document.getElementById('footer-count').textContent =
545
+ `${state.currentIndex + 1} / ${state.tracks.length}`;
546
+ }
547
+
548
+ function tickProgress() {
549
+ if (!state.isPlaying) return;
550
+
551
+ const t = state.tracks[state.currentIndex];
552
+ if (!t) return;
553
+
554
+ const elapsed = audioCtx.currentTime - startTime;
555
+ const pct = Math.min(100, (elapsed / t.buffer.duration) * 100);
556
+
557
+ document.getElementById('progress-fill').style.width = pct + '%';
558
+ document.getElementById('np-elapsed').textContent = formatTime(elapsed);
559
+
560
+ animFrame = requestAnimationFrame(tickProgress);
561
+ }
562
+
563
+ document.getElementById('progress-wrap').addEventListener('click', (e) => {
564
+ if (state.currentIndex < 0) return;
565
+ const rect = e.currentTarget.getBoundingClientRect();
566
+ const ratio = (e.clientX - rect.left) / rect.width;
567
+ const duration = state.tracks[state.currentIndex].buffer.duration;
568
+ pauseOffset = ratio * duration;
569
+ if (state.isPlaying) { audioPause(); audioPlay(); }
570
+ else {
571
+ document.getElementById('progress-fill').style.width = (ratio * 100) + '%';
572
+ document.getElementById('np-elapsed').textContent = formatTime(pauseOffset);
573
+ }
574
+ });
575
+
576
+ // ─── DRAG & DROP ──────────────────────────────────────────────────────────────
577
+ const ipodBody = document.querySelector('.ipod-body');
578
+ const dropOverlay = document.getElementById('drop-overlay');
579
+
580
+ ipodBody.addEventListener('dragover', (e) => {
581
+ e.preventDefault();
582
+ dropOverlay.classList.add('active');
583
+ });
584
+
585
+ ipodBody.addEventListener('dragleave', (e) => {
586
+ // Ne masquer que si on quitte vraiment l'iPod (pas juste un enfant)
587
+ if (!ipodBody.contains(e.relatedTarget)) {
588
+ dropOverlay.classList.remove('active');
589
+ }
590
+ });
591
+
592
+ ipodBody.addEventListener('drop', async (e) => {
593
+ e.preventDefault();
594
+ dropOverlay.classList.remove('active');
595
+ const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('audio/'));
596
+ if (!files.length) return;
597
+ await addTracksToPlaylist(files);
598
+ });
599
+
600
+ // ─── SHUFFLE ──────────────────────────────────────────────────────────────────
601
+ function buildShuffleOrder() {
602
+ const order = state.tracks.map((_, i) => i);
603
+ // Fisher-Yates
604
+ for (let i = order.length - 1; i > 0; i--) {
605
+ const j = Math.floor(Math.random() * (i + 1));
606
+ [order[i], order[j]] = [order[j], order[i]];
607
+ }
608
+ // Mettre la piste courante en premier si elle existe
609
+ if (state.currentIndex >= 0) {
610
+ const ci = order.indexOf(state.currentIndex);
611
+ if (ci !== -1) { [order[0], order[ci]] = [order[ci], order[0]]; }
612
+ }
613
+ return order;
614
+ }
615
+
616
+ function toggleShuffle() {
617
+ state.shuffle = !state.shuffle;
618
+ if (state.shuffle) {
619
+ state.shuffleOrder = buildShuffleOrder();
620
+ state.shufflePos = 0;
621
+ }
622
+ document.getElementById('shuffle-icon').textContent = state.shuffle ? '⇄' : '';
623
+ }
624
+
625
+ </script>
626
+ </body>
627
  </html>