AdityaAdaki commited on
Commit
31fbee6
·
1 Parent(s): 094028b

ui/ux fixes

Browse files
Files changed (4) hide show
  1. static/css/style.css +581 -728
  2. static/img/favicon.svg +4 -0
  3. static/js/main.js +743 -260
  4. templates/index.html +84 -17
static/css/style.css CHANGED
@@ -1,990 +1,843 @@
1
  :root {
2
  --primary-color: #4CAF50;
3
  --primary-hover: #45a049;
 
 
 
4
  --bg-color: #0a0a0a;
5
  --surface-color: rgba(15, 23, 42, 0.8);
 
6
  --text-primary: #ffffff;
7
  --text-secondary: rgba(255, 255, 255, 0.7);
8
  --error-color: #ff4444;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
10
 
11
  /* Light theme variables */
12
  [data-theme="light"] {
13
  --bg-color: #f5f5f5;
14
  --surface-color: rgba(255, 255, 255, 0.9);
 
15
  --text-primary: #1a1a1a;
16
  --text-secondary: rgba(0, 0, 0, 0.7);
17
  }
18
 
19
- .app-header {
 
20
  position: fixed;
21
  top: 0;
22
  left: 0;
23
- right: 0;
24
- display: flex;
25
- justify-content: space-between;
26
- align-items: center;
27
- padding: 0.75rem 1.5rem;
28
- background: var(--surface-color);
29
- backdrop-filter: blur(12px);
30
- z-index: 100;
31
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
32
- }
33
-
34
- .logo {
35
  display: flex;
 
36
  align-items: center;
37
- gap: 0.5rem;
38
- font-size: 1.5rem;
39
- font-weight: 700;
40
- color: var(--primary-color);
41
  }
42
 
43
- .controls-container {
44
- position: fixed;
45
- bottom: 2rem;
46
- left: 50%;
47
- transform: translateX(-50%);
48
- width: 90%;
49
- max-width: 800px;
50
- z-index: 100;
51
- background: var(--surface-color);
52
- backdrop-filter: blur(12px);
53
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
54
- padding: 1.5rem;
55
  }
56
 
57
- .upload-content {
58
  display: flex;
59
  flex-direction: column;
60
  align-items: center;
61
  gap: 1rem;
62
  }
63
 
64
- .upload-content i {
65
- font-size: 3rem;
66
- color: var(--primary-color);
67
- }
68
-
69
- .upload-text h3 {
70
- margin: 0;
71
- font-size: 1.2rem;
72
- font-weight: 600;
73
- }
74
-
75
- .upload-text p {
76
- margin: 0.5rem 0;
77
- color: var(--text-secondary);
78
  }
79
 
80
- .file-types {
81
- font-size: 0.8rem;
82
  color: var(--text-secondary);
 
83
  }
84
 
85
- .error-toast {
 
86
  position: fixed;
87
- top: 90px;
88
- left: 50%;
89
- transform: translateX(-50%) translateY(-150%);
90
- background: rgba(255, 68, 68, 0.95);
91
- color: white;
92
- padding: 0.75rem 2rem;
93
- border-radius: 8px;
94
- box-shadow: 0 4px 15px rgba(255, 68, 68, 0.2);
95
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
96
- z-index: 1001;
97
- text-align: center;
98
- max-width: 90%;
99
- pointer-events: none;
100
- font-size: 0.9rem;
101
- font-weight: 500;
102
- backdrop-filter: blur(8px);
103
- border: 1px solid rgba(255, 255, 255, 0.1);
104
- opacity: 0;
105
  }
106
 
107
- .error-toast.visible {
108
- transform: translateX(-50%) translateY(0);
109
- opacity: 1;
 
 
 
 
 
110
  }
111
 
112
- select {
113
- padding: 8px 12px;
114
- border-radius: 8px;
115
- background: rgba(255, 255, 255, 0.1);
116
- border: 1px solid rgba(255, 255, 255, 0.2);
117
- color: white;
118
- cursor: pointer;
119
- font-size: 14px;
120
- -webkit-appearance: none;
121
- -moz-appearance: none;
122
- appearance: none;
123
- padding-right: 30px;
124
- background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
125
- background-repeat: no-repeat;
126
- background-position: right 8px center;
127
- background-size: 16px;
128
  }
129
 
130
- select:focus {
131
- outline: none;
132
- border-color: #4CAF50;
 
 
133
  }
134
 
135
- select option {
136
- background-color: #1a1a1a;
137
- color: white;
138
- padding: 8px;
 
 
 
 
139
  }
140
 
141
- select::-ms-expand {
142
- display: none;
 
143
  }
144
 
145
- input[type="range"] {
146
- /* ... existing properties ... */
147
- -webkit-appearance: none;
148
- -moz-appearance: none;
149
- appearance: none; /* Add standard property */
150
- /* ... rest of properties ... */
151
  }
152
 
153
- .track-title {
154
- display: flex;
155
- justify-content: space-between;
156
- align-items: center;
157
- font-weight: 500;
 
 
158
  color: var(--text-primary);
159
- margin-bottom: 4px;
160
- }
161
-
162
- .track-duration {
163
- font-size: 0.85rem;
164
- color: var(--text-secondary);
165
- margin-left: 8px;
166
- }
167
-
168
- .track-metadata {
169
- font-size: 0.85rem;
170
- color: var(--text-secondary);
171
- overflow: hidden;
172
- text-overflow: ellipsis;
173
  white-space: nowrap;
 
 
 
 
174
  }
175
 
176
- .playlist-item {
177
- padding: 8px 12px;
178
- cursor: pointer;
179
- border-radius: 4px;
180
- transition: background-color 0.2s;
181
- }
182
-
183
- .playlist-item:hover {
184
- background: rgba(255, 255, 255, 0.1);
185
  }
186
 
187
- .playlist-item.active {
188
- background: rgba(76, 175, 80, 0.3);
 
 
 
 
 
 
 
 
 
189
  }
190
 
191
- .music-controls {
192
- display: flex;
193
- justify-content: center;
194
- align-items: center;
195
- gap: 16px;
196
- margin-top: 12px;
197
  }
198
 
199
- .control-btn {
200
- background: transparent;
201
- border: none;
202
- color: white;
203
- padding: 8px;
204
- min-width: auto;
205
- border-radius: 50%;
206
- cursor: pointer;
207
- transition: all 0.2s ease;
 
208
  }
209
 
210
- .control-btn:hover {
211
- background: rgba(255, 255, 255, 0.1);
212
- transform: scale(1.1);
 
213
  }
214
 
215
- .control-btn:active {
216
- transform: scale(0.95);
 
217
  }
218
 
219
- .play-pause-btn {
220
- background: #4CAF50;
221
- width: 40px;
222
- height: 40px;
223
  display: flex;
 
224
  align-items: center;
225
- justify-content: center;
226
- }
227
-
228
- .play-pause-btn:hover {
229
- background: #45a049;
230
  }
231
 
232
- .previous-btn,
233
- .next-btn {
234
- width: 32px;
235
- height: 32px;
236
- display: flex;
237
- align-items: center;
238
- justify-content: center;
239
  }
240
 
241
- .control-btn i {
242
- font-size: 1.2em;
 
 
 
243
  }
244
 
245
- .play-pause-btn i {
246
- font-size: 1.4em;
 
247
  }
248
 
249
- .control-btn:disabled {
250
- opacity: 0.5;
251
- cursor: not-allowed;
252
- pointer-events: none;
253
  }
254
 
255
- /* Update volume control styles */
256
- .volume-control {
 
 
257
  position: relative;
258
- display: flex;
259
- align-items: center;
260
- gap: 0.5rem;
261
- margin-left: auto;
262
- min-width: 120px;
263
  }
264
 
265
- .volume-btn {
266
- background: transparent;
267
- border: none;
268
- padding: 8px;
269
- min-width: auto;
270
- color: var(--text-primary);
271
- }
272
-
273
- .volume-slider-container {
274
  position: relative;
275
- width: 80px;
276
  height: 4px;
277
- background: rgba(255, 255, 255, 0.2);
278
  border-radius: 2px;
 
 
279
  }
280
 
281
- .volume-progress {
 
282
  position: absolute;
283
  left: 0;
284
  top: 0;
285
  height: 100%;
286
  background: var(--primary-color);
287
  border-radius: 2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  pointer-events: none;
289
- width: 50%;
 
 
 
 
 
 
290
  }
291
 
292
- #volume {
 
293
  position: absolute;
 
 
294
  width: 100%;
295
  height: 100%;
296
  opacity: 0;
297
  cursor: pointer;
 
 
298
  }
299
 
300
- /* Add tooltip styles */
301
- .tooltip {
302
- position: fixed;
303
- background: rgba(0, 0, 0, 0.8);
304
- color: white;
305
- padding: 0.5rem 1rem;
306
- border-radius: 0.25rem;
307
- font-size: 0.8rem;
308
- pointer-events: none;
309
- opacity: 0;
310
- transition: opacity 0.2s;
311
- z-index: 1002;
312
  }
313
 
314
- .tooltip.visible {
315
- opacity: 1;
 
 
 
 
 
 
 
 
316
  }
317
 
 
318
  .playlist-container {
319
  background: var(--surface-color);
320
- border-radius: 1rem;
321
- margin: 0;
322
- max-height: 100%;
323
  overflow: hidden;
324
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 
 
 
 
 
 
 
 
 
325
  }
326
 
327
  .playlist-header {
328
  display: flex;
329
  justify-content: space-between;
330
  align-items: center;
331
- padding: 1rem;
332
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
333
- }
334
-
335
- .playlist-header h3 {
336
- margin: 0;
337
- font-size: 1.2rem;
338
- font-weight: 600;
339
- color: var(--text-primary);
340
  }
341
 
342
- .tracks-list {
343
- max-height: 300px;
344
- overflow-y: auto;
345
- padding: 0.5rem;
346
  }
347
 
348
- .playlist-item {
349
- padding: 0.75rem;
350
- border-radius: 0.5rem;
 
 
 
 
 
 
 
 
351
  cursor: pointer;
352
- transition: background-color 0.2s;
353
- }
354
-
355
- .playlist-item:hover {
356
- background: rgba(255, 255, 255, 0.1);
357
  }
358
 
359
- .playlist-item.active {
360
- background: rgba(76, 175, 80, 0.2);
 
 
361
  }
362
 
363
- .track-info {
364
- display: flex;
365
- align-items: center;
366
- gap: 1rem;
367
- }
368
-
369
- .track-number {
370
- color: var(--text-secondary);
371
- min-width: 2rem;
372
- text-align: right;
373
- }
374
-
375
- .track-content {
376
- flex: 1;
377
- }
378
-
379
- .track-title {
380
- display: flex;
381
- justify-content: space-between;
382
- align-items: center;
383
- margin-bottom: 0.25rem;
384
- }
385
-
386
- .track-duration {
387
- color: var(--text-secondary);
388
- font-size: 0.9rem;
389
- }
390
-
391
- .track-metadata {
392
- color: var(--text-secondary);
393
- font-size: 0.9rem;
394
  }
395
 
396
- .track-controls {
397
- opacity: 0;
398
- transition: opacity 0.2s;
 
 
399
  }
400
 
401
- .playlist-item:hover .track-controls,
402
- .playlist-item.active .track-controls {
403
- opacity: 1;
404
  }
405
 
406
- /* Add custom scrollbar for the tracks list */
407
- .tracks-list::-webkit-scrollbar {
408
- width: 6px;
 
409
  }
410
 
411
- .tracks-list::-webkit-scrollbar-track {
412
- background: rgba(0, 0, 0, 0.1);
413
- border-radius: 3px;
 
 
414
  }
415
 
416
- .tracks-list::-webkit-scrollbar-thumb {
417
- background: rgba(255, 255, 255, 0.2);
418
- border-radius: 3px;
 
 
419
  }
420
 
421
- .tracks-list::-webkit-scrollbar-thumb:hover {
422
- background: rgba(255, 255, 255, 0.3);
 
423
  }
424
 
425
- @media screen and (max-width: 768px) {
426
- .app-header {
427
- padding: 0.75rem 1rem;
428
- }
429
-
430
- .logo span {
431
- display: none; /* Hide logo text on mobile */
432
- }
433
-
434
- .main-content {
435
- position: fixed;
436
- top: auto;
437
- right: 0;
438
- bottom: 140px; /* Position above player controls */
439
- width: 100%;
440
- max-height: 40vh;
441
- padding: 0 1rem;
442
- z-index: 10;
443
- }
444
-
445
- .upload-area {
446
- margin: 0.5rem 0;
447
- padding: 1rem;
448
- }
449
-
450
- .controls-container {
451
- bottom: 0;
452
- width: 100%;
453
- border-radius: 1rem 1rem 0 0;
454
- padding: 1rem;
455
- }
456
-
457
- .music-controls {
458
- gap: 12px;
459
- }
460
-
461
- .control-btn {
462
- padding: 6px;
463
- }
464
-
465
- .volume-control {
466
- display: none; /* Hide volume control on mobile */
467
- }
468
-
469
- .playlist-container {
470
- border-radius: 1rem;
471
- max-height: 100%;
472
- }
473
-
474
- .tracks-list {
475
- max-height: calc(40vh - 60px); /* Adjust based on playlist header height */
476
- }
477
-
478
- .track-metadata {
479
- max-width: 200px; /* Prevent long metadata from breaking layout */
480
- overflow: hidden;
481
- text-overflow: ellipsis;
482
- white-space: nowrap;
483
- }
484
  }
485
 
486
- /* Add specific adjustments for very small screens */
487
- @media screen and (max-width: 380px) {
488
- .music-controls {
489
- gap: 8px;
490
- }
491
-
492
- .control-btn {
493
- width: 28px;
494
- height: 28px;
495
- }
496
-
497
- .play-pause-btn {
498
- width: 36px;
499
- height: 36px;
500
- }
501
-
502
- .track-title {
503
- font-size: 0.9rem;
504
- }
505
-
506
- .track-metadata {
507
- font-size: 0.8rem;
508
- max-width: 150px;
509
- }
510
  }
511
 
512
- /* Add these styles for the now playing info */
513
  .now-playing-info {
514
  display: flex;
515
- justify-content: space-between;
516
  align-items: center;
517
- margin-bottom: 8px;
 
518
  }
519
 
520
- .now-playing-text {
 
 
 
 
521
  display: flex;
522
- flex-direction: column;
523
- overflow: hidden;
524
- flex: 1;
525
- margin-right: 16px;
526
- }
527
-
528
- .now-playing-title {
529
- font-size: 1rem;
530
- font-weight: 500;
531
- color: var(--text-primary);
532
- margin-bottom: 4px;
533
- overflow: hidden;
534
- text-overflow: ellipsis;
535
- white-space: nowrap;
536
- }
537
-
538
- .now-playing-artist {
539
- font-size: 0.9rem;
540
  color: var(--text-secondary);
541
- overflow: hidden;
542
- text-overflow: ellipsis;
543
- white-space: nowrap;
544
  }
545
 
546
- .time-display {
547
- font-size: 0.85rem;
548
- color: var(--text-secondary);
549
- white-space: nowrap;
 
 
550
  }
551
 
552
- /* Update mobile styles */
553
- @media screen and (max-width: 768px) {
554
- .now-playing-info {
555
- flex-direction: column;
556
- align-items: flex-start;
557
- gap: 4px;
558
- }
559
-
560
- .now-playing-text {
561
- width: 100%;
562
- margin-right: 0;
563
- }
564
-
565
- .time-display {
566
- align-self: flex-end;
567
- }
568
  }
569
 
570
- /* Update very small screen styles */
571
- @media screen and (max-width: 380px) {
572
- .now-playing-title {
573
- font-size: 0.9rem;
574
- }
575
-
576
- .now-playing-artist {
577
- font-size: 0.8rem;
578
- }
579
  }
580
 
581
- /* Add these styles at the appropriate location in your CSS file */
582
-
583
- /* Ensure the canvas container takes full viewport */
584
- body {
585
- position: relative;
586
- width: 100vw;
587
- height: 100vh;
588
- margin: 0;
589
- overflow: hidden;
590
  }
591
 
592
- /* Position the THREE.js canvas */
593
- canvas {
594
- position: fixed !important;
595
- top: 0;
596
- left: 0;
597
- z-index: 1;
598
  }
599
 
600
- /* Adjust the header positioning */
601
- .app-header {
602
- /* ... existing styles ... */
603
- z-index: 100;
604
  }
605
 
606
- /* Create a main content area for the playlist */
607
- .main-content {
608
- position: fixed;
609
- top: 80px; /* Adjust based on your header height */
610
- right: 2rem;
611
- width: 400px;
612
- max-height: calc(100vh - 260px); /* Adjust based on your controls height */
613
- z-index: 10;
614
- overflow: visible;
 
 
 
615
  }
616
 
617
- /* Adjust the playlist container */
618
- .playlist-container {
619
- margin: 0;
620
- max-height: 100%;
621
- background: var(--surface-color);
622
- backdrop-filter: blur(12px);
623
  }
624
 
625
- /* Adjust the controls container */
626
- .controls-container {
627
  position: fixed;
628
  bottom: 2rem;
629
- left: 50%;
630
- transform: translateX(-50%);
631
- width: 90%;
632
- max-width: 800px;
633
- z-index: 100;
634
  background: var(--surface-color);
635
  backdrop-filter: blur(12px);
 
 
 
 
 
636
  }
637
 
638
- /* Adjust the upload area to be inside the playlist */
639
- .upload-area {
640
- margin: 1rem;
641
- }
642
-
643
- /* Add these styles for the progress bar */
644
- .progress-bar {
645
- position: relative;
646
- width: 100%;
647
- height: 4px;
648
- background: rgba(255, 255, 255, 0.1);
649
- border-radius: 2px;
650
- margin: 8px 0;
651
- cursor: pointer;
652
- }
653
-
654
- .progress {
655
- position: absolute;
656
- left: 0;
657
- top: 0;
658
- height: 100%;
659
- background: var(--primary-color);
660
- border-radius: 2px;
661
- pointer-events: none;
662
- width: 0;
663
  }
664
 
665
- .seek-slider {
666
- position: absolute;
667
- width: 100%;
668
- height: 100%;
669
- opacity: 0;
670
- cursor: pointer;
671
  }
672
 
673
- /* Add styles for the playlist buttons */
674
- .playlist-btn {
675
- background: transparent;
676
- border: none;
677
- color: var(--text-primary);
678
- padding: 8px;
679
- cursor: pointer;
680
- border-radius: 50%;
681
- transition: all 0.2s ease;
682
  }
683
 
684
- .playlist-btn:hover {
685
- background: rgba(255, 255, 255, 0.1);
686
- transform: scale(1.1);
687
- }
 
 
 
688
 
689
- .playlist-btn.active {
690
- color: var(--primary-color);
691
- }
692
 
693
- /* Add loading spinner styles */
694
- .loading {
695
- display: none;
696
- align-items: center;
697
- justify-content: center;
698
- gap: 1rem;
699
- padding: 1rem;
700
- }
701
 
702
- .loading.visible {
703
- display: flex;
704
- }
705
 
706
- .spinner {
707
- width: 20px;
708
- height: 20px;
709
- border: 2px solid rgba(255, 255, 255, 0.3);
710
- border-top-color: var(--primary-color);
711
- border-radius: 50%;
712
- animation: spin 1s linear infinite;
713
  }
714
 
 
715
  @keyframes spin {
716
  to {
717
  transform: rotate(360deg);
718
  }
719
  }
720
 
721
- /* Add styles for dragover state */
722
- .upload-area.dragover {
723
- border-color: var(--primary-color);
724
- background: rgba(76, 175, 80, 0.1);
725
- }
726
-
727
- /* Add styles for the main container */
728
- .container {
729
- display: flex;
730
- flex-direction: column;
731
- height: 100vh;
732
- padding: 80px 2rem 2rem;
733
- max-width: 1200px;
734
- margin: 0 auto;
735
- box-sizing: border-box;
736
- }
737
-
738
- /* Update body styles */
739
- body {
740
- margin: 0;
741
- font-family: 'Inter', sans-serif;
742
- background: var(--bg-color);
743
- color: var(--text-primary);
744
- overflow-x: hidden;
745
- line-height: 1.5;
746
  }
747
 
748
- /* Add styles for buttons */
749
- button {
750
- font-family: 'Inter', sans-serif;
751
- border: none;
752
- background: none;
753
- cursor: pointer;
754
- padding: 0;
755
- color: inherit;
756
  }
757
 
758
- button:disabled {
759
- opacity: 0.5;
760
- cursor: not-allowed;
761
  }
762
 
763
- /* Hide file input */
764
- input[type="file"] {
765
- display: none;
766
  }
767
 
768
- /* Add transition for theme changes */
769
- * {
770
- transition: background-color 0.3s, color 0.3s;
771
  }
772
 
773
- /* Add styles for mobile responsiveness */
774
- @media (max-width: 768px) {
775
- .container {
776
- padding: 60px 1rem 1rem;
777
- }
778
-
779
- .controls-container {
780
- padding: 1rem;
781
- }
782
-
783
- .music-controls {
784
- flex-wrap: wrap;
785
- gap: 8px;
786
- }
787
-
788
- .volume-control {
789
- width: 100%;
790
- order: 3;
791
- }
792
-
793
- #visualization-type {
794
- width: 100%;
795
- order: 4;
796
- }
797
  }
798
 
799
- /* Add styles for the error message */
800
- #error-message {
801
- color: var(--error-color);
802
- text-align: center;
803
- margin: 8px 0;
804
- font-size: 0.9rem;
805
  }
806
 
807
- /* Add styles for file name display */
808
- #file-name {
809
- margin-top: 8px;
810
- font-size: 0.9rem;
811
- color: var(--text-secondary);
812
- text-align: center;
813
- word-break: break-word;
814
  }
815
 
816
- /* Add styles for header buttons */
817
- .header-controls {
818
  display: flex;
819
  align-items: center;
820
- gap: 8px;
821
- }
822
-
823
- .header-btn {
824
  background: transparent;
825
  border: none;
826
  color: var(--text-primary);
827
- width: 36px;
828
- height: 36px;
829
- border-radius: 50%;
830
- display: flex;
831
- align-items: center;
832
- justify-content: center;
833
  cursor: pointer;
834
- transition: all 0.2s ease;
 
835
  }
836
 
837
- .header-btn:hover {
838
- background: rgba(255, 255, 255, 0.1);
839
  }
840
 
841
- .header-btn.active {
842
- color: var(--primary-color);
843
- background: rgba(76, 175, 80, 0.1);
844
  }
845
 
846
- /* Update main content styles */
 
 
 
 
 
847
  .main-content {
848
  position: fixed;
849
  top: 80px;
850
- right: -420px; /* Hide by default */
851
  width: 400px;
852
- max-height: calc(100vh - 260px);
853
  z-index: 10;
854
- overflow: visible;
855
- transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
856
  }
857
 
858
  .main-content.visible {
859
- right: 2rem;
 
860
  }
861
 
862
- /* Update upload area styles */
863
- .upload-area {
864
- display: none;
865
- margin: 1rem;
866
- opacity: 0;
867
- transform: translateY(-20px);
868
- transition: all 0.3s ease;
869
- }
870
-
871
- .upload-area.visible {
872
- display: block;
873
- opacity: 1;
874
- transform: translateY(0);
875
- }
876
-
877
- /* Update playlist container styles */
878
  .playlist-container {
879
- display: none;
880
- opacity: 0;
881
- transform: translateY(-20px);
882
- transition: all 0.3s ease;
 
 
883
  }
884
 
885
- .playlist-container.visible {
886
- display: block;
887
- opacity: 1;
888
- transform: translateY(0);
 
 
 
 
 
 
 
 
 
 
 
889
  }
890
 
891
- /* Add these styles for the visualization type dropdown */
892
- .viz-type-dropdown {
893
  position: fixed;
894
- top: 60px;
895
- right: -220px;
 
 
 
 
 
896
  background: var(--surface-color);
897
  backdrop-filter: blur(12px);
898
- border-radius: 8px;
899
- padding: 8px;
900
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
901
- transition: right 0.3s ease;
902
- z-index: 99;
903
- width: 200px;
904
  }
905
 
906
- .viz-type-dropdown.visible {
907
- right: 16px;
 
 
 
 
 
 
908
  }
909
 
910
- .viz-type-options {
911
- display: flex;
912
- flex-direction: column;
913
- gap: 8px;
 
914
  }
915
 
916
- .viz-type-options button {
917
- background: transparent;
918
- border: none;
919
- color: var(--text-primary);
920
- padding: 8px 16px;
921
- text-align: left;
922
- border-radius: 4px;
923
- cursor: pointer;
924
- transition: background-color 0.2s;
925
- width: 100%;
926
  }
927
 
928
- .viz-type-options button:hover {
929
- background: rgba(255, 255, 255, 0.1);
 
 
 
 
930
  }
931
 
932
- .viz-type-options button.active {
933
- background: var(--primary-color);
934
- color: white;
 
 
 
 
 
935
  }
936
 
937
- /* Update mobile styles */
938
- @media screen and (max-width: 768px) {
939
- .header-controls {
940
- gap: 4px;
941
- }
 
 
 
 
 
 
 
 
942
 
943
- .header-btn {
944
- width: 32px;
945
- height: 32px;
946
- }
 
 
 
 
 
 
 
 
 
 
 
 
947
 
948
- .viz-type-dropdown {
949
- top: 50px;
950
- width: calc(100% - 32px);
951
- right: -100%;
952
- }
953
 
954
- .viz-type-dropdown.visible {
955
- right: 16px;
956
- }
957
  }
958
 
959
- /* Fix the main content positioning */
960
- .main-content {
961
- position: fixed;
962
- top: 60px; /* Adjust based on header height */
963
- right: -420px;
964
- width: 400px;
965
- max-height: calc(100vh - 180px); /* Adjust for header and player */
966
- z-index: 10;
967
- overflow: visible;
968
- transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
969
- padding: 16px;
970
- box-sizing: border-box;
971
  }
972
 
973
- /* Update the mobile layout */
974
- @media screen and (max-width: 768px) {
975
- .main-content {
976
- width: 100%;
977
- right: -100%;
978
- padding: 8px;
979
- }
980
 
981
- .main-content.visible {
982
- right: 0;
983
- }
 
 
 
 
 
 
984
 
985
- .playlist-container,
986
- .upload-area {
987
- margin: 0;
988
- border-radius: 8px;
989
- }
990
  }
 
1
  :root {
2
  --primary-color: #4CAF50;
3
  --primary-hover: #45a049;
4
+ --primary-active: #3d8b40;
5
+ --accent-color: #FF4081;
6
+ --accent-hover: #f50057;
7
  --bg-color: #0a0a0a;
8
  --surface-color: rgba(15, 23, 42, 0.8);
9
+ --surface-light: rgba(255, 255, 255, 0.1);
10
  --text-primary: #ffffff;
11
  --text-secondary: rgba(255, 255, 255, 0.7);
12
  --error-color: #ff4444;
13
+ --success-color: #00C853;
14
+ --warning-color: #FFA000;
15
+
16
+ /* Animation durations */
17
+ --transition-fast: 0.15s;
18
+ --transition-normal: 0.25s;
19
+ --transition-slow: 0.35s;
20
+
21
+ /* Shadows */
22
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
23
+ --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
24
+ --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.15);
25
+
26
+ /* Border radius */
27
+ --radius-sm: 4px;
28
+ --radius-md: 8px;
29
+ --radius-lg: 12px;
30
+ --radius-xl: 16px;
31
  }
32
 
33
  /* Light theme variables */
34
  [data-theme="light"] {
35
  --bg-color: #f5f5f5;
36
  --surface-color: rgba(255, 255, 255, 0.9);
37
+ --surface-light: rgba(0, 0, 0, 0.1);
38
  --text-primary: #1a1a1a;
39
  --text-secondary: rgba(0, 0, 0, 0.7);
40
  }
41
 
42
+ /* Loading screen */
43
+ .loading-screen {
44
  position: fixed;
45
  top: 0;
46
  left: 0;
47
+ width: 100%;
48
+ height: 100%;
49
+ background: var(--bg-color);
 
 
 
 
 
 
 
 
 
50
  display: flex;
51
+ justify-content: center;
52
  align-items: center;
53
+ z-index: 1000;
54
+ opacity: 1;
55
+ transition: opacity var(--transition-normal);
 
56
  }
57
 
58
+ .loading-screen.hidden {
59
+ opacity: 0;
60
+ pointer-events: none;
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
+ .loader {
64
  display: flex;
65
  flex-direction: column;
66
  align-items: center;
67
  gap: 1rem;
68
  }
69
 
70
+ .loader-circle {
71
+ width: 48px;
72
+ height: 48px;
73
+ border: 3px solid var(--surface-light);
74
+ border-top-color: var(--primary-color);
75
+ border-radius: 50%;
76
+ animation: spin 1s linear infinite;
 
 
 
 
 
 
 
77
  }
78
 
79
+ .loader-text {
 
80
  color: var(--text-secondary);
81
+ font-size: 0.9rem;
82
  }
83
 
84
+ /* Header styles */
85
+ .app-header {
86
  position: fixed;
87
+ top: 0;
88
+ left: 0;
89
+ right: 0;
90
+ display: flex;
91
+ justify-content: space-between;
92
+ align-items: center;
93
+ padding: 1rem 1.5rem;
94
+ background: var(--surface-color);
95
+ backdrop-filter: blur(12px);
96
+ z-index: 100;
97
+ box-shadow: var(--shadow-md);
98
+ border-bottom: 1px solid var(--surface-light);
 
 
 
 
 
 
99
  }
100
 
101
+ .logo {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.75rem;
105
+ font-size: 1.5rem;
106
+ font-weight: 700;
107
+ color: var(--primary-color);
108
+ transition: transform var(--transition-fast);
109
  }
110
 
111
+ .logo:hover {
112
+ transform: scale(1.05);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
+ /* Header controls */
116
+ .header-controls {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 0.5rem;
120
  }
121
 
122
+ .header-btn {
123
+ position: relative;
124
+ width: 40px;
125
+ height: 40px;
126
+ border-radius: 50%;
127
+ background: var(--surface-light);
128
+ color: var(--text-primary);
129
+ transition: all var(--transition-fast);
130
  }
131
 
132
+ .header-btn:hover {
133
+ background: var(--surface-light);
134
+ transform: translateY(-2px);
135
  }
136
 
137
+ .header-btn.active {
138
+ background: var(--primary-color);
139
+ color: white;
 
 
 
140
  }
141
 
142
+ /* Tooltips */
143
+ .tooltip {
144
+ position: absolute;
145
+ bottom: -30px;
146
+ left: 50%;
147
+ transform: translateX(-50%);
148
+ background: var(--surface-color);
149
  color: var(--text-primary);
150
+ padding: 0.5rem 0.75rem;
151
+ border-radius: var(--radius-sm);
152
+ font-size: 0.8rem;
 
 
 
 
 
 
 
 
 
 
 
153
  white-space: nowrap;
154
+ opacity: 0;
155
+ visibility: hidden;
156
+ transition: all var(--transition-fast);
157
+ box-shadow: var(--shadow-md);
158
  }
159
 
160
+ .header-btn:hover .tooltip {
161
+ opacity: 1;
162
+ visibility: visible;
163
+ transform: translateX(-50%) translateY(0);
 
 
 
 
 
164
  }
165
 
166
+ /* Main content area */
167
+ .main-content {
168
+ position: fixed;
169
+ top: 80px;
170
+ right: -420px;
171
+ width: 400px;
172
+ max-height: calc(100vh - 180px);
173
+ z-index: 10;
174
+ transition: all var(--transition-normal);
175
+ padding: 1rem;
176
+ pointer-events: none;
177
  }
178
 
179
+ .main-content.visible {
180
+ right: 1rem;
181
+ pointer-events: all;
 
 
 
182
  }
183
 
184
+ /* Upload area */
185
+ .upload-area {
186
+ background: var(--surface-color);
187
+ border-radius: var(--radius-lg);
188
+ padding: 2rem;
189
+ text-align: center;
190
+ transition: all var(--transition-normal);
191
+ opacity: 0;
192
+ transform: translateY(-20px);
193
+ display: none;
194
  }
195
 
196
+ .upload-area.visible {
197
+ opacity: 1;
198
+ transform: translateY(0);
199
+ display: block;
200
  }
201
 
202
+ .upload-area.dragover {
203
+ background: var(--surface-light);
204
+ border: 2px dashed var(--primary-color);
205
  }
206
 
207
+ .upload-content {
 
 
 
208
  display: flex;
209
+ flex-direction: column;
210
  align-items: center;
211
+ gap: 1.5rem;
 
 
 
 
212
  }
213
 
214
+ .upload-content i {
215
+ font-size: 3rem;
216
+ color: var(--primary-color);
 
 
 
 
217
  }
218
 
219
+ .upload-text h3 {
220
+ margin: 0;
221
+ font-size: 1.2rem;
222
+ font-weight: 600;
223
+ color: var(--text-primary);
224
  }
225
 
226
+ .upload-progress {
227
+ width: 100%;
228
+ display: none;
229
  }
230
 
231
+ .upload-progress.visible {
232
+ display: block;
 
 
233
  }
234
 
235
+ /* Progress bar container */
236
+ .progress-container {
237
+ width: 100%;
238
+ margin: 1rem 0;
239
  position: relative;
 
 
 
 
 
240
  }
241
 
242
+ /* Main progress bar */
243
+ .progress-bar {
 
 
 
 
 
 
 
244
  position: relative;
245
+ width: 100%;
246
  height: 4px;
247
+ background: var(--surface-light);
248
  border-radius: 2px;
249
+ cursor: pointer;
250
+ overflow: visible;
251
  }
252
 
253
+ /* Progress fill */
254
+ .progress {
255
  position: absolute;
256
  left: 0;
257
  top: 0;
258
  height: 100%;
259
  background: var(--primary-color);
260
  border-radius: 2px;
261
+ transition: width 0.1s ease;
262
+ }
263
+
264
+ /* Progress handle */
265
+ .progress-handle {
266
+ position: absolute;
267
+ top: 50%;
268
+ left: 0;
269
+ width: 12px;
270
+ height: 12px;
271
+ background: var(--primary-color);
272
+ border: 2px solid white;
273
+ border-radius: 50%;
274
+ transform: translate(-50%, -50%);
275
+ box-shadow: var(--shadow-sm);
276
  pointer-events: none;
277
+ z-index: 2;
278
+ transition: transform 0.1s ease;
279
+ }
280
+
281
+ /* Progress handle hover effect */
282
+ .progress-bar:hover .progress-handle {
283
+ transform: translate(-50%, -50%) scale(1.2);
284
  }
285
 
286
+ /* Hide the default range input but keep it functional */
287
+ .seek-slider {
288
  position: absolute;
289
+ top: 0;
290
+ left: 0;
291
  width: 100%;
292
  height: 100%;
293
  opacity: 0;
294
  cursor: pointer;
295
+ margin: 0;
296
+ z-index: 1;
297
  }
298
 
299
+ /* Active states for progress bar */
300
+ .progress-bar:active .progress-handle,
301
+ .progress-bar.dragging .progress-handle {
302
+ transform: translate(-50%, -50%) scale(1.3);
303
+ background: var(--primary-hover);
 
 
 
 
 
 
 
304
  }
305
 
306
+ /* Mobile optimization */
307
+ @media (max-width: 768px) {
308
+ .progress-handle {
309
+ width: 16px;
310
+ height: 16px;
311
+ }
312
+
313
+ .progress-bar {
314
+ height: 6px;
315
+ }
316
  }
317
 
318
+ /* Playlist styles */
319
  .playlist-container {
320
  background: var(--surface-color);
321
+ border-radius: var(--radius-lg);
 
 
322
  overflow: hidden;
323
+ opacity: 0;
324
+ transform: translateY(-20px);
325
+ display: none;
326
+ transition: all var(--transition-normal);
327
+ }
328
+
329
+ .playlist-container.visible {
330
+ opacity: 1;
331
+ transform: translateY(0);
332
+ display: block;
333
  }
334
 
335
  .playlist-header {
336
  display: flex;
337
  justify-content: space-between;
338
  align-items: center;
339
+ padding: 1rem 1.5rem;
340
+ border-bottom: 1px solid var(--surface-light);
 
 
 
 
 
 
 
341
  }
342
 
343
+ .playlist-controls {
344
+ display: flex;
345
+ gap: 0.5rem;
 
346
  }
347
 
348
+ .playlist-btn {
349
+ width: 36px;
350
+ height: 36px;
351
+ border-radius: 50%;
352
+ color: var(--text-primary);
353
+ background: var(--surface-light);
354
+ border: 1px solid var(--surface-light);
355
+ transition: all var(--transition-fast);
356
+ display: flex;
357
+ align-items: center;
358
+ justify-content: center;
359
  cursor: pointer;
 
 
 
 
 
360
  }
361
 
362
+ .playlist-btn:hover {
363
+ background: var(--surface-light);
364
+ transform: translateY(-2px);
365
+ color: var(--primary-color);
366
  }
367
 
368
+ .playlist-btn.active {
369
+ background: var(--primary-color);
370
+ border-color: var(--primary-color);
371
+ color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  }
373
 
374
+ /* Light theme adjustments */
375
+ [data-theme="light"] .playlist-btn {
376
+ background: var(--surface-light);
377
+ border-color: var(--surface-light);
378
+ color: var(--text-primary);
379
  }
380
 
381
+ [data-theme="light"] .playlist-btn:hover {
382
+ background: var(--surface-light);
383
+ color: var(--primary-color);
384
  }
385
 
386
+ [data-theme="light"] .playlist-btn.active {
387
+ background: var(--primary-color);
388
+ border-color: var(--primary-color);
389
+ color: white;
390
  }
391
 
392
+ /* Track list */
393
+ .tracks-list {
394
+ max-height: 400px;
395
+ overflow-y: auto;
396
+ padding: 0.5rem;
397
  }
398
 
399
+ .playlist-item {
400
+ padding: 0.75rem 1rem;
401
+ border-radius: var(--radius-md);
402
+ cursor: pointer;
403
+ transition: all var(--transition-fast);
404
  }
405
 
406
+ .playlist-item:hover {
407
+ background: var(--surface-light);
408
+ transform: translateX(4px);
409
  }
410
 
411
+ .playlist-item.active {
412
+ background: var(--primary-color);
413
+ color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
415
 
416
+ /* Player controls */
417
+ .controls-container {
418
+ position: fixed;
419
+ bottom: 2rem;
420
+ left: 50%;
421
+ transform: translateX(-50%);
422
+ width: 90%;
423
+ max-width: 800px;
424
+ background: var(--surface-color);
425
+ backdrop-filter: blur(12px);
426
+ border-radius: var(--radius-lg);
427
+ padding: 1.5rem;
428
+ box-shadow: var(--shadow-lg);
429
+ border: 1px solid var(--surface-light);
430
+ z-index: 100;
 
 
 
 
 
 
 
 
 
431
  }
432
 
 
433
  .now-playing-info {
434
  display: flex;
 
435
  align-items: center;
436
+ gap: 1rem;
437
+ margin-bottom: 1rem;
438
  }
439
 
440
+ .track-artwork {
441
+ width: 48px;
442
+ height: 48px;
443
+ border-radius: var(--radius-md);
444
+ background: var(--surface-light);
445
  display: flex;
446
+ align-items: center;
447
+ justify-content: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  color: var(--text-secondary);
 
 
 
449
  }
450
 
451
+ .music-controls {
452
+ display: flex;
453
+ align-items: center;
454
+ justify-content: center;
455
+ gap: 1rem;
456
+ margin-top: 1rem;
457
  }
458
 
459
+ .control-btn {
460
+ width: 40px;
461
+ height: 40px;
462
+ border-radius: 50%;
463
+ color: var(--text-primary);
464
+ transition: all var(--transition-fast);
 
 
 
 
 
 
 
 
 
 
465
  }
466
 
467
+ .control-btn:hover:not(:disabled) {
468
+ background: var(--surface-light);
469
+ transform: scale(1.1);
 
 
 
 
 
 
470
  }
471
 
472
+ .play-pause-btn {
473
+ width: 48px;
474
+ height: 48px;
475
+ background: var(--primary-color);
476
+ color: white;
 
 
 
 
477
  }
478
 
479
+ .play-pause-btn:hover:not(:disabled) {
480
+ background: var(--primary-hover);
 
 
 
 
481
  }
482
 
483
+ /* Volume control */
484
+ .volume-control {
485
+ position: relative;
486
+ margin-left: auto;
487
  }
488
 
489
+ .volume-slider-container {
490
+ position: absolute;
491
+ bottom: 100%;
492
+ left: 50%;
493
+ transform: translateX(-50%);
494
+ background: var(--surface-color);
495
+ padding: 0.75rem;
496
+ border-radius: var(--radius-md);
497
+ box-shadow: var(--shadow-md);
498
+ opacity: 0;
499
+ visibility: hidden;
500
+ transition: all var(--transition-fast);
501
  }
502
 
503
+ .volume-control:hover .volume-slider-container {
504
+ opacity: 1;
505
+ visibility: visible;
506
+ transform: translateX(-50%) translateY(-8px);
 
 
507
  }
508
 
509
+ /* Keyboard shortcuts */
510
+ .keyboard-shortcuts {
511
  position: fixed;
512
  bottom: 2rem;
513
+ left: 2rem;
 
 
 
 
514
  background: var(--surface-color);
515
  backdrop-filter: blur(12px);
516
+ border-radius: var(--radius-lg);
517
+ padding: 1.5rem;
518
+ box-shadow: var(--shadow-lg);
519
+ border: 1px solid var(--surface-light);
520
+ z-index: 90;
521
  }
522
 
523
+ .shortcut-list {
524
+ display: flex;
525
+ flex-direction: column;
526
+ gap: 0.75rem;
527
+ margin-top: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  }
529
 
530
+ .shortcut-item {
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 1rem;
 
 
534
  }
535
 
536
+ .key {
537
+ background: var(--surface-light);
538
+ padding: 0.25rem 0.5rem;
539
+ border-radius: var(--radius-sm);
540
+ font-size: 0.9rem;
541
+ min-width: 24px;
542
+ text-align: center;
 
 
543
  }
544
 
545
+ /* Mobile responsiveness */
546
+ @media (max-width: 768px) {
547
+ .main-content {
548
+ width: 100%;
549
+ right: -100%;
550
+ padding: 1rem;
551
+ }
552
 
553
+ .main-content.visible {
554
+ right: 0;
555
+ }
556
 
557
+ .controls-container {
558
+ bottom: 0;
559
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
560
+ padding: 1rem;
561
+ }
 
 
 
562
 
563
+ .keyboard-shortcuts {
564
+ display: none;
565
+ }
566
 
567
+ .volume-control {
568
+ display: none;
569
+ }
 
 
 
 
570
  }
571
 
572
+ /* Animations */
573
  @keyframes spin {
574
  to {
575
  transform: rotate(360deg);
576
  }
577
  }
578
 
579
+ @keyframes pulse {
580
+ 0% {
581
+ transform: scale(1);
582
+ }
583
+ 50% {
584
+ transform: scale(1.05);
585
+ }
586
+ 100% {
587
+ transform: scale(1);
588
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  }
590
 
591
+ /* Scrollbar styling */
592
+ ::-webkit-scrollbar {
593
+ width: 8px;
 
 
 
 
 
594
  }
595
 
596
+ ::-webkit-scrollbar-track {
597
+ background: var(--surface-light);
598
+ border-radius: 4px;
599
  }
600
 
601
+ ::-webkit-scrollbar-thumb {
602
+ background: var(--text-secondary);
603
+ border-radius: 4px;
604
  }
605
 
606
+ ::-webkit-scrollbar-thumb:hover {
607
+ background: var(--text-primary);
 
608
  }
609
 
610
+ /* Fix visualization dropdown */
611
+ .viz-type-dropdown {
612
+ position: fixed;
613
+ top: 70px;
614
+ right: 16px;
615
+ background: var(--surface-color);
616
+ backdrop-filter: blur(12px);
617
+ border-radius: var(--radius-lg);
618
+ padding: 1rem;
619
+ box-shadow: var(--shadow-lg);
620
+ z-index: 99;
621
+ width: 200px;
622
+ opacity: 0;
623
+ visibility: hidden;
624
+ transform: translateY(-10px);
625
+ transition: all var(--transition-normal);
626
+ border: 1px solid var(--surface-light);
 
 
 
 
 
 
 
627
  }
628
 
629
+ .viz-type-dropdown.visible {
630
+ opacity: 1;
631
+ visibility: visible;
632
+ transform: translateY(0);
 
 
633
  }
634
 
635
+ .viz-type-options {
636
+ display: flex;
637
+ flex-direction: column;
638
+ gap: 0.5rem;
 
 
 
639
  }
640
 
641
+ .viz-type-options button {
 
642
  display: flex;
643
  align-items: center;
644
+ gap: 0.75rem;
 
 
 
645
  background: transparent;
646
  border: none;
647
  color: var(--text-primary);
648
+ padding: 0.75rem 1rem;
649
+ text-align: left;
650
+ border-radius: var(--radius-md);
 
 
 
651
  cursor: pointer;
652
+ transition: all var(--transition-fast);
653
+ width: 100%;
654
  }
655
 
656
+ .viz-type-options button:hover {
657
+ background: var(--surface-light);
658
  }
659
 
660
+ .viz-type-options button.active {
661
+ background: var(--primary-color);
662
+ color: white;
663
  }
664
 
665
+ .viz-type-options button i {
666
+ width: 20px;
667
+ text-align: center;
668
+ }
669
+
670
+ /* Fix main content area */
671
  .main-content {
672
  position: fixed;
673
  top: 80px;
674
+ right: -420px;
675
  width: 400px;
676
+ max-height: calc(100vh - 180px);
677
  z-index: 10;
678
+ transition: all var(--transition-normal);
679
+ padding: 1rem;
680
+ pointer-events: none;
681
  }
682
 
683
  .main-content.visible {
684
+ right: 1rem;
685
+ pointer-events: all;
686
  }
687
 
688
+ /* Fix upload area and playlist container */
689
+ .upload-area,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  .playlist-container {
691
+ background: var(--surface-color);
692
+ border-radius: var(--radius-lg);
693
+ border: 1px solid var(--surface-light);
694
+ backdrop-filter: blur(12px);
695
+ margin-bottom: 1rem;
696
+ overflow: hidden;
697
  }
698
 
699
+ /* Fix controls container */
700
+ .controls-container {
701
+ position: fixed;
702
+ bottom: 2rem;
703
+ left: 50%;
704
+ transform: translateX(-50%);
705
+ width: 90%;
706
+ max-width: 800px;
707
+ background: var(--surface-color);
708
+ backdrop-filter: blur(12px);
709
+ border-radius: var(--radius-lg);
710
+ padding: 1.5rem;
711
+ box-shadow: var(--shadow-lg);
712
+ border: 1px solid var(--surface-light);
713
+ z-index: 100;
714
  }
715
 
716
+ /* Fix header */
717
+ .app-header {
718
  position: fixed;
719
+ top: 0;
720
+ left: 0;
721
+ right: 0;
722
+ display: flex;
723
+ justify-content: space-between;
724
+ align-items: center;
725
+ padding: 1rem 1.5rem;
726
  background: var(--surface-color);
727
  backdrop-filter: blur(12px);
728
+ z-index: 100;
729
+ box-shadow: var(--shadow-md);
730
+ border-bottom: 1px solid var(--surface-light);
 
 
 
731
  }
732
 
733
+ /* Fix buttons and controls */
734
+ .header-btn,
735
+ .control-btn,
736
+ .playlist-btn {
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: center;
740
+ border: 1px solid var(--surface-light);
741
  }
742
 
743
+ /* Fix progress bars */
744
+ .progress-bar {
745
+ background: var(--surface-light);
746
+ border: 1px solid var(--surface-light);
747
+ overflow: hidden;
748
  }
749
 
750
+ .progress-handle {
751
+ width: 16px;
752
+ height: 16px;
753
+ background: var(--primary-color);
754
+ border: 2px solid white;
755
+ box-shadow: var(--shadow-md);
 
 
 
 
756
  }
757
 
758
+ /* Fix volume control */
759
+ .volume-slider-container {
760
+ width: 120px;
761
+ padding: 1rem;
762
+ background: var(--surface-color);
763
+ border: 1px solid var(--surface-light);
764
  }
765
 
766
+ /* Fix tooltips */
767
+ .tooltip {
768
+ background: var(--surface-color);
769
+ border: 1px solid var(--surface-light);
770
+ padding: 0.5rem 0.75rem;
771
+ font-size: 0.85rem;
772
+ white-space: nowrap;
773
+ pointer-events: none;
774
  }
775
 
776
+ /* Fix keyboard shortcuts panel */
777
+ .keyboard-shortcuts {
778
+ position: fixed;
779
+ bottom: 2rem;
780
+ left: 2rem;
781
+ background: var(--surface-color);
782
+ backdrop-filter: blur(12px);
783
+ border-radius: var(--radius-lg);
784
+ padding: 1.5rem;
785
+ box-shadow: var(--shadow-lg);
786
+ border: 1px solid var(--surface-light);
787
+ z-index: 90;
788
+ }
789
 
790
+ /* Fix toast notifications */
791
+ .error-toast {
792
+ position: fixed;
793
+ top: 90px;
794
+ left: 50%;
795
+ transform: translateX(-50%) translateY(-100%);
796
+ background: var(--surface-color);
797
+ backdrop-filter: blur(12px);
798
+ border-radius: var(--radius-md);
799
+ padding: 1rem 2rem;
800
+ box-shadow: var(--shadow-lg);
801
+ border: 1px solid var(--surface-light);
802
+ z-index: 1000;
803
+ opacity: 0;
804
+ transition: all var(--transition-normal);
805
+ }
806
 
807
+ .error-toast.visible {
808
+ transform: translateX(-50%) translateY(0);
809
+ opacity: 1;
810
+ }
 
811
 
812
+ .error-toast.error {
813
+ border-color: var(--error-color);
814
+ color: var(--error-color);
815
  }
816
 
817
+ .error-toast.success {
818
+ border-color: var(--success-color);
819
+ color: var(--success-color);
 
 
 
 
 
 
 
 
 
820
  }
821
 
822
+ /* Fix light theme colors */
823
+ [data-theme="light"] {
824
+ --surface-light: rgba(0, 0, 0, 0.1);
825
+ --surface-color: rgba(255, 255, 255, 0.9);
826
+ }
 
 
827
 
828
+ /* Prevent content shifting */
829
+ body {
830
+ overflow: hidden;
831
+ margin: 0;
832
+ padding: 0;
833
+ font-family: 'Inter', sans-serif;
834
+ background: var(--bg-color);
835
+ color: var(--text-primary);
836
+ }
837
 
838
+ canvas {
839
+ position: fixed !important;
840
+ top: 0;
841
+ left: 0;
842
+ z-index: 1;
843
  }
static/img/favicon.svg ADDED
static/js/main.js CHANGED
@@ -1,16 +1,28 @@
1
  // Initialize Three.js scene and renderer
2
  const scene = new THREE.Scene();
3
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
4
- const renderer = new THREE.WebGLRenderer({ antialias: true });
 
 
 
 
 
5
  renderer.setSize(window.innerWidth, window.innerHeight);
 
 
 
 
6
 
7
  // Set initial background color based on theme
8
  const savedTheme = localStorage.getItem('theme') || 'dark';
9
  document.documentElement.setAttribute('data-theme', savedTheme);
10
- renderer.setClearColor(savedTheme === 'light' ? 0xf5f5f5 : 0x000000);
11
  document.body.appendChild(renderer.domElement);
12
 
13
- // Initialize controls
 
 
 
14
  const controls = new THREE.OrbitControls(camera, renderer.domElement);
15
  setupOrbitControls();
16
 
@@ -64,7 +76,7 @@ if (themeBtn) {
64
  '<i class="fas fa-moon"></i>' :
65
  '<i class="fas fa-sun"></i>';
66
 
67
- renderer.setClearColor(newTheme === 'light' ? 0xf5f5f5 : 0x000000);
68
  });
69
  }
70
 
@@ -81,45 +93,149 @@ function initAudio() {
81
 
82
  // Visualization creation functions
83
  function createBarsVisualization() {
84
- const geometry = new THREE.BoxGeometry(0.1, 1, 0.1);
85
- const numBars = 128;
86
- const materials = [
87
- new THREE.MeshPhongMaterial({ color: 0x4CAF50 }),
88
- new THREE.MeshPhongMaterial({ color: 0x2196F3 }),
89
- new THREE.MeshPhongMaterial({ color: 0xFF4081 })
90
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  for (let i = 0; i < numBars; i++) {
93
- const material = materials[i % materials.length];
94
- const bar = new THREE.Mesh(geometry, material);
95
- const angle = (i / numBars) * Math.PI * 2;
96
- const radius = 2;
97
  bar.position.x = Math.cos(angle) * radius;
98
  bar.position.z = Math.sin(angle) * radius;
 
 
 
 
 
99
  bar.userData.initialY = bar.position.y;
100
- bar.userData.phase = i * 0.1;
 
 
 
101
  scene.add(bar);
102
  visualizers.bars.push(bar);
103
  }
104
-
105
- // Enhanced lighting
106
- const light = new THREE.PointLight(0xffffff, 1.5, 100);
107
- light.position.set(0, 5, 0);
108
- scene.add(light);
109
 
 
110
  const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
111
  scene.add(ambientLight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
  function createSphereVisualization() {
115
  const geometry = new THREE.IcosahedronGeometry(1, 4);
116
- const material = new THREE.MeshPhongMaterial({
117
- color: 0x4CAF50,
118
- wireframe: true,
119
- emissive: 0x2196F3,
120
- emissiveIntensity: 0.5,
121
- shininess: 50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  });
 
123
  visualizers.sphere = new THREE.Mesh(geometry, material);
124
 
125
  // Store original vertex positions
@@ -128,60 +244,118 @@ function createSphereVisualization() {
128
 
129
  scene.add(visualizers.sphere);
130
 
131
- // Enhanced lighting
132
- const light1 = new THREE.PointLight(0xFF4081, 1, 100);
133
- light1.position.set(5, 5, 5);
134
- scene.add(light1);
135
 
136
- const light2 = new THREE.PointLight(0x2196F3, 1, 100);
137
- light2.position.set(-5, -5, -5);
138
- scene.add(light2);
 
 
 
 
 
 
 
 
 
 
139
  }
140
 
141
  function createParticlesVisualization() {
142
- const particleCount = 2000;
143
  const geometry = new THREE.BufferGeometry();
144
  const positions = new Float32Array(particleCount * 3);
 
145
  const colors = new Float32Array(particleCount * 3);
146
- const sizes = new Float32Array(particleCount);
147
 
148
  const color1 = new THREE.Color(0x4CAF50);
149
  const color2 = new THREE.Color(0x2196F3);
150
  const color3 = new THREE.Color(0xFF4081);
151
 
152
- for (let i = 0; i < particleCount * 3; i += 3) {
153
- // Create spiral pattern with larger radius
154
- const angle = (i / particleCount) * Math.PI * 8;
155
- const radius = (Math.random() * 4) + 2;
 
 
156
 
157
- positions[i] = Math.cos(angle) * radius;
158
- positions[i + 1] = (Math.random() - 0.5) * 6;
159
- positions[i + 2] = Math.sin(angle) * radius;
 
160
 
161
- // Interpolate between colors based on position
162
- const colorMix = Math.abs(Math.sin(angle));
 
 
 
163
  const finalColor = new THREE.Color().lerpColors(
164
  color1,
165
  colorMix > 0.5 ? color2 : color3,
166
  colorMix
167
  );
168
 
169
- colors[i] = finalColor.r;
170
- colors[i + 1] = finalColor.g;
171
- colors[i + 2] = finalColor.b;
172
-
173
- sizes[i / 3] = Math.random() * 0.1 + 0.05;
174
  }
175
 
176
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
 
177
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
178
- geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
179
 
180
- const material = new THREE.PointsMaterial({
181
- size: 0.15,
182
- vertexColors: true,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  transparent: true,
184
- opacity: 0.8,
185
  blending: THREE.AdditiveBlending
186
  });
187
 
@@ -192,21 +366,28 @@ function createParticlesVisualization() {
192
  // Visualization update functions
193
  function updateBarsVisualization(dataArray) {
194
  const time = Date.now() * 0.001;
195
- const multiplier = 0.015;
196
 
197
  visualizers.bars.forEach((bar, i) => {
198
- const value = dataArray[i] * multiplier;
199
- // Add wave effect
200
- const wave = Math.sin(time * 2 + bar.userData.phase) * 0.1;
201
- bar.scale.y = value + 0.1;
202
- bar.position.y = (value / 2) + wave;
203
-
204
- // Rotate bars based on audio
205
- bar.rotation.y = time * 0.1 + (value * 0.1);
206
-
207
- // Update bar color based on frequency
208
- const hue = (i / visualizers.bars.length) + (time * 0.1);
209
- bar.material.color.setHSL(hue, 0.7, 0.5);
 
 
 
 
 
 
 
210
  });
211
  }
212
 
@@ -217,65 +398,60 @@ function updateSphereVisualization(dataArray) {
217
  const originalPositions = visualizers.sphere.userData.originalPositions;
218
  const time = Date.now() * 0.001;
219
 
 
 
 
 
220
  for (let i = 0; i < positions.length; i += 3) {
221
  const i3 = i / 3;
222
  const value = dataArray[i3 % dataArray.length] / 255;
223
 
224
- // Create complex deformation
225
  const deform = value * 0.5;
226
- positions[i] = originalPositions[i] * (1 + deform * Math.sin(time + i3));
227
- positions[i + 1] = originalPositions[i + 1] * (1 + deform * Math.cos(time + i3));
228
- positions[i + 2] = originalPositions[i + 2] * (1 + deform * Math.sin(time * 0.5 + i3));
 
 
229
  }
230
 
231
  visualizers.sphere.geometry.attributes.position.needsUpdate = true;
232
- visualizers.sphere.rotation.y += 0.01;
233
- visualizers.sphere.rotation.x += 0.005;
234
 
235
- // Update material colors
236
- const hue = (Math.sin(time * 0.1) + 1) * 0.5;
237
- visualizers.sphere.material.color.setHSL(hue, 0.7, 0.5);
238
- visualizers.sphere.material.emissive.setHSL((hue + 0.5) % 1, 0.7, 0.3);
239
  }
240
 
241
  function updateParticlesVisualization(dataArray) {
242
  if (!visualizers.particles) return;
243
 
 
244
  const positions = visualizers.particles.geometry.attributes.position.array;
 
245
  const colors = visualizers.particles.geometry.attributes.color.array;
246
- const sizes = visualizers.particles.geometry.attributes.size.array;
247
- const time = Date.now() * 0.001;
 
248
 
249
  for (let i = 0; i < positions.length; i += 3) {
250
  const i3 = i / 3;
251
  const value = dataArray[i3 % dataArray.length] / 255;
252
 
253
- // Keep consistent radius while rotating
254
- const angle = (i3 / positions.length) * Math.PI * 8 + time;
255
- const baseRadius = 8; // Fixed base radius
256
 
257
- positions[i] = Math.cos(angle) * baseRadius;
258
- positions[i + 1] += (Math.random() - 0.5) * 0.15;
259
- positions[i + 2] = Math.sin(angle) * baseRadius;
 
 
260
 
261
- // Update colors based on audio and position
262
- const hue = (angle / (Math.PI * 2)) + time * 0.1;
263
- const color = new THREE.Color().setHSL(hue, 0.7, 0.5 + value * 0.5);
264
  colors[i] = color.r;
265
  colors[i + 1] = color.g;
266
  colors[i + 2] = color.b;
267
-
268
- // Update particle sizes with audio reactivity
269
- sizes[i3] = (value * 0.3 + 0.08) * (1 + Math.sin(time + i3) * 0.2);
270
  }
271
 
272
- visualizers.particles.geometry.attributes.position.needsUpdate = true;
273
  visualizers.particles.geometry.attributes.color.needsUpdate = true;
274
- visualizers.particles.geometry.attributes.size.needsUpdate = true;
275
-
276
- // Rotate the entire particle system
277
- visualizers.particles.rotation.y += 0.001;
278
- visualizers.particles.rotation.x += 0.0005;
279
  }
280
 
281
  // Animation loop
@@ -304,10 +480,34 @@ window.addEventListener('resize', () => {
304
 
305
  // Initialize the application
306
  document.addEventListener('DOMContentLoaded', () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  setupUploadHandlers();
308
  setupPlayerControls();
309
  setupToggleHandlers();
 
 
310
  createVisualization();
 
 
 
311
  });
312
 
313
  // Export necessary functions and variables
@@ -325,12 +525,20 @@ function setupOrbitControls() {
325
  // Camera position is now set in createVisualization
326
  }
327
 
328
- // Handle file uploads
329
  function setupUploadHandlers() {
330
  const uploadArea = document.getElementById('upload-area');
331
  const fileInput = document.getElementById('audio-upload');
 
 
 
332
  const errorToast = document.querySelector('.error-toast');
333
 
 
 
 
 
 
334
  uploadArea.addEventListener('dragover', (e) => {
335
  e.preventDefault();
336
  uploadArea.classList.add('dragover');
@@ -354,76 +562,256 @@ function setupUploadHandlers() {
354
  handleFiles(e.target.files);
355
  });
356
 
357
- // Setup shuffle and repeat buttons
358
- const shuffleBtn = document.querySelector('.shuffle-btn');
359
- const repeatBtn = document.querySelector('.repeat-btn');
360
-
361
- shuffleBtn?.addEventListener('click', () => {
362
- isShuffleActive = !isShuffleActive;
363
- shuffleBtn.classList.toggle('active');
364
- });
365
-
366
- repeatBtn?.addEventListener('click', () => {
367
- isRepeatActive = !isRepeatActive;
368
- repeatBtn.classList.toggle('active');
369
- });
370
- }
371
-
372
- // Handle the uploaded files
373
- async function handleFiles(files) {
374
- if (!audioContext) initAudio();
375
-
376
- const formData = new FormData();
377
- Array.from(files).forEach(file => {
378
- formData.append('files[]', file);
379
- });
380
 
381
- try {
382
- const response = await fetch('/upload', {
383
- method: 'POST',
384
- body: formData
 
 
 
 
 
 
 
 
 
385
  });
386
 
387
- const data = await response.json();
 
 
 
 
 
388
 
389
- if (data.success) {
390
- // Debug log to check the response
391
- console.log('Upload response:', data);
392
-
393
- playlist = data.files.filter(file => file.success).map(file => ({
394
- name: file.filename,
395
- url: file.filepath,
396
- metadata: file.metadata
397
- }));
398
-
399
- // Debug log to check the playlist
400
- console.log('Created playlist:', playlist);
401
 
402
- createPlaylist();
403
- if (playlist.length > 0 && playlist[0].url) {
404
- playTrack(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  } else {
406
- showError('No valid tracks were uploaded');
 
 
 
 
 
 
 
 
 
 
407
  }
408
- } else {
409
- showError(data.error || 'Upload failed');
410
  }
411
- } catch (error) {
412
- console.error('Upload error:', error);
413
- showError('Error uploading files');
414
  }
415
  }
416
 
417
- // Show error message
418
  function showError(message) {
419
- const errorToast = document.querySelector('.error-toast');
420
- errorToast.textContent = message;
421
- errorToast.classList.add('visible');
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  setTimeout(() => {
423
- errorToast.classList.remove('visible');
424
  }, 3000);
425
  }
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  // Setup player controls
428
  function setupPlayerControls() {
429
  const playPauseBtn = document.getElementById('play-pause');
@@ -512,27 +900,37 @@ function updateVolume(e) {
512
 
513
  // Seek to position
514
  function seekTo(e) {
515
- if (audioElement && audioElement.duration) {
516
- const seekSlider = document.querySelector('.seek-slider');
517
- const progress = document.querySelector('.progress');
518
- const time = (seekSlider.value / 100) * audioElement.duration;
519
-
520
- audioElement.currentTime = time;
 
 
 
 
 
 
521
  progress.style.width = `${seekSlider.value}%`;
522
-
523
- // Force time display update
524
- updateTimeDisplay();
525
  }
 
 
 
 
 
 
526
  }
527
 
528
  // Update time display
529
  function updateTimeDisplay() {
530
  if (!audioElement) return;
531
 
532
- const currentTime = document.querySelector('.current-time');
533
- const totalTime = document.querySelector('.total-time');
534
- const seekSlider = document.querySelector('.seek-slider');
535
  const progress = document.querySelector('.progress');
 
 
536
 
537
  // Only update if duration is available and not NaN
538
  if (audioElement.duration && !isNaN(audioElement.duration)) {
@@ -540,26 +938,37 @@ function updateTimeDisplay() {
540
  const total = formatTime(audioElement.duration);
541
  const progressPercent = (audioElement.currentTime / audioElement.duration) * 100;
542
 
543
- // Update current time display
544
- if (currentTime) currentTime.textContent = current;
545
-
546
- // Update total time display
547
- if (totalTime) totalTime.textContent = total;
548
 
549
- // Update slider and progress bar
550
- if (seekSlider && !seekSlider.matches(':active')) { // Only update if user isn't dragging
551
- seekSlider.value = progressPercent;
552
  progress.style.width = `${progressPercent}%`;
553
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  }
555
  }
556
 
557
- // Format time in MM:SS
558
  function formatTime(seconds) {
559
  if (!seconds || isNaN(seconds)) return '0:00';
 
560
  const mins = Math.floor(seconds / 60);
561
- const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
562
- return `${mins}:${secs}`;
563
  }
564
 
565
  // Handle track end
@@ -672,64 +1081,72 @@ function updateNowPlayingInfo() {
672
  }
673
  }
674
 
675
- // Play selected track
676
- function playTrack(index) {
677
- if (!playlist || index < 0 || index >= playlist.length) return;
678
 
679
- currentTrackIndex = index;
680
- const track = playlist[index];
681
-
682
- if (!audioElement) {
683
- initAudio();
684
- }
685
-
686
- if (!track || !track.url) {
687
- console.error('Invalid track or track URL');
688
- showError('Invalid track data');
689
- return;
690
- }
691
-
692
- // Update playlist UI
693
- document.querySelectorAll('.playlist-item').forEach((item, i) => {
694
- item.classList.toggle('active', i === index);
695
- });
696
-
697
- // Enable controls
698
- const playPauseBtn = document.getElementById('play-pause');
699
- const prevBtn = document.querySelector('.previous-btn');
700
- const nextBtn = document.querySelector('.next-btn');
701
-
702
- if (playPauseBtn) playPauseBtn.disabled = false;
703
- if (prevBtn) prevBtn.disabled = false;
704
- if (nextBtn) nextBtn.disabled = false;
705
-
706
- // Remove any existing event listeners before adding new ones
707
- audioElement.removeEventListener('loadedmetadata', updateTimeDisplay);
708
- audioElement.removeEventListener('timeupdate', updateTimeDisplay);
709
-
710
- // Add event listeners
711
- audioElement.addEventListener('loadedmetadata', updateTimeDisplay);
712
- audioElement.addEventListener('timeupdate', updateTimeDisplay);
713
-
714
- // Update audio source and play
715
- audioElement.src = track.url;
716
-
717
- // Force an initial time display update
718
- setTimeout(updateTimeDisplay, 100);
719
-
720
- audioElement.play()
721
- .then(() => {
722
  isPlaying = true;
723
- if (playPauseBtn) {
724
- playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
725
- }
726
  updateNowPlayingInfo();
727
- updateTimeDisplay(); // Update time display immediately after play starts
728
- })
729
- .catch(error => {
730
- console.error('Error playing track:', error);
731
- showError('Error playing track: ' + error.message);
732
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  }
734
 
735
  // Add these functions after the existing initialization code
@@ -741,60 +1158,70 @@ function setupToggleHandlers() {
741
  const playlistToggleBtn = document.querySelector('.playlist-toggle-btn');
742
  const vizTypeBtn = document.querySelector('.viz-type-btn');
743
  const vizTypeDropdown = document.querySelector('.viz-type-dropdown');
744
- const vizTypeOptions = document.querySelectorAll('.viz-type-options button');
 
 
 
 
745
 
746
  // Show playlist by default
747
- mainContent?.classList.add('visible');
748
- playlistContainer?.classList.add('visible');
749
- playlistToggleBtn?.classList.add('active');
750
 
 
751
  uploadToggleBtn?.addEventListener('click', () => {
752
- const isVisible = uploadArea?.classList.contains('visible');
753
 
754
  // Hide playlist if it's visible
755
- playlistContainer?.classList.remove('visible');
756
  playlistToggleBtn?.classList.remove('active');
757
 
758
  // Toggle upload area
759
- uploadArea?.classList.toggle('visible');
760
- uploadToggleBtn?.classList.toggle('active');
761
 
762
  // Show/hide main content
763
- mainContent?.classList.toggle('visible', !isVisible || playlistContainer?.classList.contains('visible'));
764
  });
765
 
 
766
  playlistToggleBtn?.addEventListener('click', () => {
767
- const isVisible = playlistContainer?.classList.contains('visible');
768
 
769
  // Hide upload area if it's visible
770
- uploadArea?.classList.remove('visible');
771
  uploadToggleBtn?.classList.remove('active');
772
 
773
  // Toggle playlist
774
- playlistContainer?.classList.toggle('visible');
775
- playlistToggleBtn?.classList.toggle('active');
776
 
777
  // Show/hide main content
778
- mainContent?.classList.toggle('visible', !isVisible || uploadArea?.classList.contains('visible'));
779
  });
780
 
781
- // Handle visualization type button click
 
 
782
  vizTypeBtn?.addEventListener('click', (e) => {
783
- e.stopPropagation(); // Prevent document click from immediately closing
784
- vizTypeDropdown?.classList.toggle('visible');
785
- vizTypeBtn?.classList.toggle('active');
786
 
787
- // Close other dropdowns
788
- uploadArea?.classList.remove('visible');
789
- uploadToggleBtn?.classList.remove('active');
790
- playlistContainer?.classList.remove('visible');
791
- playlistToggleBtn?.classList.remove('active');
 
 
792
  });
793
 
794
  // Handle visualization type selection
 
795
  vizTypeOptions?.forEach(button => {
796
  button.addEventListener('click', (e) => {
797
- e.stopPropagation(); // Prevent document click from closing dropdown
798
  const type = button.dataset.type;
799
 
800
  // Update active state
@@ -806,18 +1233,74 @@ function setupToggleHandlers() {
806
  createVisualization();
807
 
808
  // Close dropdown
 
809
  vizTypeDropdown?.classList.remove('visible');
810
  vizTypeBtn?.classList.remove('active');
811
  });
812
  });
813
 
814
- // Close dropdown when clicking outside
815
  document.addEventListener('click', (e) => {
816
- if (vizTypeBtn && vizTypeDropdown &&
817
- !vizTypeBtn.contains(e.target) &&
818
- !vizTypeDropdown.contains(e.target)) {
 
 
819
  vizTypeDropdown.classList.remove('visible');
820
- vizTypeBtn.classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  }
822
  });
 
 
 
 
 
 
823
  }
 
1
  // Initialize Three.js scene and renderer
2
  const scene = new THREE.Scene();
3
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
4
+ const renderer = new THREE.WebGLRenderer({
5
+ antialias: true,
6
+ alpha: true,
7
+ powerPreference: "high-performance"
8
+ });
9
+ renderer.setPixelRatio(window.devicePixelRatio);
10
  renderer.setSize(window.innerWidth, window.innerHeight);
11
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
12
+ renderer.toneMappingExposure = 1;
13
+ renderer.shadowMap.enabled = true;
14
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
15
 
16
  // Set initial background color based on theme
17
  const savedTheme = localStorage.getItem('theme') || 'dark';
18
  document.documentElement.setAttribute('data-theme', savedTheme);
19
+ renderer.setClearColor(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9);
20
  document.body.appendChild(renderer.domElement);
21
 
22
+ // Add fog to the scene for depth
23
+ scene.fog = new THREE.FogExp2(savedTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.02);
24
+
25
+ // Initialize controls with enhanced settings
26
  const controls = new THREE.OrbitControls(camera, renderer.domElement);
27
  setupOrbitControls();
28
 
 
76
  '<i class="fas fa-moon"></i>' :
77
  '<i class="fas fa-sun"></i>';
78
 
79
+ renderer.setClearColor(newTheme === 'light' ? 0xf5f5f5 : 0x000000, 0.9);
80
  });
81
  }
82
 
 
93
 
94
  // Visualization creation functions
95
  function createBarsVisualization() {
96
+ const numBars = 180;
97
+ const geometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 8);
98
+ geometry.translate(0, 0.5, 0); // Move pivot to bottom
99
+
100
+ // Create custom shader material for bars
101
+ const material = new THREE.ShaderMaterial({
102
+ uniforms: {
103
+ time: { value: 0 },
104
+ color1: { value: new THREE.Color(0x4CAF50) },
105
+ color2: { value: new THREE.Color(0x2196F3) },
106
+ color3: { value: new THREE.Color(0xFF4081) }
107
+ },
108
+ vertexShader: `
109
+ varying vec3 vPosition;
110
+ varying vec3 vNormal;
111
+
112
+ void main() {
113
+ vPosition = position;
114
+ vNormal = normalize(normalMatrix * normal);
115
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
116
+ }
117
+ `,
118
+ fragmentShader: `
119
+ uniform float time;
120
+ uniform vec3 color1;
121
+ uniform vec3 color2;
122
+ uniform vec3 color3;
123
+ varying vec3 vPosition;
124
+ varying vec3 vNormal;
125
+
126
+ void main() {
127
+ float heightFactor = vPosition.y;
128
+
129
+ // Create dynamic color gradient
130
+ vec3 baseColor = mix(
131
+ mix(color1, color2, heightFactor),
132
+ color3,
133
+ sin(time * 0.5) * 0.5 + 0.5
134
+ );
135
+
136
+ // Add fresnel effect for edge glow
137
+ float fresnel = pow(1.0 - abs(dot(vNormal, vec3(0, 0, 1.0))), 2.0);
138
+ vec3 finalColor = mix(baseColor, vec3(1.0), fresnel * 0.5);
139
+
140
+ gl_FragColor = vec4(finalColor, 0.9);
141
+ }
142
+ `,
143
+ transparent: true,
144
+ side: THREE.DoubleSide
145
+ });
146
+
147
+ const radius = 4;
148
+ const angleStep = (Math.PI * 2) / numBars;
149
 
150
  for (let i = 0; i < numBars; i++) {
151
+ const angle = i * angleStep;
152
+ const bar = new THREE.Mesh(geometry, material.clone());
153
+
154
+ // Position in a circle
155
  bar.position.x = Math.cos(angle) * radius;
156
  bar.position.z = Math.sin(angle) * radius;
157
+
158
+ // Rotate to face center
159
+ bar.rotation.y = -angle;
160
+
161
+ // Store initial properties
162
  bar.userData.initialY = bar.position.y;
163
+ bar.userData.initialScale = 1;
164
+ bar.userData.angle = angle;
165
+ bar.userData.index = i;
166
+
167
  scene.add(bar);
168
  visualizers.bars.push(bar);
169
  }
 
 
 
 
 
170
 
171
+ // Add ambient light
172
  const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
173
  scene.add(ambientLight);
174
+
175
+ // Add multiple point lights with different colors
176
+ const colors = [0xFF4081, 0x2196F3, 0x4CAF50];
177
+ colors.forEach((color, i) => {
178
+ const light = new THREE.PointLight(color, 1, 20);
179
+ const angle = (i / colors.length) * Math.PI * 2;
180
+ const lightRadius = radius * 1.5;
181
+ light.position.set(
182
+ Math.cos(angle) * lightRadius,
183
+ 5,
184
+ Math.sin(angle) * lightRadius
185
+ );
186
+ scene.add(light);
187
+ });
188
  }
189
 
190
  function createSphereVisualization() {
191
  const geometry = new THREE.IcosahedronGeometry(1, 4);
192
+
193
+ // Create a more complex material with gradient and glow effects
194
+ const material = new THREE.ShaderMaterial({
195
+ uniforms: {
196
+ time: { value: 0 },
197
+ color1: { value: new THREE.Color(0x4CAF50) },
198
+ color2: { value: new THREE.Color(0x2196F3) },
199
+ color3: { value: new THREE.Color(0xFF4081) }
200
+ },
201
+ vertexShader: `
202
+ varying vec3 vNormal;
203
+ varying vec3 vPosition;
204
+ void main() {
205
+ vNormal = normalize(normalMatrix * normal);
206
+ vPosition = position;
207
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
208
+ }
209
+ `,
210
+ fragmentShader: `
211
+ uniform float time;
212
+ uniform vec3 color1;
213
+ uniform vec3 color2;
214
+ uniform vec3 color3;
215
+ varying vec3 vNormal;
216
+ varying vec3 vPosition;
217
+
218
+ void main() {
219
+ float noise = sin(vPosition.x * 10.0 + time) *
220
+ cos(vPosition.y * 10.0 + time) *
221
+ sin(vPosition.z * 10.0 + time);
222
+
223
+ vec3 color = mix(
224
+ mix(color1, color2, noise * 0.5 + 0.5),
225
+ color3,
226
+ sin(time * 0.5) * 0.5 + 0.5
227
+ );
228
+
229
+ float fresnel = pow(1.0 + dot(vNormal, vec3(0, 0, 1.0)), 3.0);
230
+ color = mix(color, vec3(1.0), fresnel * 0.7);
231
+
232
+ gl_FragColor = vec4(color, 0.9);
233
+ }
234
+ `,
235
+ transparent: true,
236
+ side: THREE.DoubleSide
237
  });
238
+
239
  visualizers.sphere = new THREE.Mesh(geometry, material);
240
 
241
  // Store original vertex positions
 
244
 
245
  scene.add(visualizers.sphere);
246
 
247
+ // Add ambient light for base illumination
248
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
249
+ scene.add(ambientLight);
 
250
 
251
+ // Add multiple point lights with different colors
252
+ const colors = [0xFF4081, 0x2196F3, 0x4CAF50];
253
+ const radius = 5;
254
+ colors.forEach((color, i) => {
255
+ const light = new THREE.PointLight(color, 1, 20);
256
+ const angle = (i / colors.length) * Math.PI * 2;
257
+ light.position.set(
258
+ Math.cos(angle) * radius,
259
+ Math.sin(angle) * radius,
260
+ radius
261
+ );
262
+ scene.add(light);
263
+ });
264
  }
265
 
266
  function createParticlesVisualization() {
267
+ const particleCount = 5000;
268
  const geometry = new THREE.BufferGeometry();
269
  const positions = new Float32Array(particleCount * 3);
270
+ const scales = new Float32Array(particleCount);
271
  const colors = new Float32Array(particleCount * 3);
 
272
 
273
  const color1 = new THREE.Color(0x4CAF50);
274
  const color2 = new THREE.Color(0x2196F3);
275
  const color3 = new THREE.Color(0xFF4081);
276
 
277
+ // Create a spiral galaxy formation
278
+ for (let i = 0; i < particleCount; i++) {
279
+ const i3 = i * 3;
280
+ const radius = (Math.random() * 3) + 2;
281
+ const spinAngle = (i / particleCount) * Math.PI * 24;
282
+ const heightRange = Math.random() * Math.PI * 2;
283
 
284
+ // Create spiral arms
285
+ positions[i3] = Math.cos(spinAngle + radius) * radius;
286
+ positions[i3 + 1] = Math.sin(heightRange) * (radius * 0.2);
287
+ positions[i3 + 2] = Math.sin(spinAngle + radius) * radius;
288
 
289
+ // Vary particle sizes
290
+ scales[i] = Math.random() * 0.5 + 0.5;
291
+
292
+ // Create color gradient along the spiral
293
+ const colorMix = Math.abs(Math.sin(spinAngle));
294
  const finalColor = new THREE.Color().lerpColors(
295
  color1,
296
  colorMix > 0.5 ? color2 : color3,
297
  colorMix
298
  );
299
 
300
+ colors[i3] = finalColor.r;
301
+ colors[i3 + 1] = finalColor.g;
302
+ colors[i3 + 2] = finalColor.b;
 
 
303
  }
304
 
305
  geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
306
+ geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
307
  geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
 
308
 
309
+ // Create custom shader material for particles
310
+ const material = new THREE.ShaderMaterial({
311
+ uniforms: {
312
+ time: { value: 0 },
313
+ size: { value: 15.0 },
314
+ pixelRatio: { value: window.devicePixelRatio }
315
+ },
316
+ vertexShader: `
317
+ attribute float scale;
318
+ attribute vec3 color;
319
+ uniform float time;
320
+ uniform float size;
321
+ uniform float pixelRatio;
322
+ varying vec3 vColor;
323
+
324
+ void main() {
325
+ vColor = color;
326
+ vec3 pos = position;
327
+
328
+ // Add some movement
329
+ float angle = time * 0.2;
330
+ pos.x = position.x * cos(angle) - position.z * sin(angle);
331
+ pos.z = position.x * sin(angle) + position.z * cos(angle);
332
+ pos.y += sin(time + position.x * 0.5) * 0.3;
333
+
334
+ vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
335
+ gl_Position = projectionMatrix * mvPosition;
336
+
337
+ // Size attenuation
338
+ gl_PointSize = size * scale * pixelRatio * (1.0 / -mvPosition.z);
339
+ }
340
+ `,
341
+ fragmentShader: `
342
+ varying vec3 vColor;
343
+
344
+ void main() {
345
+ // Create circular particles
346
+ vec2 xy = gl_PointCoord.xy - vec2(0.5);
347
+ float radius = length(xy);
348
+ float alpha = 1.0 - smoothstep(0.45, 0.5, radius);
349
+
350
+ // Add glow effect
351
+ vec3 glow = vColor * (1.0 - radius * 2.0);
352
+ vec3 finalColor = mix(vColor, glow, 0.5);
353
+
354
+ gl_FragColor = vec4(finalColor, alpha);
355
+ }
356
+ `,
357
  transparent: true,
358
+ depthWrite: false,
359
  blending: THREE.AdditiveBlending
360
  });
361
 
 
366
  // Visualization update functions
367
  function updateBarsVisualization(dataArray) {
368
  const time = Date.now() * 0.001;
369
+ const multiplier = 0.02;
370
 
371
  visualizers.bars.forEach((bar, i) => {
372
+ const value = dataArray[i % dataArray.length] * multiplier;
373
+
374
+ // Update shader uniforms
375
+ bar.material.uniforms.time.value = time;
376
+
377
+ // Calculate dynamic height
378
+ const baseHeight = value + 0.1;
379
+ const wave = Math.sin(time * 2 + bar.userData.angle) * 0.1;
380
+ const finalHeight = baseHeight + wave;
381
+
382
+ // Update bar scale and position
383
+ bar.scale.y = finalHeight;
384
+
385
+ // Add floating effect
386
+ bar.position.y = Math.sin(time + bar.userData.angle) * 0.1;
387
+
388
+ // Add subtle rotation
389
+ bar.rotation.x = Math.sin(time * 0.5 + bar.userData.angle) * 0.1;
390
+ bar.rotation.z = Math.cos(time * 0.5 + bar.userData.angle) * 0.1;
391
  });
392
  }
393
 
 
398
  const originalPositions = visualizers.sphere.userData.originalPositions;
399
  const time = Date.now() * 0.001;
400
 
401
+ // Update shader uniforms
402
+ visualizers.sphere.material.uniforms.time.value = time;
403
+
404
+ // Create more complex deformation based on audio data
405
  for (let i = 0; i < positions.length; i += 3) {
406
  const i3 = i / 3;
407
  const value = dataArray[i3 % dataArray.length] / 255;
408
 
 
409
  const deform = value * 0.5;
410
+ const noise = Math.sin(time + i3 * 0.1) * 0.2;
411
+
412
+ positions[i] = originalPositions[i] * (1 + deform * Math.sin(time + i3) + noise);
413
+ positions[i + 1] = originalPositions[i + 1] * (1 + deform * Math.cos(time + i3) + noise);
414
+ positions[i + 2] = originalPositions[i + 2] * (1 + deform * Math.sin(time * 0.5 + i3) + noise);
415
  }
416
 
417
  visualizers.sphere.geometry.attributes.position.needsUpdate = true;
 
 
418
 
419
+ // Add smooth rotation
420
+ visualizers.sphere.rotation.y += 0.002;
421
+ visualizers.sphere.rotation.x += 0.001;
 
422
  }
423
 
424
  function updateParticlesVisualization(dataArray) {
425
  if (!visualizers.particles) return;
426
 
427
+ const time = Date.now() * 0.001;
428
  const positions = visualizers.particles.geometry.attributes.position.array;
429
+ const scales = visualizers.particles.geometry.attributes.scale.array;
430
  const colors = visualizers.particles.geometry.attributes.color.array;
431
+
432
+ // Update shader uniforms
433
+ visualizers.particles.material.uniforms.time.value = time;
434
 
435
  for (let i = 0; i < positions.length; i += 3) {
436
  const i3 = i / 3;
437
  const value = dataArray[i3 % dataArray.length] / 255;
438
 
439
+ // Update particle scales based on audio
440
+ scales[i3] = (value * 0.5 + 0.5) * (Math.sin(time + i3) * 0.2 + 0.8);
 
441
 
442
+ // Update colors with audio reactivity
443
+ const hue = (i3 / positions.length) + time * 0.1;
444
+ const saturation = 0.7 + value * 0.3;
445
+ const lightness = 0.4 + value * 0.2;
446
+ const color = new THREE.Color().setHSL(hue, saturation, lightness);
447
 
 
 
 
448
  colors[i] = color.r;
449
  colors[i + 1] = color.g;
450
  colors[i + 2] = color.b;
 
 
 
451
  }
452
 
453
+ visualizers.particles.geometry.attributes.scale.needsUpdate = true;
454
  visualizers.particles.geometry.attributes.color.needsUpdate = true;
 
 
 
 
 
455
  }
456
 
457
  // Animation loop
 
480
 
481
  // Initialize the application
482
  document.addEventListener('DOMContentLoaded', () => {
483
+ // Initialize loading screen
484
+ const loadingScreen = document.querySelector('.loading-screen');
485
+
486
+ function showLoading() {
487
+ if (loadingScreen) {
488
+ loadingScreen.classList.remove('hidden');
489
+ }
490
+ }
491
+
492
+ function hideLoading() {
493
+ if (loadingScreen) {
494
+ loadingScreen.classList.add('hidden');
495
+ }
496
+ }
497
+
498
+ // Show loading screen immediately
499
+ showLoading();
500
+
501
+ // Initialize all components
502
  setupUploadHandlers();
503
  setupPlayerControls();
504
  setupToggleHandlers();
505
+ setupProgressBar();
506
+ setupPlaylistControls();
507
  createVisualization();
508
+
509
+ // Hide loading screen after initialization
510
+ hideLoading();
511
  });
512
 
513
  // Export necessary functions and variables
 
525
  // Camera position is now set in createVisualization
526
  }
527
 
528
+ // Enhanced file upload handling
529
  function setupUploadHandlers() {
530
  const uploadArea = document.getElementById('upload-area');
531
  const fileInput = document.getElementById('audio-upload');
532
+ const uploadProgress = document.querySelector('.upload-progress');
533
+ const progressFill = uploadProgress?.querySelector('.progress-fill');
534
+ const progressText = uploadProgress?.querySelector('.progress-text');
535
  const errorToast = document.querySelector('.error-toast');
536
 
537
+ if (!uploadArea || !fileInput) {
538
+ console.error('Upload elements not found');
539
+ return;
540
+ }
541
+
542
  uploadArea.addEventListener('dragover', (e) => {
543
  e.preventDefault();
544
  uploadArea.classList.add('dragover');
 
562
  handleFiles(e.target.files);
563
  });
564
 
565
+ // Enhanced file upload with progress
566
+ async function handleFiles(files) {
567
+ if (!files || files.length === 0) {
568
+ showError('No files selected');
569
+ return;
570
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ if (!audioContext) {
573
+ try {
574
+ initAudio();
575
+ } catch (error) {
576
+ console.error('Failed to initialize audio:', error);
577
+ showError('Failed to initialize audio system');
578
+ return;
579
+ }
580
+ }
581
+
582
+ const formData = new FormData();
583
+ Array.from(files).forEach(file => {
584
+ formData.append('files[]', file);
585
  });
586
 
587
+ // Show upload progress if elements exist
588
+ if (uploadProgress && progressFill && progressText) {
589
+ uploadProgress.classList.add('visible');
590
+ progressFill.style.width = '0%';
591
+ progressText.textContent = '0%';
592
+ }
593
 
594
+ try {
595
+ const response = await fetch('/upload', {
596
+ method: 'POST',
597
+ body: formData
598
+ });
599
+
600
+ if (!response.ok) {
601
+ throw new Error(`HTTP error! status: ${response.status}`);
602
+ }
603
+
604
+ const data = await response.json();
 
605
 
606
+ if (data.success) {
607
+ showSuccess('Files uploaded successfully');
608
+
609
+ // Filter successful uploads and create playlist
610
+ const successfulFiles = data.files.filter(file => file.success);
611
+ if (successfulFiles.length === 0) {
612
+ showError('No files were uploaded successfully');
613
+ return;
614
+ }
615
+
616
+ // Add originalIndex to each track
617
+ playlist = successfulFiles.map((file, index) => ({
618
+ name: file.filename,
619
+ url: file.filepath,
620
+ metadata: file.metadata,
621
+ originalIndex: index
622
+ }));
623
+
624
+ createPlaylist();
625
+ if (playlist.length > 0) {
626
+ playTrack(0);
627
+ }
628
  } else {
629
+ showError(data.error || 'Upload failed');
630
+ }
631
+ } catch (error) {
632
+ console.error('Upload error:', error);
633
+ showError('Error uploading files');
634
+ } finally {
635
+ // Hide upload progress if elements exist
636
+ if (uploadProgress && progressFill && progressText) {
637
+ uploadProgress.classList.remove('visible');
638
+ progressFill.style.width = '0%';
639
+ progressText.textContent = '0%';
640
  }
 
 
641
  }
 
 
 
642
  }
643
  }
644
 
645
+ // Enhanced toast notifications with null checks
646
  function showError(message) {
647
+ const toast = document.querySelector('.error-toast');
648
+ if (!toast) return;
649
+
650
+ toast.textContent = message;
651
+ toast.className = 'error-toast error visible';
652
+ setTimeout(() => {
653
+ toast.classList.remove('visible');
654
+ }, 3000);
655
+ }
656
+
657
+ function showSuccess(message) {
658
+ const toast = document.querySelector('.error-toast');
659
+ if (!toast) return;
660
+
661
+ toast.textContent = message;
662
+ toast.className = 'error-toast success visible';
663
  setTimeout(() => {
664
+ toast.classList.remove('visible');
665
  }, 3000);
666
  }
667
 
668
+ // Enhanced keyboard shortcuts
669
+ document.addEventListener('keydown', (e) => {
670
+ if (e.code === 'Space' && !e.target.matches('input, textarea')) {
671
+ e.preventDefault();
672
+ togglePlayPause();
673
+ } else if (e.code === 'ArrowLeft') {
674
+ playPrevious();
675
+ } else if (e.code === 'ArrowRight') {
676
+ playNext();
677
+ } else if (e.code === 'KeyM') {
678
+ toggleMute();
679
+ }
680
+ });
681
+
682
+ // Volume control
683
+ function toggleMute() {
684
+ if (!audioElement) return;
685
+
686
+ const volumeBtn = document.querySelector('.volume-btn i');
687
+ if (audioElement.volume > 0) {
688
+ audioElement.volume = 0;
689
+ volumeBtn.className = 'fas fa-volume-mute';
690
+ } else {
691
+ audioElement.volume = 0.5;
692
+ volumeBtn.className = 'fas fa-volume-up';
693
+ }
694
+
695
+ updateVolumeUI();
696
+ }
697
+
698
+ function updateVolumeUI() {
699
+ const volumeProgress = document.querySelector('.volume-progress');
700
+ const volumeHandle = document.querySelector('.volume-handle');
701
+ const volumeSlider = document.getElementById('volume');
702
+
703
+ if (volumeProgress && volumeHandle && volumeSlider) {
704
+ const value = audioElement ? audioElement.volume : 0.5;
705
+ volumeProgress.style.width = `${value * 100}%`;
706
+ volumeHandle.style.left = `${value * 100}%`;
707
+ volumeSlider.value = value;
708
+ }
709
+ }
710
+
711
+ // Enhanced progress bar interaction
712
+ function setupProgressBar() {
713
+ const progressContainer = document.querySelector('.progress-container');
714
+ const progressBar = document.querySelector('.progress-bar');
715
+ const progress = document.querySelector('.progress');
716
+ const progressHandle = document.querySelector('.progress-handle');
717
+ const seekSlider = document.querySelector('.seek-slider');
718
+
719
+ // Return early if required elements are not found
720
+ if (!progressBar || !progress) {
721
+ console.error('Progress bar elements not found');
722
+ return;
723
+ }
724
+
725
+ let isDragging = false;
726
+
727
+ // Mouse events for desktop
728
+ progressBar.addEventListener('mousedown', startDragging);
729
+ document.addEventListener('mousemove', updateDragging);
730
+ document.addEventListener('mouseup', stopDragging);
731
+
732
+ // Touch events for mobile
733
+ progressBar.addEventListener('touchstart', handleTouchStart);
734
+ document.addEventListener('touchmove', handleTouchMove);
735
+ document.addEventListener('touchend', handleTouchEnd);
736
+
737
+ function handleTouchStart(e) {
738
+ e.preventDefault();
739
+ isDragging = true;
740
+ progressBar.classList.add('dragging');
741
+ updateProgress(e.touches[0]);
742
+ }
743
+
744
+ function handleTouchMove(e) {
745
+ if (!isDragging) return;
746
+ e.preventDefault();
747
+ updateProgress(e.touches[0]);
748
+ }
749
+
750
+ function handleTouchEnd() {
751
+ isDragging = false;
752
+ progressBar.classList.remove('dragging');
753
+ }
754
+
755
+ function startDragging(e) {
756
+ isDragging = true;
757
+ progressBar.classList.add('dragging');
758
+ updateProgress(e);
759
+ }
760
+
761
+ function updateDragging(e) {
762
+ if (!isDragging) return;
763
+ updateProgress(e);
764
+ }
765
+
766
+ function stopDragging() {
767
+ isDragging = false;
768
+ progressBar.classList.remove('dragging');
769
+ }
770
+
771
+ function updateProgress(e) {
772
+ if (!audioElement || !audioElement.duration) return;
773
+
774
+ try {
775
+ const rect = progressBar.getBoundingClientRect();
776
+ const x = e.clientX || e.pageX;
777
+ const percent = Math.min(Math.max((x - rect.left) / rect.width, 0), 1);
778
+
779
+ // Update progress bar and handle
780
+ progress.style.width = `${percent * 100}%`;
781
+ if (progressHandle) {
782
+ progressHandle.style.left = `${percent * 100}%`;
783
+ }
784
+ if (seekSlider) {
785
+ seekSlider.value = percent * 100;
786
+ }
787
+
788
+ // Update audio time
789
+ audioElement.currentTime = percent * audioElement.duration;
790
+
791
+ // Force time display update
792
+ updateTimeDisplay();
793
+ } catch (error) {
794
+ console.error('Error updating progress:', error);
795
+ }
796
+ }
797
+
798
+ // Add seek slider input handler
799
+ if (seekSlider) {
800
+ seekSlider.addEventListener('input', (e) => {
801
+ if (!audioElement || !audioElement.duration) return;
802
+
803
+ const percent = e.target.value;
804
+ progress.style.width = `${percent}%`;
805
+ if (progressHandle) {
806
+ progressHandle.style.left = `${percent}%`;
807
+ }
808
+
809
+ audioElement.currentTime = (percent / 100) * audioElement.duration;
810
+ updateTimeDisplay();
811
+ });
812
+ }
813
+ }
814
+
815
  // Setup player controls
816
  function setupPlayerControls() {
817
  const playPauseBtn = document.getElementById('play-pause');
 
900
 
901
  // Seek to position
902
  function seekTo(e) {
903
+ if (!audioElement || !audioElement.duration) return;
904
+
905
+ const seekSlider = e.target;
906
+ const progress = document.querySelector('.progress');
907
+ const progressHandle = document.querySelector('.progress-handle');
908
+ const time = (seekSlider.value / 100) * audioElement.duration;
909
+
910
+ // Update audio time
911
+ audioElement.currentTime = time;
912
+
913
+ // Update progress bar and handle
914
+ if (progress) {
915
  progress.style.width = `${seekSlider.value}%`;
 
 
 
916
  }
917
+ if (progressHandle) {
918
+ progressHandle.style.left = `${seekSlider.value}%`;
919
+ }
920
+
921
+ // Force time display update
922
+ updateTimeDisplay();
923
  }
924
 
925
  // Update time display
926
  function updateTimeDisplay() {
927
  if (!audioElement) return;
928
 
929
+ const currentTimeEl = document.querySelector('.current-time');
930
+ const totalTimeEl = document.querySelector('.total-time');
 
931
  const progress = document.querySelector('.progress');
932
+ const progressHandle = document.querySelector('.progress-handle');
933
+ const seekSlider = document.querySelector('.seek-slider');
934
 
935
  // Only update if duration is available and not NaN
936
  if (audioElement.duration && !isNaN(audioElement.duration)) {
 
938
  const total = formatTime(audioElement.duration);
939
  const progressPercent = (audioElement.currentTime / audioElement.duration) * 100;
940
 
941
+ // Update time displays
942
+ if (currentTimeEl) currentTimeEl.textContent = current;
943
+ if (totalTimeEl) totalTimeEl.textContent = total;
 
 
944
 
945
+ // Update progress bar and handle
946
+ if (progress) {
 
947
  progress.style.width = `${progressPercent}%`;
948
  }
949
+ if (progressHandle) {
950
+ progressHandle.style.left = `${progressPercent}%`;
951
+ }
952
+ if (seekSlider && !seekSlider.matches(':active')) {
953
+ seekSlider.value = progressPercent;
954
+ }
955
+ } else {
956
+ // Reset displays if no duration available
957
+ if (currentTimeEl) currentTimeEl.textContent = '0:00';
958
+ if (totalTimeEl) totalTimeEl.textContent = '0:00';
959
+ if (progress) progress.style.width = '0%';
960
+ if (progressHandle) progressHandle.style.left = '0%';
961
+ if (seekSlider) seekSlider.value = 0;
962
  }
963
  }
964
 
965
+ // Format time helper function
966
  function formatTime(seconds) {
967
  if (!seconds || isNaN(seconds)) return '0:00';
968
+
969
  const mins = Math.floor(seconds / 60);
970
+ const secs = Math.floor(seconds % 60);
971
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
972
  }
973
 
974
  // Handle track end
 
1081
  }
1082
  }
1083
 
1084
+ // Update playTrack function to properly set up time updates
1085
+ async function playTrack(index) {
1086
+ if (index < 0 || index >= playlist.length) return;
1087
 
1088
+ try {
1089
+ // Stop current playback
1090
+ if (audioElement) {
1091
+ audioElement.pause();
1092
+ audioElement.currentTime = 0;
1093
+
1094
+ // Remove existing listeners
1095
+ audioElement.removeEventListener('timeupdate', updateTimeDisplay);
1096
+ audioElement.removeEventListener('loadedmetadata', updateTimeDisplay);
1097
+ audioElement.removeEventListener('ended', handleTrackEnd);
1098
+ }
1099
+
1100
+ currentTrackIndex = index;
1101
+ const track = playlist[currentTrackIndex];
1102
+
1103
+ if (!audioElement) {
1104
+ console.warn('Audio element not initialized');
1105
+ return;
1106
+ }
1107
+
1108
+ // Update source and load new track
1109
+ audioElement.src = track.url;
1110
+ await audioElement.load();
1111
+
1112
+ // Add event listeners for time updates
1113
+ audioElement.addEventListener('timeupdate', updateTimeDisplay);
1114
+ audioElement.addEventListener('loadedmetadata', updateTimeDisplay);
1115
+ audioElement.addEventListener('ended', handleTrackEnd);
1116
+
1117
+ // Update playlist UI
1118
+ document.querySelectorAll('.playlist-item').forEach((item, i) => {
1119
+ item.classList.toggle('active', i === index);
1120
+ });
1121
+
1122
+ // Force initial time display update
1123
+ updateTimeDisplay();
1124
+
1125
+ // Attempt to play with retry logic
1126
+ try {
1127
+ await audioElement.play();
 
 
 
1128
  isPlaying = true;
1129
+ updatePlayPauseButton();
 
 
1130
  updateNowPlayingInfo();
1131
+ } catch (playError) {
1132
+ console.warn('Play interrupted, retrying...', playError);
1133
+ // Add a small delay before retrying
1134
+ setTimeout(async () => {
1135
+ try {
1136
+ await audioElement.play();
1137
+ isPlaying = true;
1138
+ updatePlayPauseButton();
1139
+ updateNowPlayingInfo();
1140
+ } catch (retryError) {
1141
+ console.error('Failed to play after retry:', retryError);
1142
+ showError('Failed to play track. Please try again.');
1143
+ }
1144
+ }, 100);
1145
+ }
1146
+ } catch (error) {
1147
+ console.error('Error playing track:', error);
1148
+ showError('Error playing track');
1149
+ }
1150
  }
1151
 
1152
  // Add these functions after the existing initialization code
 
1158
  const playlistToggleBtn = document.querySelector('.playlist-toggle-btn');
1159
  const vizTypeBtn = document.querySelector('.viz-type-btn');
1160
  const vizTypeDropdown = document.querySelector('.viz-type-dropdown');
1161
+
1162
+ if (!mainContent || !uploadArea || !playlistContainer) {
1163
+ console.error('Required elements not found');
1164
+ return;
1165
+ }
1166
 
1167
  // Show playlist by default
1168
+ mainContent.classList.add('visible');
1169
+ playlistContainer.classList.add('visible');
1170
+ if (playlistToggleBtn) playlistToggleBtn.classList.add('active');
1171
 
1172
+ // Upload button handler
1173
  uploadToggleBtn?.addEventListener('click', () => {
1174
+ const isVisible = uploadArea.classList.contains('visible');
1175
 
1176
  // Hide playlist if it's visible
1177
+ playlistContainer.classList.remove('visible');
1178
  playlistToggleBtn?.classList.remove('active');
1179
 
1180
  // Toggle upload area
1181
+ uploadArea.classList.toggle('visible');
1182
+ uploadToggleBtn.classList.toggle('active');
1183
 
1184
  // Show/hide main content
1185
+ mainContent.classList.toggle('visible', !isVisible || playlistContainer.classList.contains('visible'));
1186
  });
1187
 
1188
+ // Playlist button handler
1189
  playlistToggleBtn?.addEventListener('click', () => {
1190
+ const isVisible = playlistContainer.classList.contains('visible');
1191
 
1192
  // Hide upload area if it's visible
1193
+ uploadArea.classList.remove('visible');
1194
  uploadToggleBtn?.classList.remove('active');
1195
 
1196
  // Toggle playlist
1197
+ playlistContainer.classList.toggle('visible');
1198
+ playlistToggleBtn.classList.toggle('active');
1199
 
1200
  // Show/hide main content
1201
+ mainContent.classList.toggle('visible', !isVisible || uploadArea.classList.contains('visible'));
1202
  });
1203
 
1204
+ // Visualization type button handler
1205
+ let isVizDropdownVisible = false;
1206
+
1207
  vizTypeBtn?.addEventListener('click', (e) => {
1208
+ e.stopPropagation();
1209
+ isVizDropdownVisible = !isVizDropdownVisible;
 
1210
 
1211
+ if (isVizDropdownVisible) {
1212
+ vizTypeDropdown?.classList.add('visible');
1213
+ vizTypeBtn.classList.add('active');
1214
+ } else {
1215
+ vizTypeDropdown?.classList.remove('visible');
1216
+ vizTypeBtn.classList.remove('active');
1217
+ }
1218
  });
1219
 
1220
  // Handle visualization type selection
1221
+ const vizTypeOptions = document.querySelectorAll('.viz-type-options button');
1222
  vizTypeOptions?.forEach(button => {
1223
  button.addEventListener('click', (e) => {
1224
+ e.stopPropagation();
1225
  const type = button.dataset.type;
1226
 
1227
  // Update active state
 
1233
  createVisualization();
1234
 
1235
  // Close dropdown
1236
+ isVizDropdownVisible = false;
1237
  vizTypeDropdown?.classList.remove('visible');
1238
  vizTypeBtn?.classList.remove('active');
1239
  });
1240
  });
1241
 
1242
+ // Close dropdowns when clicking outside
1243
  document.addEventListener('click', (e) => {
1244
+ if (isVizDropdownVisible &&
1245
+ vizTypeDropdown &&
1246
+ !vizTypeDropdown.contains(e.target) &&
1247
+ !vizTypeBtn?.contains(e.target)) {
1248
+ isVizDropdownVisible = false;
1249
  vizTypeDropdown.classList.remove('visible');
1250
+ vizTypeBtn?.classList.remove('active');
1251
+ }
1252
+ });
1253
+ }
1254
+
1255
+ // Add updatePlayPauseButton function
1256
+ function updatePlayPauseButton() {
1257
+ const playPauseBtn = document.getElementById('play-pause');
1258
+ if (!playPauseBtn) return;
1259
+
1260
+ playPauseBtn.innerHTML = isPlaying ?
1261
+ '<i class="fas fa-pause"></i>' :
1262
+ '<i class="fas fa-play"></i>';
1263
+
1264
+ // Update button state
1265
+ playPauseBtn.disabled = !audioElement || !playlist.length;
1266
+ }
1267
+
1268
+ // Setup shuffle and repeat buttons
1269
+ function setupPlaylistControls() {
1270
+ const shuffleBtn = document.querySelector('.shuffle-btn');
1271
+ const repeatBtn = document.querySelector('.repeat-btn');
1272
+
1273
+ // Setup shuffle button
1274
+ shuffleBtn?.addEventListener('click', () => {
1275
+ isShuffleActive = !isShuffleActive;
1276
+ shuffleBtn.classList.toggle('active', isShuffleActive);
1277
+
1278
+ if (isShuffleActive) {
1279
+ // Save current track index
1280
+ const currentTrack = playlist[currentTrackIndex];
1281
+
1282
+ // Shuffle playlist
1283
+ for (let i = playlist.length - 1; i > 0; i--) {
1284
+ const j = Math.floor(Math.random() * (i + 1));
1285
+ [playlist[i], playlist[j]] = [playlist[j], playlist[i]];
1286
+ }
1287
+
1288
+ // Find new index of current track
1289
+ currentTrackIndex = playlist.findIndex(track => track === currentTrack);
1290
+
1291
+ // Update playlist UI
1292
+ createPlaylist();
1293
+ updateNowPlayingInfo();
1294
+ } else {
1295
+ // Restore original order if needed
1296
+ playlist.sort((a, b) => a.originalIndex - b.originalIndex);
1297
+ createPlaylist();
1298
  }
1299
  });
1300
+
1301
+ // Setup repeat button
1302
+ repeatBtn?.addEventListener('click', () => {
1303
+ isRepeatActive = !isRepeatActive;
1304
+ repeatBtn.classList.toggle('active', isRepeatActive);
1305
+ });
1306
  }
templates/index.html CHANGED
@@ -1,32 +1,46 @@
1
  <!DOCTYPE html>
2
- <html>
3
  <head>
 
 
4
  <title>3D Music Visualizer</title>
 
5
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
6
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
7
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
11
  </head>
12
  <body>
 
 
 
 
 
 
 
13
  <header class="app-header">
14
  <div class="logo">
15
  <i class="fas fa-music"></i>
16
  <span>Soundscape</span>
17
  </div>
18
  <div class="header-controls">
19
- <button class="header-btn upload-toggle-btn" title="Show upload">
20
  <i class="fas fa-cloud-upload-alt"></i>
 
21
  </button>
22
- <button class="header-btn playlist-toggle-btn" title="Show playlist">
23
  <i class="fas fa-list"></i>
 
24
  </button>
25
- <button class="header-btn viz-type-btn" title="Change visualization">
26
  <i class="fas fa-cube"></i>
 
27
  </button>
28
- <button class="header-btn theme-btn" title="Toggle theme">
29
  <i class="fas fa-moon"></i>
 
30
  </button>
31
  </div>
32
  </header>
@@ -40,6 +54,12 @@
40
  <p>or click to browse</p>
41
  <span class="file-types">Supports MP3, WAV, OGG, FLAC</span>
42
  </div>
 
 
 
 
 
 
43
  <div id="file-name"></div>
44
  </div>
45
  <input type="file" id="audio-upload" accept=".mp3,.wav,.ogg,.flac" multiple>
@@ -51,20 +71,30 @@
51
  <div class="playlist-controls">
52
  <button class="playlist-btn shuffle-btn" title="Shuffle">
53
  <i class="fas fa-random"></i>
 
54
  </button>
55
  <button class="playlist-btn repeat-btn" title="Repeat">
56
  <i class="fas fa-redo"></i>
 
57
  </button>
58
  </div>
59
  </div>
60
  <div class="tracks-list">
61
  <!-- Tracks will be dynamically added here -->
62
  </div>
 
 
 
 
 
63
  </div>
64
  </div>
65
 
66
- <div id="controls" class="controls-container">
67
  <div class="now-playing-info">
 
 
 
68
  <div class="now-playing-text">
69
  <div class="now-playing-title">No track selected</div>
70
  <div class="now-playing-artist">Upload some music to begin</div>
@@ -75,28 +105,32 @@
75
  </div>
76
  </div>
77
 
78
- <div class="progress-bar">
79
- <div class="progress"></div>
80
- <input type="range" class="seek-slider" min="0" max="100" value="0">
 
 
 
81
  </div>
82
 
83
  <div class="music-controls">
84
- <button class="control-btn previous-btn" disabled>
85
  <i class="fas fa-backward"></i>
86
  </button>
87
- <button class="control-btn play-pause-btn" id="play-pause" disabled>
88
  <i class="fas fa-play"></i>
89
  </button>
90
- <button class="control-btn next-btn" disabled>
91
  <i class="fas fa-forward"></i>
92
  </button>
93
 
94
  <div class="volume-control">
95
- <button class="volume-btn">
96
  <i class="fas fa-volume-up"></i>
97
  </button>
98
  <div class="volume-slider-container">
99
  <div class="volume-progress"></div>
 
100
  <input type="range" id="volume" min="0" max="1" step="0.1" value="0.5">
101
  </div>
102
  </div>
@@ -108,9 +142,42 @@
108
 
109
  <div class="viz-type-dropdown">
110
  <div class="viz-type-options">
111
- <button data-type="sphere" class="active">Sphere</button>
112
- <button data-type="bars">Circular Bars</button>
113
- <button data-type="particles">Particles</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  </div>
115
  </div>
116
 
 
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>3D Music Visualizer</title>
7
+ <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='img/favicon.svg') }}">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
13
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
14
  </head>
15
  <body>
16
+ <div class="loading-screen">
17
+ <div class="loader">
18
+ <div class="loader-circle"></div>
19
+ <div class="loader-text">Loading...</div>
20
+ </div>
21
+ </div>
22
+
23
  <header class="app-header">
24
  <div class="logo">
25
  <i class="fas fa-music"></i>
26
  <span>Soundscape</span>
27
  </div>
28
  <div class="header-controls">
29
+ <button class="header-btn upload-toggle-btn" title="Upload Music">
30
  <i class="fas fa-cloud-upload-alt"></i>
31
+ <span class="tooltip">Upload Music</span>
32
  </button>
33
+ <button class="header-btn playlist-toggle-btn" title="Show Playlist">
34
  <i class="fas fa-list"></i>
35
+ <span class="tooltip">Playlist</span>
36
  </button>
37
+ <button class="header-btn viz-type-btn" title="Change Visualization">
38
  <i class="fas fa-cube"></i>
39
+ <span class="tooltip">Visualization Style</span>
40
  </button>
41
+ <button class="header-btn theme-btn" title="Toggle Theme">
42
  <i class="fas fa-moon"></i>
43
+ <span class="tooltip">Toggle Theme</span>
44
  </button>
45
  </div>
46
  </header>
 
54
  <p>or click to browse</p>
55
  <span class="file-types">Supports MP3, WAV, OGG, FLAC</span>
56
  </div>
57
+ <div class="upload-progress">
58
+ <div class="progress-bar">
59
+ <div class="progress-fill"></div>
60
+ </div>
61
+ <div class="progress-text">0%</div>
62
+ </div>
63
  <div id="file-name"></div>
64
  </div>
65
  <input type="file" id="audio-upload" accept=".mp3,.wav,.ogg,.flac" multiple>
 
71
  <div class="playlist-controls">
72
  <button class="playlist-btn shuffle-btn" title="Shuffle">
73
  <i class="fas fa-random"></i>
74
+ <span class="tooltip">Shuffle</span>
75
  </button>
76
  <button class="playlist-btn repeat-btn" title="Repeat">
77
  <i class="fas fa-redo"></i>
78
+ <span class="tooltip">Repeat</span>
79
  </button>
80
  </div>
81
  </div>
82
  <div class="tracks-list">
83
  <!-- Tracks will be dynamically added here -->
84
  </div>
85
+ <div class="no-tracks-message">
86
+ <i class="fas fa-music"></i>
87
+ <p>No tracks added yet</p>
88
+ <button class="upload-btn">Upload Music</button>
89
+ </div>
90
  </div>
91
  </div>
92
 
93
+ <div class="controls-container">
94
  <div class="now-playing-info">
95
+ <div class="track-artwork">
96
+ <i class="fas fa-music"></i>
97
+ </div>
98
  <div class="now-playing-text">
99
  <div class="now-playing-title">No track selected</div>
100
  <div class="now-playing-artist">Upload some music to begin</div>
 
105
  </div>
106
  </div>
107
 
108
+ <div class="progress-container">
109
+ <div class="progress-bar">
110
+ <div class="progress"></div>
111
+ <div class="progress-handle"></div>
112
+ <input type="range" class="seek-slider" min="0" max="100" value="0">
113
+ </div>
114
  </div>
115
 
116
  <div class="music-controls">
117
+ <button class="control-btn previous-btn" disabled title="Previous">
118
  <i class="fas fa-backward"></i>
119
  </button>
120
+ <button class="control-btn play-pause-btn" id="play-pause" disabled title="Play">
121
  <i class="fas fa-play"></i>
122
  </button>
123
+ <button class="control-btn next-btn" disabled title="Next">
124
  <i class="fas fa-forward"></i>
125
  </button>
126
 
127
  <div class="volume-control">
128
+ <button class="volume-btn" title="Volume">
129
  <i class="fas fa-volume-up"></i>
130
  </button>
131
  <div class="volume-slider-container">
132
  <div class="volume-progress"></div>
133
+ <div class="volume-handle"></div>
134
  <input type="range" id="volume" min="0" max="1" step="0.1" value="0.5">
135
  </div>
136
  </div>
 
142
 
143
  <div class="viz-type-dropdown">
144
  <div class="viz-type-options">
145
+ <button data-type="sphere" class="active">
146
+ <i class="fas fa-globe"></i>
147
+ <span>Sphere</span>
148
+ </button>
149
+ <button data-type="bars">
150
+ <i class="fas fa-chart-bar"></i>
151
+ <span>Circular Bars</span>
152
+ </button>
153
+ <button data-type="particles">
154
+ <i class="fas fa-sparkles"></i>
155
+ <span>Particles</span>
156
+ </button>
157
+ </div>
158
+ </div>
159
+
160
+ <div class="keyboard-shortcuts">
161
+ <div class="shortcuts-content">
162
+ <h3>Keyboard Shortcuts</h3>
163
+ <div class="shortcut-list">
164
+ <div class="shortcut-item">
165
+ <span class="key">Space</span>
166
+ <span class="action">Play/Pause</span>
167
+ </div>
168
+ <div class="shortcut-item">
169
+ <span class="key">←</span>
170
+ <span class="action">Previous Track</span>
171
+ </div>
172
+ <div class="shortcut-item">
173
+ <span class="key">→</span>
174
+ <span class="action">Next Track</span>
175
+ </div>
176
+ <div class="shortcut-item">
177
+ <span class="key">M</span>
178
+ <span class="action">Mute/Unmute</span>
179
+ </div>
180
+ </div>
181
  </div>
182
  </div>
183