aaurelions commited on
Commit
3825e3e
·
verified ·
1 Parent(s): dccba27

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1109 -19
index.html CHANGED
@@ -1,19 +1,1109 @@
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
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Audiomax Player</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --primary-color: #8B5CF6;
14
+ /* A nice purple for the main accent */
15
+ --primary-hover-color: #7C3AED;
16
+ --background-color: #1d2b3a;
17
+ --surface-color: #2a3447;
18
+ --text-color: #f5f5f7;
19
+ --text-muted-color: #a0aec0;
20
+ --border-color: rgba(255, 255, 255, 0.12);
21
+ --error-color: #EF4444;
22
+
23
+ --border-radius-lg: 24px;
24
+ --border-radius-md: 14px;
25
+ --transition-speed: 0.4s;
26
+ }
27
+
28
+ body {
29
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
30
+ margin: 0;
31
+ padding: 1.5rem;
32
+ display: flex;
33
+ justify-content: center;
34
+ align-items: center;
35
+ min-height: 100vh;
36
+ background-color: var(--background-color);
37
+ transition: background-color var(--transition-speed);
38
+ color: var(--text-color);
39
+ -webkit-font-smoothing: antialiased;
40
+ }
41
+
42
+ .container {
43
+ width: 100%;
44
+ max-width: 400px;
45
+ /* Increased max-width */
46
+ background: var(--surface-color);
47
+ border-radius: var(--border-radius-lg);
48
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.25);
49
+ border: 1px solid var(--border-color);
50
+ transition: background-color var(--transition-speed);
51
+ overflow: hidden;
52
+ position: relative;
53
+ }
54
+
55
+ /* --- NOTIFICATION --- */
56
+ #notification {
57
+ position: absolute;
58
+ top: 0;
59
+ left: 1.5rem;
60
+ right: 1.5rem;
61
+ background-color: var(--primary-color);
62
+ color: white;
63
+ padding: 1rem;
64
+ border-radius: 0 0 var(--border-radius-md) var(--border-radius-md);
65
+ text-align: center;
66
+ font-weight: 600;
67
+ transform: translateY(-120%);
68
+ transition: transform 0.4s ease-in-out;
69
+ z-index: 150;
70
+ }
71
+
72
+ #notification.show {
73
+ transform: translateY(0);
74
+ }
75
+
76
+ #notification.error {
77
+ background-color: var(--error-color);
78
+ }
79
+
80
+ /* --- VIEW TRANSITIONS --- */
81
+ .view {
82
+ transition: opacity var(--transition-speed), visibility var(--transition-speed);
83
+ }
84
+
85
+ .view:not(.visible) {
86
+ opacity: 0;
87
+ visibility: hidden;
88
+ display: none;
89
+ }
90
+
91
+ .view.visible {
92
+ opacity: 1;
93
+ visibility: visible;
94
+ display: block;
95
+ }
96
+
97
+
98
+ /* UPLOAD SECTION */
99
+ .upload-section {
100
+ padding: 2.5rem;
101
+ }
102
+
103
+ .upload-header {
104
+ text-align: center;
105
+ }
106
+
107
+ .upload-header h1 {
108
+ margin: 0 0 0.5rem;
109
+ font-size: 2.5rem;
110
+ font-weight: 700;
111
+ }
112
+
113
+ .upload-header p {
114
+ margin-bottom: 2rem;
115
+ color: var(--text-muted-color);
116
+ font-size: 1rem;
117
+ }
118
+
119
+ .upload-drop-zone {
120
+ cursor: pointer;
121
+ text-align: center;
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: center;
125
+ justify-content: center;
126
+ height: 100%;
127
+ padding: 2.5rem;
128
+ border: 2px dashed var(--border-color);
129
+ border-radius: var(--border-radius-md);
130
+ transition: all 0.2s ease-in-out;
131
+ }
132
+
133
+ .upload-drop-zone.dragover {
134
+ border-color: var(--primary-color);
135
+ background-color: rgba(139, 92, 246, 0.1);
136
+ }
137
+
138
+ .upload-drop-zone svg {
139
+ width: 48px;
140
+ height: 48px;
141
+ margin-bottom: 1rem;
142
+ fill: var(--primary-color);
143
+ }
144
+
145
+ .upload-drop-zone span {
146
+ font-weight: 600;
147
+ font-size: 1.1rem;
148
+ display: block;
149
+ }
150
+
151
+ .upload-drop-zone .subtext {
152
+ font-size: 0.85rem;
153
+ color: var(--text-muted-color);
154
+ margin-top: 0.25rem;
155
+ }
156
+
157
+ #file-input {
158
+ display: none;
159
+ }
160
+
161
+ .loader {
162
+ border: 4px solid var(--border-color);
163
+ border-top: 4px solid var(--primary-color);
164
+ border-radius: 50%;
165
+ width: 40px;
166
+ height: 40px;
167
+ animation: spin 1s linear infinite;
168
+ margin: 2rem auto;
169
+ display: none;
170
+ }
171
+
172
+ /* HISTORY SECTION */
173
+ .history-section {
174
+ margin-top: 2.5rem;
175
+ }
176
+
177
+ .history-section h2 {
178
+ font-size: 1rem;
179
+ font-weight: 600;
180
+ color: var(--text-muted-color);
181
+ text-align: center;
182
+ margin-bottom: 1.5rem;
183
+ text-transform: uppercase;
184
+ letter-spacing: 0.05em;
185
+ }
186
+
187
+ .history-grid {
188
+ display: grid;
189
+ grid-template-columns: repeat(3, 1fr);
190
+ gap: 1rem;
191
+ justify-items: center;
192
+ }
193
+
194
+ .history-item {
195
+ width: 90px;
196
+ height: 90px;
197
+ border-radius: var(--border-radius-md);
198
+ background-color: rgba(255, 255, 255, 0.05);
199
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
200
+ overflow: hidden;
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: center;
204
+ position: relative;
205
+ }
206
+
207
+ .history-item img {
208
+ width: 100%;
209
+ height: 100%;
210
+ object-fit: cover;
211
+ }
212
+
213
+ .history-item .title {
214
+ position: absolute;
215
+ bottom: 0;
216
+ left: 0;
217
+ right: 0;
218
+ background: rgba(0, 0, 0, 0.6);
219
+ backdrop-filter: blur(2px);
220
+ color: white;
221
+ font-size: 0.7rem;
222
+ padding: 4px 6px;
223
+ text-align: center;
224
+ white-space: nowrap;
225
+ overflow: hidden;
226
+ text-overflow: ellipsis;
227
+ }
228
+
229
+ /* PLAYER SECTION */
230
+ .player-header {
231
+ display: flex;
232
+ align-items: center;
233
+ padding: 0.8rem 1rem 0;
234
+ }
235
+
236
+ .player-header button {
237
+ background: none;
238
+ border: none;
239
+ cursor: pointer;
240
+ opacity: 0.7;
241
+ transition: opacity 0.2s;
242
+ padding: 0.5rem;
243
+ }
244
+
245
+ .player-header button:hover {
246
+ opacity: 1;
247
+ }
248
+
249
+ .player-header button svg {
250
+ width: 24px;
251
+ height: 24px;
252
+ fill: var(--text-color);
253
+ }
254
+
255
+ .main-player {
256
+ padding: 0 2rem 1.5rem;
257
+ }
258
+
259
+ #artwork-placeholder {
260
+ width: 75%;
261
+ max-width: 240px;
262
+ aspect-ratio: 1 / 1;
263
+ margin: 0.5rem auto 1.5rem;
264
+ background: rgba(255, 255, 255, 0.05);
265
+ border-radius: var(--border-radius-md);
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: center;
269
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
270
+ }
271
+
272
+ #artwork-placeholder img {
273
+ width: 100%;
274
+ height: 100%;
275
+ object-fit: cover;
276
+ border-radius: var(--border-radius-md);
277
+ }
278
+
279
+ #artwork-placeholder svg {
280
+ width: 60px;
281
+ height: 60px;
282
+ opacity: 0.5;
283
+ fill: var(--text-color);
284
+ }
285
+
286
+ #current-track {
287
+ text-align: center;
288
+ font-size: 1.4rem;
289
+ font-weight: 600;
290
+ margin-bottom: 0.5rem;
291
+ white-space: nowrap;
292
+ overflow: hidden;
293
+ text-overflow: ellipsis;
294
+ }
295
+
296
+ .time-display {
297
+ display: flex;
298
+ justify-content: space-between;
299
+ font-size: 0.8rem;
300
+ font-weight: 500;
301
+ color: var(--text-muted-color);
302
+ margin: 0.5rem 0 1.5rem;
303
+ }
304
+
305
+ input[type="range"] {
306
+ -webkit-appearance: none;
307
+ appearance: none;
308
+ width: 100%;
309
+ height: 6px;
310
+ background: rgba(255, 255, 255, 0.1);
311
+ border-radius: 3px;
312
+ outline: none;
313
+ cursor: pointer;
314
+ transition: all 0.2s;
315
+ }
316
+
317
+ input[type="range"]:hover {
318
+ height: 8px;
319
+ }
320
+
321
+ input[type="range"]::-webkit-slider-thumb {
322
+ -webkit-appearance: none;
323
+ appearance: none;
324
+ width: 18px;
325
+ height: 18px;
326
+ background: var(--primary-color);
327
+ border-radius: 50%;
328
+ border: 2px solid var(--surface-color);
329
+ }
330
+
331
+ .playback-controls {
332
+ text-align: center;
333
+ margin: 1.5rem 0;
334
+ }
335
+
336
+ .playback-controls button {
337
+ background: transparent;
338
+ border: none;
339
+ border-radius: 50%;
340
+ width: 50px;
341
+ height: 50px;
342
+ display: inline-grid;
343
+ place-items: center;
344
+ cursor: pointer;
345
+ transition: all 0.2s;
346
+ vertical-align: middle;
347
+ }
348
+
349
+ .playback-controls button svg {
350
+ fill: var(--text-color);
351
+ transition: fill 0.2s;
352
+ }
353
+
354
+ .playback-controls button:hover:not(:disabled) {
355
+ background-color: rgba(255, 255, 255, 0.05);
356
+ }
357
+
358
+ #play-pause-btn {
359
+ width: 70px;
360
+ height: 70px;
361
+ background-color: var(--primary-color);
362
+ margin: 0 1rem;
363
+ }
364
+
365
+ #play-pause-btn:hover {
366
+ background-color: var(--primary-hover-color);
367
+ }
368
+
369
+ #play-pause-btn svg {
370
+ fill: white;
371
+ width: 32px;
372
+ height: 32px;
373
+ }
374
+
375
+ #prev-btn svg,
376
+ #next-btn svg {
377
+ width: 28px;
378
+ height: 28px;
379
+ }
380
+
381
+ .speed-control-group {
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ gap: 0.8rem;
386
+ margin-top: 1rem;
387
+ }
388
+
389
+ .speed-btn {
390
+ font-size: 1.4rem;
391
+ line-height: 1;
392
+ font-weight: bold;
393
+ width: 40px;
394
+ height: 40px;
395
+ border-radius: 50%;
396
+ border: 1px solid var(--border-color);
397
+ background: transparent;
398
+ color: var(--text-muted-color);
399
+ cursor: pointer;
400
+ transition: all 0.2s;
401
+ }
402
+
403
+ .speed-btn:disabled {
404
+ opacity: 0.4;
405
+ cursor: not-allowed;
406
+ }
407
+
408
+ .speed-btn:hover:not(:disabled) {
409
+ background: var(--primary-color);
410
+ color: white;
411
+ border-color: var(--primary-color);
412
+ }
413
+
414
+ #speed-label {
415
+ text-align: center;
416
+ font-weight: 600;
417
+ font-size: 1.1rem;
418
+ min-width: 60px;
419
+ }
420
+
421
+ .playlist-section {
422
+ max-height: 220px;
423
+ overflow-y: auto;
424
+ border-top: 1px solid var(--border-color);
425
+ }
426
+
427
+ #playlist {
428
+ list-style: none;
429
+ margin: 0;
430
+ padding: 0;
431
+ }
432
+
433
+ #playlist li {
434
+ display: flex;
435
+ justify-content: space-between;
436
+ align-items: center;
437
+ padding: 1rem 1.5rem;
438
+ cursor: pointer;
439
+ transition: background-color 0.2s;
440
+ border-bottom: 1px solid var(--border-color);
441
+ }
442
+
443
+ #playlist li:last-child {
444
+ border-bottom: none;
445
+ }
446
+
447
+ #playlist li:hover {
448
+ background-color: rgba(255, 255, 255, 0.04);
449
+ }
450
+
451
+ #playlist li.active {
452
+ background-color: rgba(139, 92, 246, 0.08);
453
+ color: var(--text-color);
454
+ }
455
+
456
+ .playlist-track-info {
457
+ display: flex;
458
+ align-items: center;
459
+ overflow: hidden;
460
+ padding-right: 1rem;
461
+ }
462
+
463
+ .now-playing-icon {
464
+ display: flex;
465
+ gap: 2px;
466
+ width: 16px;
467
+ height: 16px;
468
+ margin-right: 12px;
469
+ align-items: flex-end;
470
+ display: none;
471
+ }
472
+
473
+ #playlist li.active .now-playing-icon {
474
+ display: flex;
475
+ }
476
+
477
+ @keyframes bounce {
478
+
479
+ 0%,
480
+ 100% {
481
+ transform: scaleY(0.4);
482
+ }
483
+
484
+ 50% {
485
+ transform: scaleY(1);
486
+ }
487
+ }
488
+
489
+ .now-playing-icon .bar {
490
+ width: 3px;
491
+ height: 100%;
492
+ background: var(--primary-color);
493
+ animation: bounce 1.2s ease-in-out infinite;
494
+ }
495
+
496
+ .now-playing-icon .bar:nth-child(2) {
497
+ animation-delay: -1.0s;
498
+ }
499
+
500
+ .now-playing-icon .bar:nth-child(3) {
501
+ animation-delay: -0.8s;
502
+ }
503
+
504
+ .playlist-track-title {
505
+ white-space: nowrap;
506
+ overflow: hidden;
507
+ text-overflow: ellipsis;
508
+ font-weight: 500;
509
+ }
510
+
511
+ #playlist li.active .playlist-track-title {
512
+ font-weight: 700;
513
+ color: var(--primary-color);
514
+ }
515
+
516
+ .playlist-track-duration {
517
+ font-size: 0.85rem;
518
+ color: var(--text-muted-color);
519
+ font-weight: 500;
520
+ white-space: nowrap;
521
+ }
522
+
523
+ /* MODAL */
524
+ .modal-overlay {
525
+ position: fixed;
526
+ top: 0;
527
+ left: 0;
528
+ right: 0;
529
+ bottom: 0;
530
+ background: rgba(0, 0, 0, 0.4);
531
+ backdrop-filter: blur(5px);
532
+ display: flex;
533
+ justify-content: center;
534
+ align-items: center;
535
+ opacity: 0;
536
+ visibility: hidden;
537
+ transition: all 0.3s;
538
+ z-index: 100;
539
+ }
540
+
541
+ .modal-overlay.visible {
542
+ opacity: 1;
543
+ visibility: visible;
544
+ }
545
+
546
+ .modal-content {
547
+ background: var(--surface-color);
548
+ border-radius: var(--border-radius-lg);
549
+ padding: 2rem;
550
+ text-align: center;
551
+ max-width: 320px;
552
+ transform: scale(0.9);
553
+ transition: all 0.3s;
554
+ }
555
+
556
+ .modal-overlay.visible .modal-content {
557
+ transform: scale(1);
558
+ }
559
+
560
+ .modal-content h3 {
561
+ margin-top: 0;
562
+ }
563
+
564
+ .modal-content p {
565
+ color: var(--text-muted-color);
566
+ margin-bottom: 2rem;
567
+ }
568
+
569
+ .modal-buttons {
570
+ display: flex;
571
+ gap: 1rem;
572
+ }
573
+
574
+ .modal-buttons button {
575
+ flex: 1;
576
+ padding: 0.8rem;
577
+ font-weight: 600;
578
+ cursor: pointer;
579
+ border-radius: var(--border-radius-md);
580
+ transition: all 0.2s;
581
+ }
582
+
583
+ .modal-buttons .modal-secondary-btn {
584
+ background: transparent;
585
+ border: 1px solid var(--border-color);
586
+ color: var(--text-color);
587
+ }
588
+
589
+ .modal-buttons .modal-secondary-btn:hover {
590
+ background: rgba(255, 255, 255, 0.05);
591
+ }
592
+
593
+ .modal-buttons .modal-primary-btn {
594
+ background: var(--primary-color);
595
+ color: white;
596
+ border: none;
597
+ }
598
+
599
+ .modal-buttons .modal-primary-btn:hover {
600
+ background: var(--primary-hover-color);
601
+ }
602
+
603
+ @keyframes spin {
604
+ 100% {
605
+ transform: rotate(360deg);
606
+ }
607
+ }
608
+ </style>
609
+ </head>
610
+
611
+ <body>
612
+ <div class="container">
613
+ <div id="notification"></div>
614
+
615
+ <!-- UPLOAD VIEW -->
616
+ <div class="upload-section view visible" id="upload-section">
617
+ <div class="upload-header">
618
+ <h1>Audiomax</h1>
619
+ <p>Listen at your own pace.</p>
620
+ </div>
621
+ <label for="file-input" class="upload-drop-zone" id="upload-drop-zone">
622
+ <svg viewBox="0 0 24 24">
623
+ <path
624
+ d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z" />
625
+ </svg>
626
+ <span>Choose Audiobook</span>
627
+ <span class="subtext">Click or drag & drop a .zip or audio file</span>
628
+ </label>
629
+ <input type="file" id="file-input" accept=".zip,.mp3,.wav,.ogg,.m4a,.flac">
630
+ <div class="loader" id="loader"></div>
631
+ <div class="history-section" id="history-section">
632
+ <h2>Recently Played</h2>
633
+ <div class="history-grid" id="history-grid">
634
+ <!-- History items will be injected here -->
635
+ </div>
636
+ </div>
637
+ </div>
638
+
639
+ <!-- PLAYER VIEW -->
640
+ <div class="player-section view" id="player-section">
641
+ <div class="player-header">
642
+ <button id="back-btn" title="Back to Upload">
643
+ <svg viewBox="0 0 24 24">
644
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
645
+ </svg>
646
+ </button>
647
+ </div>
648
+ <div class="main-player">
649
+ <div id="artwork-placeholder">
650
+ <!-- Artwork img or placeholder svg will be injected here -->
651
+ </div>
652
+ <h2 id="current-track">Track Title</h2>
653
+
654
+ <div class="progress-container">
655
+ <input type="range" id="progress-bar" value="0" step="0.1">
656
+ <div class="time-display">
657
+ <span id="current-time">0:00</span>
658
+ <span id="total-duration">0:00</span>
659
+ </div>
660
+ </div>
661
+
662
+ <div class="playback-controls">
663
+ <button id="prev-btn" title="Previous"><svg viewBox="0 0 24 24">
664
+ <path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
665
+ </svg></button>
666
+ <button id="play-pause-btn" title="Play/Pause"></button>
667
+ <button id="next-btn" title="Next"><svg viewBox="0 0 24 24">
668
+ <path d="M8 5v14l11-7zM18 6h-2v12h2z" />
669
+ </svg></button>
670
+ </div>
671
+
672
+ <div class="speed-control-group">
673
+ <button id="speed-down-btn" class="speed-btn" title="Decrease Speed">-</button>
674
+ <span id="speed-label">1.0x</span>
675
+ <button id="speed-up-btn" class="speed-btn" title="Increase Speed">+</button>
676
+ </div>
677
+ </div>
678
+
679
+ <div class="playlist-section">
680
+ <ul id="playlist"></ul>
681
+ </div>
682
+ </div>
683
+ </div>
684
+
685
+ <!-- RESUME MODAL -->
686
+ <div class="modal-overlay" id="resume-modal">
687
+ <div class="modal-content">
688
+ <h3>Resume Playback?</h3>
689
+ <p>We found a saved session. Would you like to continue from where you left off?</p>
690
+ <div class="modal-buttons">
691
+ <button class="modal-secondary-btn" id="resume-no">Start Over</button>
692
+ <button class="modal-primary-btn" id="resume-yes">Yes, Resume</button>
693
+ </div>
694
+ </div>
695
+ </div>
696
+
697
+ <!-- Libraries -->
698
+ <script src="https://unpkg.com/@zip.js/zip.js/dist/zip-full.min.js"></script>
699
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
700
+
701
+ <script>
702
+ document.addEventListener('DOMContentLoaded', () => {
703
+ const dom = {
704
+ fileInput: document.getElementById('file-input'),
705
+ backBtn: document.getElementById('back-btn'),
706
+ uploadDropZone: document.getElementById('upload-drop-zone'),
707
+ uploadSection: document.getElementById('upload-section'),
708
+ playerSection: document.getElementById('player-section'),
709
+ loader: document.getElementById('loader'),
710
+ currentTrackEl: document.getElementById('current-track'),
711
+ playPauseBtn: document.getElementById('play-pause-btn'),
712
+ prevBtn: document.getElementById('prev-btn'),
713
+ nextBtn: document.getElementById('next-btn'),
714
+ progressBar: document.getElementById('progress-bar'),
715
+ currentTimeEl: document.getElementById('current-time'),
716
+ totalDurationEl: document.getElementById('total-duration'),
717
+ speedLabel: document.getElementById('speed-label'),
718
+ speedDownBtn: document.getElementById('speed-down-btn'),
719
+ speedUpBtn: document.getElementById('speed-up-btn'),
720
+ playlistEl: document.getElementById('playlist'),
721
+ artworkPlaceholder: document.getElementById('artwork-placeholder'),
722
+ resumeModal: document.getElementById('resume-modal'),
723
+ resumeYesBtn: document.getElementById('resume-yes'),
724
+ resumeNoBtn: document.getElementById('resume-no'),
725
+ historyGrid: document.getElementById('history-grid'),
726
+ historySection: document.getElementById('history-section'),
727
+ notification: document.getElementById('notification'),
728
+ };
729
+
730
+ const audio = new Audio();
731
+ let playlistFiles = [];
732
+ let currentTrackIndex = 0;
733
+ let currentArchiveId = null;
734
+ let saveInterval = null;
735
+ let overallBookTitle = 'Untitled Audiobook';
736
+ let overallBookArtwork = null;
737
+
738
+ const SPEED_MIN = 0.5,
739
+ SPEED_MAX = 16.0,
740
+ SPEED_INCREMENT = 0.1;
741
+ const HISTORY_KEY = 'audiomax_history';
742
+ const MAX_HISTORY_ITEMS = 6;
743
+
744
+ const playIcon = `<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`;
745
+ const pauseIcon = `<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
746
+ const artworkIcon = `<svg viewBox="0 0 24 24"><path d="M6 22h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2zm6-14c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z" /></svg>`;
747
+
748
+ const setupEventListeners = () => {
749
+ dom.fileInput.addEventListener('change', (e) => handleFileUpload(e.target.files));
750
+ dom.backBtn.addEventListener('click', goBackToUpload);
751
+ dom.playPauseBtn.addEventListener('click', togglePlayPause);
752
+ dom.prevBtn.addEventListener('click', playPrevious);
753
+ dom.nextBtn.addEventListener('click', playNext);
754
+ dom.speedDownBtn.addEventListener('click', () => changeSpeed(-SPEED_INCREMENT));
755
+ dom.speedUpBtn.addEventListener('click', () => changeSpeed(SPEED_INCREMENT));
756
+ dom.progressBar.addEventListener('input', setSeek);
757
+ audio.addEventListener('timeupdate', updateProgress);
758
+ audio.addEventListener('loadedmetadata', handleTrackMetadata);
759
+ audio.addEventListener('ended', playNext);
760
+ audio.onplay = () => dom.playPauseBtn.innerHTML = pauseIcon;
761
+ audio.onpause = () => {
762
+ dom.playPauseBtn.innerHTML = playIcon;
763
+ saveState(); // Save state on pause
764
+ };
765
+ window.addEventListener('beforeunload', saveState);
766
+
767
+ const dropZone = dom.uploadDropZone;
768
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('dragover'); });
769
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
770
+ dropZone.addEventListener('drop', (e) => {
771
+ e.preventDefault();
772
+ dropZone.classList.remove('dragover');
773
+ if (e.dataTransfer.files.length) handleFileUpload(e.dataTransfer.files);
774
+ });
775
+ };
776
+
777
+ const showNotification = (message, type = 'info') => {
778
+ dom.notification.textContent = message;
779
+ dom.notification.className = type; // 'error' or 'info'
780
+ dom.notification.classList.add('show');
781
+ setTimeout(() => dom.notification.classList.remove('show'), 4000);
782
+ };
783
+
784
+ async function handleFileUpload(files) {
785
+ const file = files[0];
786
+ if (!file) return;
787
+
788
+ const isZip = file.name.endsWith('.zip');
789
+ const isAudio = file.type.startsWith('audio/');
790
+
791
+ if (!isZip && !isAudio) {
792
+ showNotification('Please upload a valid .zip or audio file.', 'error');
793
+ return;
794
+ }
795
+
796
+ dom.loader.style.display = 'block';
797
+ dom.uploadDropZone.style.display = 'none';
798
+ dom.historySection.style.display = 'none';
799
+ currentArchiveId = `${file.name}-${file.size}`;
800
+ overallBookTitle = file.name.replace(/\.[^/.]+$/, "");
801
+
802
+ try {
803
+ let extractedFiles;
804
+ if (isZip) {
805
+ extractedFiles = await unzipFile(file);
806
+ } else {
807
+ extractedFiles = [{ name: file.name, url: URL.createObjectURL(file), blob: file }];
808
+ }
809
+ if (extractedFiles.length === 0) throw new Error('No supported audio files found in the archive.');
810
+
811
+ playlistFiles = await Promise.all(extractedFiles.map(processFile));
812
+ playlistFiles.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }));
813
+
814
+ // Try to find a common title/artwork from tags
815
+ const firstFileWithTags = playlistFiles.find(f => f.tags);
816
+ if (firstFileWithTags) {
817
+ const tags = firstFileWithTags.tags;
818
+ overallBookTitle = tags.album || overallBookTitle;
819
+ overallBookArtwork = firstFileWithTags.artwork || null;
820
+ }
821
+
822
+ const savedState = getSavedState();
823
+ if (savedState) showResumePrompt(savedState);
824
+ else startPlayer();
825
+
826
+ } catch (error) {
827
+ showNotification(`Error: ${error.message}`, 'error');
828
+ resetToUploadView();
829
+ }
830
+ }
831
+
832
+ const showResumePrompt = (savedState) => {
833
+ dom.resumeModal.classList.add('visible');
834
+ dom.resumeYesBtn.onclick = () => { dom.resumeModal.classList.remove('visible'); startPlayer(savedState); };
835
+ dom.resumeNoBtn.onclick = () => { dom.resumeModal.classList.remove('visible'); startPlayer(); localStorage.removeItem(currentArchiveId); };
836
+ };
837
+
838
+ function startPlayer(initialState = {}) {
839
+ dom.uploadSection.classList.remove('visible');
840
+ dom.playerSection.classList.add('visible');
841
+ dom.loader.style.display = 'none';
842
+
843
+ buildPlaylist();
844
+ audio.playbackRate = initialState.speed || 1.0;
845
+ updateSpeedUI();
846
+ loadTrack(initialState.trackIndex || 0, initialState.time || 0);
847
+
848
+ updateAndSaveHistory({
849
+ id: currentArchiveId,
850
+ title: overallBookTitle,
851
+ artwork: overallBookArtwork
852
+ });
853
+
854
+ if (saveInterval) clearInterval(saveInterval);
855
+ saveInterval = setInterval(saveState, 5000);
856
+ }
857
+
858
+ function goBackToUpload() {
859
+ saveState();
860
+ resetPlayerState();
861
+ resetToUploadView();
862
+ renderHistory();
863
+ }
864
+
865
+ function resetPlayerState() {
866
+ clearInterval(saveInterval);
867
+ saveInterval = null;
868
+ audio.pause();
869
+ audio.src = '';
870
+ playlistFiles.forEach(file => URL.revokeObjectURL(file.url));
871
+ playlistFiles = [];
872
+ currentArchiveId = null;
873
+ overallBookArtwork = null;
874
+ overallBookTitle = 'Untitled Audiobook';
875
+ dom.playlistEl.innerHTML = '';
876
+ currentTrackIndex = 0;
877
+ dom.fileInput.value = ''; // Reset file input
878
+ }
879
+
880
+ function resetToUploadView() {
881
+ dom.playerSection.classList.remove('visible');
882
+ dom.uploadSection.classList.add('visible');
883
+ dom.loader.style.display = 'none';
884
+ dom.uploadDropZone.style.display = 'block';
885
+ dom.historySection.style.display = 'block';
886
+ }
887
+
888
+ function loadTrack(index, startTime = 0) {
889
+ if (index < 0 || index >= playlistFiles.length) return;
890
+ currentTrackIndex = index;
891
+ const track = playlistFiles[index];
892
+
893
+ const desiredSpeed = audio.playbackRate;
894
+
895
+ audio.src = track.url;
896
+ audio.currentTime = startTime;
897
+ dom.currentTrackEl.textContent = track.title;
898
+
899
+ updateArtwork(track);
900
+ updatePlaylistUI();
901
+
902
+ audio.addEventListener('canplay', () => {
903
+ audio.playbackRate = desiredSpeed;
904
+ }, { once: true }); // The {once: true} option is important to auto-remove the listener.
905
+
906
+ audio.play().catch(e => console.warn("Playback was interrupted.", e));
907
+ }
908
+
909
+ function updateArtwork(track) {
910
+ const artworkSrc = track.artwork || overallBookArtwork;
911
+ if (artworkSrc) {
912
+ dom.artworkPlaceholder.innerHTML = `<img src="${artworkSrc}" alt="Artwork for ${track.title}">`;
913
+ } else {
914
+ dom.artworkPlaceholder.innerHTML = artworkIcon;
915
+ }
916
+ }
917
+
918
+ function saveState() {
919
+ if (!currentArchiveId || isNaN(audio.currentTime)) return;
920
+ const state = {
921
+ trackIndex: currentTrackIndex,
922
+ time: audio.currentTime,
923
+ speed: audio.playbackRate
924
+ };
925
+ try {
926
+ localStorage.setItem(currentArchiveId, JSON.stringify(state));
927
+ } catch (e) {
928
+ console.error("Could not save state to localStorage.", e);
929
+ showNotification("Could not save progress.", "error");
930
+ }
931
+ }
932
+
933
+ const getSavedState = () => {
934
+ try {
935
+ return JSON.parse(localStorage.getItem(currentArchiveId));
936
+ } catch (e) { return null; }
937
+ }
938
+
939
+ function unzipFile(file) {
940
+ return new Promise(async (resolve, reject) => {
941
+ try {
942
+ const zipReader = new zip.ZipReader(new zip.BlobReader(file));
943
+ const entries = await zipReader.getEntries();
944
+ const audioFiles = [];
945
+ for (const entry of entries) {
946
+ if (entry.directory || entry.filename.startsWith('__MACOSX/')) continue;
947
+ if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(entry.filename)) {
948
+ const blob = await entry.getData(new zip.BlobWriter());
949
+ audioFiles.push({ name: entry.filename.split('/').pop(), url: URL.createObjectURL(blob), blob: blob });
950
+ }
951
+ }
952
+ await zipReader.close();
953
+ resolve(audioFiles);
954
+ } catch (e) { reject(new Error("Could not read zip file.")); }
955
+ });
956
+ }
957
+
958
+ function processFile(file) {
959
+ return new Promise((resolve) => {
960
+ jsmediatags.read(file.blob, {
961
+ onSuccess: (tag) => {
962
+ file.tags = tag.tags;
963
+ file.title = tag.tags.title || file.name.replace(/\.[^/.]+$/, "");
964
+ const { data, format } = tag.tags.picture || {};
965
+ if (data) {
966
+ const base64String = btoa(data.reduce((acc, byte) => acc + String.fromCharCode(byte), ''));
967
+ file.artwork = `data:${format};base64,${base64String}`;
968
+ }
969
+ resolve(file);
970
+ },
971
+ onError: () => { file.title = file.name.replace(/\.[^/.]+$/, ""); resolve(file); }
972
+ });
973
+ });
974
+ }
975
+
976
+ function handleTrackMetadata() {
977
+ const duration = audio.duration;
978
+ dom.progressBar.max = duration;
979
+ dom.totalDurationEl.textContent = formatTime(duration);
980
+ }
981
+
982
+ const togglePlayPause = () => { if (audio.src) audio.paused ? audio.play() : audio.pause(); };
983
+ const playPrevious = () => loadTrack((currentTrackIndex - 1 + playlistFiles.length) % playlistFiles.length);
984
+ const playNext = () => {
985
+ if (currentTrackIndex >= playlistFiles.length - 1) {
986
+ showNotification("Audiobook finished!", "info");
987
+ goBackToUpload();
988
+ return;
989
+ }
990
+ loadTrack((currentTrackIndex + 1) % playlistFiles.length);
991
+ }
992
+
993
+ function changeSpeed(increment) {
994
+ let newSpeed = parseFloat((audio.playbackRate + increment).toFixed(2));
995
+ newSpeed = Math.max(SPEED_MIN, Math.min(newSpeed, SPEED_MAX));
996
+ audio.playbackRate = newSpeed;
997
+ updateSpeedUI();
998
+ }
999
+
1000
+ function updateSpeedUI() {
1001
+ const currentSpeed = audio.playbackRate;
1002
+ dom.speedLabel.textContent = `${currentSpeed.toFixed(1)}x`;
1003
+ dom.speedDownBtn.disabled = (currentSpeed <= SPEED_MIN);
1004
+ dom.speedUpBtn.disabled = (currentSpeed >= SPEED_MAX);
1005
+ }
1006
+
1007
+ function updateProgress() {
1008
+ if (isNaN(audio.duration)) return;
1009
+ dom.progressBar.value = audio.currentTime;
1010
+ dom.currentTimeEl.textContent = formatTime(audio.currentTime);
1011
+ }
1012
+
1013
+ const setSeek = () => audio.currentTime = dom.progressBar.value;
1014
+
1015
+ function buildPlaylist() {
1016
+ dom.playlistEl.innerHTML = '';
1017
+ playlistFiles.forEach((file, index) => {
1018
+ const li = document.createElement('li');
1019
+ li.dataset.index = index;
1020
+ li.innerHTML = `
1021
+ <div class="playlist-track-info">
1022
+ <div class="now-playing-icon"><div class="bar"></div><div class="bar"></div><div class="bar"></div></div>
1023
+ <span class="playlist-track-title">${file.title}</span>
1024
+ </div>
1025
+ <span class="playlist-track-duration">--:--</span>`;
1026
+ li.addEventListener('click', () => { if (currentTrackIndex !== index) loadTrack(index); });
1027
+ dom.playlistEl.appendChild(li);
1028
+
1029
+ // Get duration async and update UI
1030
+ if (file.duration) {
1031
+ li.querySelector('.playlist-track-duration').textContent = formatTime(file.duration);
1032
+ } else {
1033
+ const tempAudio = new Audio(file.url);
1034
+ tempAudio.onloadedmetadata = () => {
1035
+ file.duration = tempAudio.duration;
1036
+ li.querySelector('.playlist-track-duration').textContent = formatTime(file.duration);
1037
+ };
1038
+ }
1039
+ });
1040
+ }
1041
+
1042
+ function updatePlaylistUI() {
1043
+ Array.from(dom.playlistEl.children).forEach((item, index) => {
1044
+ item.classList.toggle('active', index === currentTrackIndex);
1045
+ if (index === currentTrackIndex) {
1046
+ item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1047
+ }
1048
+ });
1049
+ }
1050
+
1051
+ function formatTime(seconds) {
1052
+ if (isNaN(seconds) || seconds < 0) return "0:00";
1053
+ const h = Math.floor(seconds / 3600);
1054
+ const m = Math.floor((seconds % 3600) / 60);
1055
+ const s = Math.floor(seconds % 60);
1056
+ const sFmt = `${s < 10 ? '0' : ''}${s}`;
1057
+ return h > 0 ? `${h}:${m < 10 ? '0' : ''}${m}:${sFmt}` : `${m}:${sFmt}`;
1058
+ }
1059
+
1060
+ // --- HISTORY FUNCTIONS ---
1061
+ function getHistory() {
1062
+ try {
1063
+ return JSON.parse(localStorage.getItem(HISTORY_KEY)) || [];
1064
+ } catch (e) { return []; }
1065
+ }
1066
+
1067
+ function updateAndSaveHistory(bookData) {
1068
+ let history = getHistory();
1069
+ // Remove existing entry if it exists
1070
+ history = history.filter(item => item.id !== bookData.id);
1071
+ // Add new entry to the front
1072
+ history.unshift(bookData);
1073
+ // Trim history to max length
1074
+ history = history.slice(0, MAX_HISTORY_ITEMS);
1075
+ localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
1076
+ }
1077
+
1078
+ function renderHistory() {
1079
+ const history = getHistory();
1080
+ dom.historyGrid.innerHTML = '';
1081
+ if (history.length === 0) {
1082
+ dom.historySection.style.display = 'none';
1083
+ return;
1084
+ }
1085
+ dom.historySection.style.display = 'block';
1086
+
1087
+ history.forEach(book => {
1088
+ const item = document.createElement('div');
1089
+ item.className = 'history-item';
1090
+ const rotation = Math.random() * 8 - 4; // -4 to +4 degrees
1091
+ item.style.transform = `rotate(${rotation}deg)`;
1092
+ item.innerHTML = `
1093
+ ${book.artwork ? `<img src="${book.artwork}" alt="">` : artworkIcon}
1094
+ <div class="title">${book.title}</div>
1095
+ `;
1096
+ dom.historyGrid.appendChild(item);
1097
+ });
1098
+ }
1099
+
1100
+ // --- INITIAL SETUP ---
1101
+ dom.playPauseBtn.innerHTML = playIcon;
1102
+ dom.artworkPlaceholder.innerHTML = artworkIcon;
1103
+ renderHistory();
1104
+ setupEventListeners();
1105
+ });
1106
+ </script>
1107
+ </body>
1108
+
1109
+ </html>