NeoPy commited on
Commit
db69e64
·
verified ·
1 Parent(s): aeb45e6

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +864 -261
app.js CHANGED
@@ -1,94 +1,190 @@
1
  // ==UserScript==
2
- // @name Spotify Enhanced UI/UX with Audio Visualizer
3
  // @namespace https://violentmonkey.github.io/
4
- // @version 2.1
5
- // @description Transforms Spotify web player with dynamic animations, stylish UI enhancements, and a mesmerizing audio visualizer
6
- // @author UI/UX Designer
7
  // @match https://open.spotify.com/*
8
  // @icon https://www.spotify.com/favicon.ico
9
  // @grant GM_addStyle
10
  // @grant GM_getValue
11
  // @grant GM_setValue
12
  // @run-at document-start
13
- // @updateURL https://github.com/user/spotify-enhanced/raw/main/script.user.js
14
- // @downloadURL https://github.com/user/spotify-enhanced/raw/main/script.user.js
15
  // ==/UserScript==
16
 
17
  (function() {
18
  'use strict';
19
 
20
- // Wait for DOM to be fully loaded
21
- if (document.readyState !== 'complete') {
22
- window.addEventListener('load', initEnhancedSpotify);
23
- } else {
24
- initEnhancedSpotify();
25
- }
 
 
 
 
 
 
 
26
 
 
27
  function initEnhancedSpotify() {
28
- // Create style element for custom CSS
29
  const style = document.createElement('style');
30
- style.textContent = `
31
- /* Global UI Enhancements */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  :root {
33
  --primary-gradient: linear-gradient(45deg, #1DB954, #1ED760, #00ffcc);
34
  --secondary-gradient: linear-gradient(45deg, #8A2BE2, #9370DB, #BA55D3);
 
35
  --glow-color: rgba(0, 255, 204, 0.7);
 
 
36
  --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
37
  --transition-normal: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
38
  --transition-slow: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
 
41
- /* Enhanced buttons */
 
 
 
 
 
42
  button, .Button-sc-1dqy6sn-0, .ButtonInner-sc-1dqy6sn-1 {
43
  transition: var(--transition-normal) !important;
44
  transform: scale(1) !important;
45
  background: var(--primary-gradient) !important;
46
  background-size: 200% 200% !important;
47
  animation: gradientBG 3s ease infinite !important;
48
- box-shadow: 0 4px 15px rgba(0, 255, 204, 0.3) !important;
49
  border: none !important;
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
  button:hover, .Button-sc-1dqy6sn-0:hover, .ButtonInner-sc-1dqy6sn-1:hover {
53
- transform: scale(1.08) !important;
54
- box-shadow: 0 6px 25px rgba(0, 255, 204, 0.5) !important;
55
  filter: brightness(1.1) !important;
56
  }
57
 
58
- /* Card hover effects */
59
- .Card-sc-1uyc430-0, .main-card-card {
60
- transition: var(--transition-normal) !important;
 
 
 
 
 
 
 
 
 
 
 
 
61
  transform: translateY(0) !important;
62
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2) !important;
63
- border-radius: 12px !important;
64
  overflow: hidden !important;
 
 
 
 
 
65
  }
66
 
67
- .Card-sc-1uyc430-0:hover, .main-card-card:hover {
68
  transform: translateY(-8px) !important;
69
- box-shadow: 0 12px 30px rgba(0, 255, 204, 0.4) !important;
70
  z-index: 100 !important;
 
71
  }
72
 
73
- /* Album art effects */
74
- .cover-art-image {
75
- transition: transform 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275),
76
- box-shadow 0.4s ease !important;
77
  transform-origin: center !important;
 
 
 
78
  }
79
 
80
- .cover-art-image:hover {
81
- transform: scale(1.15) rotate(3deg) !important;
82
- box-shadow: 0 0 30px rgba(0, 255, 204, 0.6) !important;
83
  z-index: 1000 !important;
84
  }
85
 
86
- /* Now playing bar enhancements */
87
- .now-playing-bar {
 
 
 
 
 
 
 
 
 
 
 
88
  transition: var(--transition-slow) !important;
89
- background: rgba(0, 0, 0, 0.8) !important;
90
- backdrop-filter: blur(10px) !important;
91
  border-top: 1px solid rgba(0, 255, 204, 0.3) !important;
 
 
 
92
  }
93
 
94
  .now-playing-bar:hover {
@@ -96,27 +192,36 @@
96
  box-shadow: 0 -10px 30px rgba(0, 255, 204, 0.3) !important;
97
  }
98
 
99
- /* Visualizer container */
100
  #visualizer-container {
101
  position: fixed;
102
  bottom: 0;
103
  left: 0;
104
  right: 0;
105
- height: 80px;
106
- background: rgba(0, 0, 0, 0.7);
107
- backdrop-filter: blur(5px);
108
  z-index: 9999;
109
  display: flex;
 
110
  justify-content: center;
111
  align-items: center;
112
- padding: 0 20px;
113
  border-top: 1px solid rgba(0, 255, 204, 0.3);
114
- opacity: 0.9;
115
- transition: opacity 0.3s ease;
 
 
116
  }
117
 
118
- #visualizer-container:hover {
119
- opacity: 1;
 
 
 
 
 
 
120
  }
121
 
122
  /* Visualizer canvas */
@@ -124,73 +229,80 @@
124
  width: 100%;
125
  height: 60px;
126
  display: block;
 
 
127
  }
128
 
129
- /* Visualizer control buttons */
130
  .visualizer-controls {
131
  position: absolute;
132
- bottom: 5px;
133
- right: 10px;
134
  display: flex;
135
- gap: 8px;
136
  z-index: 10000;
 
137
  }
138
 
139
  .visualizer-btn {
140
- width: 28px;
141
- height: 28px;
142
- border-radius: 50%;
143
  background: rgba(255, 255, 255, 0.1);
144
- border: 1px solid rgba(0, 255, 204, 0.5);
145
  color: white;
146
  display: flex;
147
  justify-content: center;
148
  align-items: center;
149
  cursor: pointer;
150
- font-size: 12px;
151
  transition: all 0.2s ease;
 
 
 
 
 
 
 
152
  }
153
 
154
  .visualizer-btn:hover {
155
- background: rgba(0, 255, 204, 0.3);
156
- transform: scale(1.1);
157
  }
158
 
159
- /* Progress bar enhancements */
160
- .progress-bar {
161
- height: 4px !important;
162
- background: rgba(255, 255, 255, 0.2) !important;
163
- border-radius: 2px !important;
164
  overflow: hidden !important;
165
  position: relative !important;
 
166
  }
167
 
168
- .progress-bar__slider {
169
- width: 14px !important;
170
- height: 14px !important;
171
- background: var(--primary-gradient) !important;
172
- border: 2px solid white !important;
173
- box-shadow: 0 0 10px var(--glow-color) !important;
174
  transition: all 0.2s ease !important;
 
175
  }
176
 
177
- .progress-bar__slider:hover {
178
- transform: scale(1.5) !important;
179
- box-shadow: 0 0 20px var(--glow-color) !important;
180
  }
181
 
182
- /* Volume slider */
183
- .volume-bar {
184
- height: 4px !important;
185
- background: rgba(255, 255, 255, 0.2) !important;
186
- border-radius: 2px !important;
187
- }
188
-
189
- .volume-bar__slider {
190
- width: 12px !important;
191
- height: 12px !important;
192
- background: var(--primary-gradient) !important;
193
- box-shadow: 0 0 8px var(--glow-color) !important;
194
  }
195
 
196
  /* Text animations */
@@ -205,10 +317,103 @@
205
 
206
  /* Gradient text */
207
  .glow-text {
208
- background: var(--primary-gradient);
209
  -webkit-background-clip: text;
210
  -webkit-text-fill-color: transparent;
211
  text-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
 
214
  /* Keyframes */
@@ -218,91 +423,301 @@
218
  100% { background-position: 0% 50% }
219
  }
220
 
 
 
 
 
 
 
221
  @keyframes pulse {
222
  0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 255, 204, 0.4); }
223
- 70% { transform: scale(1.02); box-shadow: 0 0 0 12px rgba(0, 255, 204, 0); }
224
  100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 255, 204, 0); }
225
  }
226
 
 
 
 
 
 
227
  @keyframes fadeIn {
228
  from { opacity: 0; transform: translateY(10px); }
229
  to { opacity: 1; transform: translateY(0); }
230
  }
231
 
232
- /* Visualizer presets */
233
- .visualizer-preset-selector {
234
- position: absolute;
235
- bottom: 5px;
236
- left: 10px;
237
- background: rgba(0, 0, 0, 0.7);
238
- border: 1px solid rgba(0, 255, 204, 0.3);
239
- border-radius: 20px;
240
- padding: 2px;
241
- display: flex;
242
- gap: 2px;
243
  }
244
 
245
- .preset-btn {
246
- width: 20px;
247
- height: 20px;
248
- border-radius: 50%;
249
- cursor: pointer;
250
- transition: all 0.2s ease;
251
- border: 2px solid transparent;
252
  }
253
 
254
- .preset-btn.active {
255
- border-color: white;
256
- transform: scale(1.1);
 
 
257
  }
258
- `;
259
- document.head.appendChild(style);
260
 
261
- // Create visualizer container
262
- createVisualizerContainer();
263
-
264
- // Add UI enhancements
265
- enhanceUIElements();
266
-
267
- // Setup audio analysis
268
- setupAudioAnalysis();
 
 
 
 
 
 
 
269
 
270
- console.log('✨ Spotify Enhanced UI/UX with Audio Visualizer loaded successfully!');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  }
272
 
 
273
  function createVisualizerContainer() {
274
  const container = document.createElement('div');
275
  container.id = 'visualizer-container';
 
 
276
  container.innerHTML = `
277
  <canvas id="audio-visualizer"></canvas>
278
  <div class="visualizer-controls">
279
  <div class="visualizer-btn" id="visualizer-toggle">❚❚</div>
280
  <div class="visualizer-btn" id="visualizer-fullscreen">⛶</div>
 
281
  </div>
282
- <div class="visualizer-preset-selector">
283
- <div class="preset-btn active" style="background: #1DB954;" data-preset="bars"></div>
284
- <div class="preset-btn" style="background: #8A2BE2;" data-preset="wave"></div>
285
- <div class="preset-btn" style="background: #FF4500;" data-preset="particles"></div>
286
- <div class="preset-btn" style="background: #1E90FF;" data-preset="circular"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  </div>
288
  `;
 
289
  document.body.appendChild(container);
 
290
 
291
  // Setup event listeners
292
- document.getElementById('visualizer-toggle').addEventListener('click', toggleVisualizer);
293
- document.getElementById('visualizer-fullscreen').addEventListener('click', toggleFullscreenVisualizer);
294
-
295
- document.querySelectorAll('.preset-btn').forEach(btn => {
296
- btn.addEventListener('click', function() {
297
- document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
298
- this.classList.add('active');
299
- setCurrentPreset(this.dataset.preset);
300
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  }
303
 
 
304
  function setupAudioAnalysis() {
305
- // Get the audio element (Spotify uses audio elements dynamically)
306
  const observer = new MutationObserver(mutations => {
307
  mutations.forEach(mutation => {
308
  if (mutation.addedNodes.length) {
@@ -320,7 +735,7 @@
320
  subtree: true
321
  });
322
 
323
- // Fallback: check periodically if audio element appears
324
  let audioCheckInterval = setInterval(() => {
325
  const audioElements = document.querySelectorAll('audio');
326
  if (audioElements.length > 0) {
@@ -331,6 +746,7 @@
331
  }, 1000);
332
  }
333
 
 
334
  function initializeVisualizer(audioElement) {
335
  const canvas = document.getElementById('audio-visualizer');
336
  const ctx = canvas.getContext('2d');
@@ -338,64 +754,47 @@
338
 
339
  // Set canvas dimensions
340
  function resizeCanvas() {
341
- canvas.width = container.clientWidth - 40;
342
- canvas.height = container.clientHeight - 20;
 
 
 
 
343
  }
344
 
345
  resizeCanvas();
346
  window.addEventListener('resize', resizeCanvas);
347
 
348
  // Create audio context and analyzer
349
- let audioContext, analyzer, source;
350
-
351
  try {
352
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
353
- analyzer = audioContext.createAnalyser();
 
354
  analyzer.fftSize = 256;
355
  analyzer.smoothingTimeConstant = 0.8;
356
 
357
- // Create source from audio element
358
- source = audioContext.createMediaElementSource(audioElement);
359
  source.connect(analyzer);
360
  analyzer.connect(audioContext.destination);
361
 
362
- // Create buffer for frequency data
363
  const bufferLength = analyzer.frequencyBinCount;
364
  const dataArray = new Uint8Array(bufferLength);
365
 
366
- // Visualizer settings
367
  let currentPreset = 'bars';
368
  let isPlaying = true;
369
- let fullscreenMode = false;
370
 
371
- function setCurrentPreset(preset) {
372
  currentPreset = preset;
373
  }
374
-
375
- function toggleVisualizer() {
376
- isPlaying = !isPlaying;
377
- document.getElementById('visualizer-toggle').textContent = isPlaying ? '❚❚' : '▶️';
378
- }
379
-
380
- function toggleFullscreenVisualizer() {
381
- fullscreenMode = !fullscreenMode;
382
- document.getElementById('visualizer-fullscreen').textContent = fullscreenMode ? '❐' : '⛶';
383
- container.style.height = fullscreenMode ? '300px' : '80px';
384
- container.style.opacity = fullscreenMode ? '0.95' : '0.9';
385
- resizeCanvas();
386
- }
387
 
388
- // Animation function
389
- function animate() {
390
  if (!isPlaying) {
391
- requestAnimationFrame(animate);
392
  return;
393
  }
394
 
395
- requestAnimationFrame(animate);
396
-
397
  analyzer.getByteFrequencyData(dataArray);
398
-
399
  ctx.clearRect(0, 0, canvas.width, canvas.height);
400
 
401
  // Draw based on current preset
@@ -413,35 +812,48 @@
413
  drawCircular(ctx, dataArray, canvas.width, canvas.height);
414
  break;
415
  }
 
 
416
  }
417
 
418
- animate();
419
 
 
 
 
 
 
 
 
 
420
  } catch (error) {
421
  console.error('Error initializing audio visualizer:', error);
422
- // Fallback visualizer if audio analysis fails
423
  createFallbackVisualizer(ctx, canvas.width, canvas.height);
424
  }
425
  }
426
 
 
427
  function drawBars(ctx, dataArray, width, height) {
428
  const barWidth = (width / dataArray.length) * 2.5;
429
  let x = 0;
 
430
 
431
  for (let i = 0; i < dataArray.length; i++) {
432
- const barHeight = (dataArray[i] / 255) * height * 1.2;
 
433
 
434
- // Gradient for bars
435
- const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight);
436
- gradient.addColorStop(0, 'rgba(0, 255, 204, 0.3)');
437
- gradient.addColorStop(1, 'rgba(0, 255, 204, 0.8)');
 
438
 
439
  ctx.fillStyle = gradient;
440
- ctx.fillRect(x, height - barHeight, barWidth, barHeight);
441
 
442
  // Glow effect
443
- ctx.shadowColor = 'rgba(0, 255, 204, 0.7)';
444
- ctx.shadowBlur = 15;
445
 
446
  x += barWidth + 1;
447
  }
@@ -450,12 +862,15 @@
450
  }
451
 
452
  function drawWave(ctx, dataArray, width, height) {
 
 
 
453
  ctx.beginPath();
454
- ctx.moveTo(0, height / 2);
455
 
456
  for (let i = 0; i < dataArray.length; i++) {
457
  const x = (i / dataArray.length) * width;
458
- const y = height / 2 + ((dataArray[i] - 128) / 128) * (height / 3);
459
 
460
  if (i === 0) {
461
  ctx.moveTo(x, y);
@@ -466,9 +881,9 @@
466
 
467
  // Gradient stroke
468
  const gradient = ctx.createLinearGradient(0, 0, width, 0);
469
- gradient.addColorStop(0, '#1DB954');
470
  gradient.addColorStop(0.5, '#00ffcc');
471
- gradient.addColorStop(1, '#1ED760');
472
 
473
  ctx.strokeStyle = gradient;
474
  ctx.lineWidth = 3;
@@ -487,37 +902,81 @@
487
 
488
  ctx.fillStyle = fillGradient;
489
  ctx.fill();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  }
491
 
492
  function drawParticles(ctx, dataArray, width, height) {
493
  const centerX = width / 2;
494
  const centerY = height / 2;
495
- const radius = Math.min(width, height) * 0.4;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
- for (let i = 0; i < dataArray.length; i += 4) {
 
498
  const angle = (i / dataArray.length) * Math.PI * 2;
499
- const distance = (dataArray[i] / 255) * radius;
500
  const x = centerX + Math.cos(angle) * distance;
501
  const y = centerY + Math.sin(angle) * distance;
502
  const size = (dataArray[i] / 255) * 8 + 2;
503
-
504
- // Particle color based on frequency
505
  const hue = (i / dataArray.length) * 360;
506
- ctx.fillStyle = `hsla(${hue}, 80%, 60%, ${dataArray[i] / 255})`;
507
 
508
  ctx.beginPath();
509
  ctx.arc(x, y, size, 0, Math.PI * 2);
 
510
  ctx.fill();
 
 
 
 
 
 
 
 
 
 
 
 
511
  }
512
 
513
  // Center glow
514
- const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius * 0.3);
515
  gradient.addColorStop(0, 'rgba(0, 255, 204, 0.8)');
516
  gradient.addColorStop(1, 'rgba(0, 255, 204, 0)');
517
 
518
  ctx.fillStyle = gradient;
519
  ctx.beginPath();
520
- ctx.arc(centerX, centerY, radius * 0.3, 0, Math.PI * 2);
521
  ctx.fill();
522
  }
523
 
@@ -527,104 +986,66 @@
527
  const maxRadius = Math.min(width, height) * 0.4;
528
 
529
  ctx.save();
 
 
 
 
 
530
 
531
  // Draw multiple rings
532
  for (let ring = 0; ring < 3; ring++) {
533
- const ringRadius = maxRadius * (1 - ring * 0.3);
534
 
535
  ctx.beginPath();
536
- ctx.arc(centerX, centerY, ringRadius, 0, Math.PI * 2);
537
 
538
- const gradient = ctx.createLinearGradient(0, 0, width, 0);
539
- gradient.addColorStop(0, '#1DB954');
540
  gradient.addColorStop(0.5, '#00ffcc');
541
- gradient.addColorStop(1, '#1ED760');
542
 
543
  ctx.strokeStyle = gradient;
544
- ctx.lineWidth = 3 + ring;
545
  ctx.stroke();
546
  }
547
 
548
  // Draw frequency bars around circle
549
  const barCount = 64;
550
- const barWidth = (Math.PI * 2) / barCount;
551
 
552
  for (let i = 0; i < barCount; i++) {
553
  const angle = (i / barCount) * Math.PI * 2;
554
  const barHeight = (dataArray[i % dataArray.length] / 255) * maxRadius * 0.3;
 
555
 
556
  ctx.save();
557
- ctx.translate(centerX, centerY);
558
  ctx.rotate(angle);
559
 
560
  const gradient = ctx.createLinearGradient(0, 0, 0, -barHeight);
561
- gradient.addColorStop(0, 'rgba(0, 255, 204, 0.3)');
562
- gradient.addColorStop(1, 'rgba(0, 255, 204, 0.8)');
563
 
564
  ctx.fillStyle = gradient;
565
- ctx.fillRect(0, -barHeight, 3, barHeight);
566
 
567
  ctx.restore();
568
  }
569
 
570
- ctx.restore();
571
- }
572
-
573
- function createFallbackVisualizer(ctx, width, height) {
574
- // Create animated gradient background if audio analysis fails
575
- let phase = 0;
576
 
577
- function animateFallback() {
578
- requestAnimationFrame(animateFallback);
579
-
580
- phase += 0.01;
581
-
582
- // Clear canvas
583
- ctx.clearRect(0, 0, width, height);
584
-
585
- // Create gradient
586
- const gradient = ctx.createLinearGradient(0, 0, width, height);
587
- gradient.addColorStop(0, `hsl(${(phase * 60) % 360}, 80%, 50%)`);
588
- gradient.addColorStop(0.5, `hsl(${(phase * 90 + 120) % 360}, 80%, 60%)`);
589
- gradient.addColorStop(1, `hsl(${(phase * 120 + 240) % 360}, 80%, 50%)`);
590
-
591
- ctx.fillStyle = gradient;
592
- ctx.fillRect(0, 0, width, height);
593
-
594
- // Draw animated bars
595
- const barCount = 32;
596
- const barWidth = width / barCount;
597
-
598
- for (let i = 0; i < barCount; i++) {
599
- const barHeight = Math.sin(phase + i * 0.2) * height * 0.4 + height * 0.3;
600
- const alpha = (Math.sin(phase + i * 0.1) + 1) * 0.5;
601
-
602
- ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.7})`;
603
- ctx.fillRect(i * barWidth, height - barHeight, barWidth - 2, barHeight);
604
- }
605
- }
606
 
607
- animateFallback();
608
  }
609
 
610
- function enhanceUIElements() {
611
- // Add hover effects and animations to various UI elements
612
- const elementsToEnhance = [
613
- '.Button-sc-1dqy6sn-0', // Buttons
614
- '.Card-sc-1uyc430-0', // Cards
615
- '.main-card-card', // Main cards
616
- '.main-shelf-shelf', // Shelves
617
- '.main-trackList-row', // Track rows
618
- '.progress-bar__slider', // Progress slider
619
- '.volume-bar__slider' // Volume slider
620
- ];
621
-
622
- elementsToEnhance.forEach(selector => {
623
- document.querySelectorAll(selector).forEach(element => {
624
- element.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
625
- });
626
- });
627
-
628
  // Add gradient text to important elements
629
  document.querySelectorAll('.Type__TypeElement-sc-goli3j-0').forEach(text => {
630
  if (text.textContent.length > 3 && text.textContent.length < 30) {
@@ -632,24 +1053,54 @@
632
  }
633
  });
634
 
635
- // Add pulse animation to play button
636
- const playButton = document.querySelector('.Button-sc-1dqy6sn-0.Lsg1a0PdZMXQ3vQZlE0_M');
637
- if (playButton) {
638
- playButton.style.animation = 'pulse 2s infinite';
639
- }
 
 
 
 
 
 
 
 
 
 
 
640
 
641
  // MutationObserver for dynamic content
642
  const uiObserver = new MutationObserver(mutations => {
643
  mutations.forEach(mutation => {
644
  if (mutation.addedNodes.length) {
645
  // Re-apply enhancements to new elements
646
- elementsToEnhance.forEach(selector => {
647
- document.querySelectorAll(selector).forEach(element => {
648
- if (!element.style.transition) {
649
- element.style.transition = 'all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1)';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
650
  }
651
  });
652
- });
653
  }
654
  });
655
  });
@@ -664,4 +1115,156 @@
664
  uiObserver.disconnect();
665
  });
666
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  })();
 
1
  // ==UserScript==
2
+ // @name Spotify Ultimate Mobile UI/UX Enhancer
3
  // @namespace https://violentmonkey.github.io/
4
+ // @version 3.0
5
+ // @description Transforms Spotify web player with stunning animations, advanced audio visualizer, and mobile-optimized UX that surpasses the native app
6
+ // @author UX Designer
7
  // @match https://open.spotify.com/*
8
  // @icon https://www.spotify.com/favicon.ico
9
  // @grant GM_addStyle
10
  // @grant GM_getValue
11
  // @grant GM_setValue
12
  // @run-at document-start
 
 
13
  // ==/UserScript==
14
 
15
  (function() {
16
  'use strict';
17
 
18
+ // Feature detection and initialization
19
+ const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
20
+ (window.innerWidth < 768);
21
+
22
+ // Mobile-specific features
23
+ const mobileFeatures = {
24
+ gesturesEnabled: true,
25
+ hapticFeedback: true,
26
+ adaptiveLayout: true,
27
+ touchOptimized: true,
28
+ swipeNavigation: true,
29
+ pullToRefresh: true
30
+ };
31
 
32
+ // Initialize the enhanced UI
33
  function initEnhancedSpotify() {
34
+ // Create style element
35
  const style = document.createElement('style');
36
+ style.textContent = getCSSStyles();
37
+ document.head.appendChild(style);
38
+
39
+ // Create visualizer container
40
+ createVisualizerContainer();
41
+
42
+ // Setup mobile gestures
43
+ if (isMobile && mobileFeatures.gesturesEnabled) {
44
+ setupMobileGestures();
45
+ }
46
+
47
+ // Setup audio analysis
48
+ setupAudioAnalysis();
49
+
50
+ // Apply UI enhancements
51
+ applyUIEnhancements();
52
+
53
+ // Setup responsive adjustments
54
+ setupResponsiveAdjustments();
55
+
56
+ console.log('✨ Spotify Ultimate Mobile UI/UX Enhancer loaded successfully!');
57
+ console.log(`📱 Mobile Mode: ${isMobile ? 'Enabled' : 'Disabled'}`);
58
+ }
59
+
60
+ // Get CSS styles with mobile optimization
61
+ function getCSSStyles() {
62
+ return `
63
+ /* Mobile-first base styles */
64
  :root {
65
  --primary-gradient: linear-gradient(45deg, #1DB954, #1ED760, #00ffcc);
66
  --secondary-gradient: linear-gradient(45deg, #8A2BE2, #9370DB, #BA55D3);
67
+ --accent-gradient: linear-gradient(45deg, #ff00cc, #3333ff, #00ffcc);
68
  --glow-color: rgba(0, 255, 204, 0.7);
69
+ --mobile-radius: 24px;
70
+ --desktop-radius: 12px;
71
  --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
72
  --transition-normal: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
73
  --transition-slow: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
74
+ --ease-out-back: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
75
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.15);
76
+ --shadow-md: 0 4px 15px rgba(0, 0, 0, 0.2);
77
+ --shadow-lg: 0 8px 30px rgba(0, 255, 204, 0.4);
78
+ --shadow-glow: 0 0 20px rgba(0, 255, 204, 0.5);
79
+ }
80
+
81
+ /* Mobile optimization */
82
+ @media (max-width: 768px) {
83
+ :root {
84
+ --mobile-radius: 28px;
85
+ --desktop-radius: 16px;
86
+ }
87
  }
88
 
89
+ /* Global enhancements */
90
+ * {
91
+ transition: var(--transition-fast);
92
+ }
93
+
94
+ /* Enhanced buttons - mobile optimized */
95
  button, .Button-sc-1dqy6sn-0, .ButtonInner-sc-1dqy6sn-1 {
96
  transition: var(--transition-normal) !important;
97
  transform: scale(1) !important;
98
  background: var(--primary-gradient) !important;
99
  background-size: 200% 200% !important;
100
  animation: gradientBG 3s ease infinite !important;
101
+ box-shadow: var(--shadow-sm) !important;
102
  border: none !important;
103
+ border-radius: 16px !important;
104
+ padding: 8px 16px !important;
105
+ font-weight: 600 !important;
106
+ will-change: transform, box-shadow !important;
107
+ }
108
+
109
+ button:active, .Button-sc-1dqy6sn-0:active, .ButtonInner-sc-1dqy6sn-1:active {
110
+ transform: scale(0.95) !important;
111
+ box-shadow: 0 2px 6px rgba(0, 255, 204, 0.3) !important;
112
  }
113
 
114
  button:hover, .Button-sc-1dqy6sn-0:hover, .ButtonInner-sc-1dqy6sn-1:hover {
115
+ transform: scale(1.05) !important;
116
+ box-shadow: var(--shadow-md) !important;
117
  filter: brightness(1.1) !important;
118
  }
119
 
120
+ /* Play button pulse */
121
+ .Button-sc-1dqy6sn-0.Lsg1a0PdZMXQ3vQZlE0_M {
122
+ animation: pulse 2s infinite !important;
123
+ position: relative !important;
124
+ z-index: 10 !important;
125
+ box-shadow: 0 0 0 rgba(0, 255, 204, 0.4) !important;
126
+ animation-name: pulse, gradientBG !important;
127
+ animation-duration: 2s, 3s !important;
128
+ animation-timing-function: ease, ease !important;
129
+ animation-iteration-count: infinite, infinite !important;
130
+ }
131
+
132
+ /* Card enhancements with mobile radius */
133
+ .Card-sc-1uyc430-0, .main-card-card, .main-shelf-shelf {
134
+ transition: var(--ease-out-back) !important;
135
  transform: translateY(0) !important;
136
+ box-shadow: var(--shadow-sm) !important;
137
+ border-radius: var(--mobile-radius) !important;
138
  overflow: hidden !important;
139
+ margin: 8px 0 !important;
140
+ will-change: transform, box-shadow !important;
141
+ background: rgba(255, 255, 255, 0.05) !important;
142
+ backdrop-filter: blur(10px) !important;
143
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
144
  }
145
 
146
+ .Card-sc-1uyc430-0:hover, .main-card-card:hover, .main-shelf-shelf:hover {
147
  transform: translateY(-8px) !important;
148
+ box-shadow: var(--shadow-lg) !important;
149
  z-index: 100 !important;
150
+ background: rgba(255, 255, 255, 0.08) !important;
151
  }
152
 
153
+ /* Album art with mobile optimization */
154
+ .cover-art-image, .main-image-image {
155
+ transition: var(--ease-out-back) !important;
 
156
  transform-origin: center !important;
157
+ border-radius: var(--mobile-radius) !important;
158
+ overflow: hidden !important;
159
+ will-change: transform, box-shadow !important;
160
  }
161
 
162
+ .cover-art-image:hover, .main-image-image:hover {
163
+ transform: scale(1.12) rotate(2deg) !important;
164
+ box-shadow: var(--shadow-glow) !important;
165
  z-index: 1000 !important;
166
  }
167
 
168
+ /* Mobile touch feedback */
169
+ [role="button"], [role="link"], .main-trackList-row {
170
+ touch-action: manipulation !important;
171
+ -webkit-tap-highlight-color: transparent !important;
172
+ }
173
+
174
+ [role="button"]:active, [role="link"]:active, .main-trackList-row:active {
175
+ transform: scale(0.98) !important;
176
+ opacity: 0.9 !important;
177
+ }
178
+
179
+ /* Now playing bar - mobile enhanced */
180
+ .now-playing-bar, .playback-bar {
181
  transition: var(--transition-slow) !important;
182
+ background: rgba(0, 0, 0, 0.9) !important;
183
+ backdrop-filter: blur(15px) !important;
184
  border-top: 1px solid rgba(0, 255, 204, 0.3) !important;
185
+ padding: 8px !important;
186
+ border-radius: var(--mobile-radius) var(--mobile-radius) 0 0 !important;
187
+ margin-bottom: env(safe-area-inset-bottom) !important;
188
  }
189
 
190
  .now-playing-bar:hover {
 
192
  box-shadow: 0 -10px 30px rgba(0, 255, 204, 0.3) !important;
193
  }
194
 
195
+ /* Visualizer container - mobile responsive */
196
  #visualizer-container {
197
  position: fixed;
198
  bottom: 0;
199
  left: 0;
200
  right: 0;
201
+ height: 120px;
202
+ background: rgba(0, 0, 0, 0.85);
203
+ backdrop-filter: blur(15px);
204
  z-index: 9999;
205
  display: flex;
206
+ flex-direction: column;
207
  justify-content: center;
208
  align-items: center;
209
+ padding: 0 16px;
210
  border-top: 1px solid rgba(0, 255, 204, 0.3);
211
+ opacity: 0.95;
212
+ transition: var(--transition-normal);
213
+ transform: translateY(0);
214
+ padding-bottom: calc(env(safe-area-inset-bottom) + 16px);
215
  }
216
 
217
+ #visualizer-container.minimized {
218
+ height: 60px;
219
+ }
220
+
221
+ #visualizer-container.collapsed {
222
+ transform: translateY(100%);
223
+ opacity: 0;
224
+ pointer-events: none;
225
  }
226
 
227
  /* Visualizer canvas */
 
229
  width: 100%;
230
  height: 60px;
231
  display: block;
232
+ border-radius: 12px;
233
+ overflow: hidden;
234
  }
235
 
236
+ /* Mobile controls */
237
  .visualizer-controls {
238
  position: absolute;
239
+ bottom: calc(env(safe-area-inset-bottom) + 16px);
240
+ right: 16px;
241
  display: flex;
242
+ gap: 12px;
243
  z-index: 10000;
244
+ flex-direction: row;
245
  }
246
 
247
  .visualizer-btn {
248
+ width: 48px;
249
+ height: 48px;
250
+ border-radius: 24px;
251
  background: rgba(255, 255, 255, 0.1);
252
+ border: 2px solid rgba(0, 255, 204, 0.5);
253
  color: white;
254
  display: flex;
255
  justify-content: center;
256
  align-items: center;
257
  cursor: pointer;
258
+ font-size: 18px;
259
  transition: all 0.2s ease;
260
+ touch-action: manipulation;
261
+ -webkit-tap-highlight-color: transparent;
262
+ }
263
+
264
+ .visualizer-btn:active {
265
+ transform: scale(0.9);
266
+ background: rgba(0, 255, 204, 0.2);
267
  }
268
 
269
  .visualizer-btn:hover {
270
+ background: rgba(0, 255, 204, 0.25);
271
+ transform: scale(1.05);
272
  }
273
 
274
+ /* Progress bar mobile optimization */
275
+ .progress-bar, .volume-bar {
276
+ height: 6px !important;
277
+ background: rgba(255, 255, 255, 0.15) !important;
278
+ border-radius: 3px !important;
279
  overflow: hidden !important;
280
  position: relative !important;
281
+ touch-action: manipulation !important;
282
  }
283
 
284
+ .progress-bar__slider, .volume-bar__slider {
285
+ width: 24px !important;
286
+ height: 24px !important;
287
+ background: var(--accent-gradient) !important;
288
+ border: 3px solid white !important;
289
+ box-shadow: 0 0 15px var(--glow-color) !important;
290
  transition: all 0.2s ease !important;
291
+ touch-action: manipulation !important;
292
  }
293
 
294
+ .progress-bar__slider:active, .volume-bar__slider:active {
295
+ transform: scale(1.3) !important;
296
+ box-shadow: 0 0 25px var(--glow-color) !important;
297
  }
298
 
299
+ /* Mobile navigation enhancements */
300
+ .main-navBar-navBar, .main-buddyFeed-buddyFeed {
301
+ background: rgba(0, 0, 0, 0.7) !important;
302
+ backdrop-filter: blur(10px) !important;
303
+ border-radius: var(--desktop-radius) !important;
304
+ margin: 8px !important;
305
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
 
 
 
 
 
306
  }
307
 
308
  /* Text animations */
 
317
 
318
  /* Gradient text */
319
  .glow-text {
320
+ background: var(--accent-gradient);
321
  -webkit-background-clip: text;
322
  -webkit-text-fill-color: transparent;
323
  text-shadow: 0 0 10px rgba(0, 255, 204, 0.3);
324
+ background-size: 200% 200%;
325
+ animation: gradientText 3s ease infinite;
326
+ }
327
+
328
+ /* Mobile gesture indicators */
329
+ .gesture-indicator {
330
+ position: fixed;
331
+ bottom: 20%;
332
+ left: 50%;
333
+ transform: translateX(-50%);
334
+ color: rgba(0, 255, 204, 0.8);
335
+ font-size: 14px;
336
+ padding: 8px 16px;
337
+ background: rgba(0, 0, 0, 0.7);
338
+ border-radius: 20px;
339
+ backdrop-filter: blur(5px);
340
+ opacity: 0;
341
+ transition: opacity 0.3s ease;
342
+ pointer-events: none;
343
+ z-index: 9999;
344
+ }
345
+
346
+ /* Mobile swipe areas */
347
+ .swipe-area {
348
+ position: fixed;
349
+ height: 100%;
350
+ width: 60px;
351
+ z-index: 9998;
352
+ touch-action: none;
353
+ }
354
+
355
+ .swipe-area.left {
356
+ left: 0;
357
+ background: linear-gradient(to right, rgba(0, 255, 204, 0.1) 0%, transparent 100%);
358
+ }
359
+
360
+ .swipe-area.right {
361
+ right: 0;
362
+ background: linear-gradient(to left, rgba(0, 255, 204, 0.1) 0%, transparent 100%);
363
+ }
364
+
365
+ /* Pulsing indicator for interactive elements */
366
+ .pulse-indicator {
367
+ position: absolute;
368
+ width: 100%;
369
+ height: 100%;
370
+ border-radius: inherit;
371
+ background: radial-gradient(circle, rgba(0, 255, 204, 0.4) 0%, transparent 70%);
372
+ opacity: 0;
373
+ pointer-events: none;
374
+ animation: pulseIndicator 0.6s ease-out;
375
+ }
376
+
377
+ /* Fullscreen visualizer overlay */
378
+ #fullscreen-visualizer-overlay {
379
+ position: fixed;
380
+ top: 0;
381
+ left: 0;
382
+ right: 0;
383
+ bottom: 0;
384
+ background: rgba(0, 0, 0, 0.95);
385
+ backdrop-filter: blur(5px);
386
+ z-index: 99999;
387
+ display: flex;
388
+ flex-direction: column;
389
+ justify-content: center;
390
+ align-items: center;
391
+ opacity: 0;
392
+ transform: scale(0.9);
393
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
394
+ pointer-events: none;
395
+ padding: 20px;
396
+ }
397
+
398
+ #fullscreen-visualizer-overlay.active {
399
+ opacity: 1;
400
+ transform: scale(1);
401
+ pointer-events: all;
402
+ }
403
+
404
+ #fullscreen-visualizer {
405
+ width: 100%;
406
+ max-width: 800px;
407
+ height: 60vh;
408
+ border-radius: 24px;
409
+ overflow: hidden;
410
+ box-shadow: 0 0 50px rgba(0, 255, 204, 0.5);
411
+ }
412
+
413
+ .fullscreen-controls {
414
+ margin-top: 20px;
415
+ display: flex;
416
+ gap: 15px;
417
  }
418
 
419
  /* Keyframes */
 
423
  100% { background-position: 0% 50% }
424
  }
425
 
426
+ @keyframes gradientText {
427
+ 0% { background-position: 0% 50% }
428
+ 50% { background-position: 100% 50% }
429
+ 100% { background-position: 0% 50% }
430
+ }
431
+
432
  @keyframes pulse {
433
  0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 255, 204, 0.4); }
434
+ 70% { transform: scale(1.05); box-shadow: 0 0 0 15px rgba(0, 255, 204, 0); }
435
  100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 255, 204, 0); }
436
  }
437
 
438
+ @keyframes pulseIndicator {
439
+ 0% { transform: scale(0.8); opacity: 0.7; }
440
+ 100% { transform: scale(1.2); opacity: 0; }
441
+ }
442
+
443
  @keyframes fadeIn {
444
  from { opacity: 0; transform: translateY(10px); }
445
  to { opacity: 1; transform: translateY(0); }
446
  }
447
 
448
+ @keyframes slideUp {
449
+ from { transform: translateY(100%); opacity: 0; }
450
+ to { transform: translateY(0); opacity: 1; }
 
 
 
 
 
 
 
 
451
  }
452
 
453
+ /* Mobile-specific animations */
454
+ .mobile-enter {
455
+ animation: slideUp 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards;
 
 
 
 
456
  }
457
 
458
+ /* Dark mode optimization */
459
+ @media (prefers-color-scheme: dark) {
460
+ .Card-sc-1uyc430-0, .main-card-card {
461
+ background: rgba(30, 30, 30, 0.8) !important;
462
+ }
463
  }
 
 
464
 
465
+ /* Reduce motion for accessibility */
466
+ @media (prefers-reduced-motion: reduce) {
467
+ * {
468
+ transition: none !important;
469
+ animation: none !important;
470
+ }
471
+ .Card-sc-1uyc430-0:hover, .main-card-card:hover {
472
+ transform: none !important;
473
+ box-shadow: var(--shadow-sm) !important;
474
+ }
475
+ .cover-art-image:hover, .main-image-image:hover {
476
+ transform: none !important;
477
+ box-shadow: none !important;
478
+ }
479
+ }
480
 
481
+ /* Mobile viewport optimization */
482
+ @media (max-width: 480px) {
483
+ #visualizer-container {
484
+ height: 100px;
485
+ padding: 0 12px;
486
+ padding-bottom: calc(env(safe-area-inset-bottom) + 12px);
487
+ }
488
+
489
+ .visualizer-btn {
490
+ width: 42px;
491
+ height: 42px;
492
+ font-size: 16px;
493
+ }
494
+
495
+ .progress-bar__slider, .volume-bar__slider {
496
+ width: 22px !important;
497
+ height: 22px !important;
498
+ }
499
+
500
+ .main-type-element {
501
+ font-size: 14px !important;
502
+ }
503
+ }
504
+ `;
505
  }
506
 
507
+ // Create visualizer container
508
  function createVisualizerContainer() {
509
  const container = document.createElement('div');
510
  container.id = 'visualizer-container';
511
+ container.className = isMobile ? 'mobile-enter' : '';
512
+
513
  container.innerHTML = `
514
  <canvas id="audio-visualizer"></canvas>
515
  <div class="visualizer-controls">
516
  <div class="visualizer-btn" id="visualizer-toggle">❚❚</div>
517
  <div class="visualizer-btn" id="visualizer-fullscreen">⛶</div>
518
+ <div class="visualizer-btn" id="visualizer-preset">🎨</div>
519
  </div>
520
+ <div class="gesture-indicator" id="gesture-indicator">Swipe up for more</div>
521
+ `;
522
+
523
+ // Add swipe areas for mobile
524
+ if (isMobile && mobileFeatures.swipeNavigation) {
525
+ const leftSwipe = document.createElement('div');
526
+ leftSwipe.className = 'swipe-area left';
527
+ leftSwipe.id = 'left-swipe-area';
528
+
529
+ const rightSwipe = document.createElement('div');
530
+ rightSwipe.className = 'swipe-area right';
531
+ rightSwipe.id = 'right-swipe-area';
532
+
533
+ document.body.appendChild(leftSwipe);
534
+ document.body.appendChild(rightSwipe);
535
+ }
536
+
537
+ // Create fullscreen overlay
538
+ const fullscreenOverlay = document.createElement('div');
539
+ fullscreenOverlay.id = 'fullscreen-visualizer-overlay';
540
+ fullscreenOverlay.innerHTML = `
541
+ <canvas id="fullscreen-visualizer"></canvas>
542
+ <div class="fullscreen-controls">
543
+ <div class="visualizer-btn" id="fullscreen-close">✕</div>
544
+ <div class="visualizer-btn" id="fullscreen-preset">🎨</div>
545
  </div>
546
  `;
547
+
548
  document.body.appendChild(container);
549
+ document.body.appendChild(fullscreenOverlay);
550
 
551
  // Setup event listeners
552
+ setupVisualizerEvents();
553
+ }
554
+
555
+ // Setup visualizer events
556
+ function setupVisualizerEvents() {
557
+ // Minimize/maximize on tap (mobile)
558
+ document.getElementById('visualizer-container').addEventListener('click', (e) => {
559
+ if (isMobile && !e.target.closest('.visualizer-btn')) {
560
+ const container = document.getElementById('visualizer-container');
561
+ container.classList.toggle('minimized');
562
+
563
+ // Show gesture indicator
564
+ const indicator = document.getElementById('gesture-indicator');
565
+ if (!container.classList.contains('minimized')) {
566
+ indicator.style.opacity = '1';
567
+ setTimeout(() => {
568
+ indicator.style.opacity = '0';
569
+ }, 2000);
570
+ }
571
+ }
572
+ }, { passive: true });
573
+
574
+ // Toggle visualizer
575
+ document.getElementById('visualizer-toggle').addEventListener('click', function() {
576
+ const container = document.getElementById('visualizer-container');
577
+ container.classList.toggle('collapsed');
578
+ this.textContent = container.classList.contains('collapsed') ? '▶️' : '❚❚';
579
  });
580
+
581
+ // Fullscreen visualizer
582
+ document.getElementById('visualizer-fullscreen').addEventListener('click', function() {
583
+ const overlay = document.getElementById('fullscreen-visualizer-overlay');
584
+ overlay.classList.add('active');
585
+ setupFullscreenVisualizer();
586
+ });
587
+
588
+ // Close fullscreen
589
+ document.getElementById('fullscreen-close').addEventListener('click', function() {
590
+ document.getElementById('fullscreen-visualizer-overlay').classList.remove('active');
591
+ });
592
+
593
+ // Preset cycling
594
+ let currentPreset = 0;
595
+ const presets = ['bars', 'wave', 'particles', 'circular'];
596
+
597
+ document.getElementById('visualizer-preset').addEventListener('click', function() {
598
+ currentPreset = (currentPreset + 1) % presets.length;
599
+ setCurrentVisualizerPreset(presets[currentPreset]);
600
+ showMobileToast(`Visualizer: ${presets[currentPreset].charAt(0).toUpperCase() + presets[currentPreset].slice(1)}`);
601
+ });
602
+
603
+ document.getElementById('fullscreen-preset').addEventListener('click', function() {
604
+ currentPreset = (currentPreset + 1) % presets.length;
605
+ setCurrentVisualizerPreset(presets[currentPreset]);
606
+ showMobileToast(`Fullscreen: ${presets[currentPreset].charAt(0).toUpperCase() + presets[currentPreset].slice(1)}`);
607
+ });
608
+ }
609
+
610
+ // Setup mobile gestures
611
+ function setupMobileGestures() {
612
+ let startY = 0;
613
+ let currentY = 0;
614
+ let isSwiping = false;
615
+
616
+ const container = document.getElementById('visualizer-container');
617
+ const indicator = document.getElementById('gesture-indicator');
618
+
619
+ // Touch start
620
+ container.addEventListener('touchstart', (e) => {
621
+ startY = e.touches[0].clientY;
622
+ currentY = startY;
623
+ isSwiping = true;
624
+ indicator.style.opacity = '0';
625
+ }, { passive: true });
626
+
627
+ // Touch move
628
+ container.addEventListener('touchmove', (e) => {
629
+ if (!isSwiping) return;
630
+
631
+ currentY = e.touches[0].clientY;
632
+ const deltaY = currentY - startY;
633
+
634
+ // Show indicator when swiping up
635
+ if (deltaY < -20 && !container.classList.contains('minimized')) {
636
+ indicator.style.opacity = '1';
637
+ } else if (deltaY > 20) {
638
+ indicator.style.opacity = '0';
639
+ }
640
+
641
+ // Pull to refresh effect
642
+ if (mobileFeatures.pullToRefresh && deltaY > 0 && container.classList.contains('minimized')) {
643
+ container.style.transform = `translateY(${deltaY/3}px)`;
644
+ }
645
+ }, { passive: true });
646
+
647
+ // Touch end
648
+ container.addEventListener('touchend', (e) => {
649
+ if (!isSwiping) return;
650
+
651
+ const deltaY = currentY - startY;
652
+
653
+ // Swipe up to expand
654
+ if (deltaY < -50 && container.classList.contains('minimized')) {
655
+ container.classList.remove('minimized');
656
+ if (mobileFeatures.hapticFeedback) triggerHapticFeedback('light');
657
+ }
658
+
659
+ // Swipe down to minimize
660
+ else if (deltaY > 50 && !container.classList.contains('minimized')) {
661
+ container.classList.add('minimized');
662
+ if (mobileFeatures.hapticFeedback) triggerHapticFeedback('light');
663
+ }
664
+
665
+ // Pull to refresh
666
+ else if (mobileFeatures.pullToRefresh && deltaY > 100 && container.classList.contains('minimized')) {
667
+ showMobileToast('Refreshing...');
668
+ if (mobileFeatures.hapticFeedback) triggerHapticFeedback('medium');
669
+ setTimeout(() => {
670
+ location.reload();
671
+ }, 1000);
672
+ }
673
+
674
+ container.style.transform = '';
675
+ isSwiping = false;
676
+ setTimeout(() => {
677
+ indicator.style.opacity = '0';
678
+ }, 1000);
679
+ }, { passive: true });
680
+
681
+ // Swipe navigation
682
+ if (mobileFeatures.swipeNavigation) {
683
+ setupSwipeNavigation();
684
+ }
685
+ }
686
+
687
+ // Setup swipe navigation
688
+ function setupSwipeNavigation() {
689
+ let touchStartX = 0;
690
+ let touchEndX = 0;
691
+
692
+ document.getElementById('left-swipe-area').addEventListener('touchstart', (e) => {
693
+ touchStartX = e.touches[0].clientX;
694
+ }, { passive: true });
695
+
696
+ document.getElementById('left-swipe-area').addEventListener('touchend', (e) => {
697
+ touchEndX = e.changedTouches[0].clientX;
698
+ if (touchStartX - touchEndX > 50) {
699
+ navigatePreviousTrack();
700
+ showMobileToast('⏮ Previous Track');
701
+ if (mobileFeatures.hapticFeedback) triggerHapticFeedback('light');
702
+ }
703
+ }, { passive: true });
704
+
705
+ document.getElementById('right-swipe-area').addEventListener('touchstart', (e) => {
706
+ touchStartX = e.touches[0].clientX;
707
+ }, { passive: true });
708
+
709
+ document.getElementById('right-swipe-area').addEventListener('touchend', (e) => {
710
+ touchEndX = e.changedTouches[0].clientX;
711
+ if (touchEndX - touchStartX > 50) {
712
+ navigateNextTrack();
713
+ showMobileToast('⏭ Next Track');
714
+ if (mobileFeatures.hapticFeedback) triggerHapticFeedback('light');
715
+ }
716
+ }, { passive: true });
717
  }
718
 
719
+ // Setup audio analysis
720
  function setupAudioAnalysis() {
 
721
  const observer = new MutationObserver(mutations => {
722
  mutations.forEach(mutation => {
723
  if (mutation.addedNodes.length) {
 
735
  subtree: true
736
  });
737
 
738
+ // Fallback check
739
  let audioCheckInterval = setInterval(() => {
740
  const audioElements = document.querySelectorAll('audio');
741
  if (audioElements.length > 0) {
 
746
  }, 1000);
747
  }
748
 
749
+ // Initialize visualizer
750
  function initializeVisualizer(audioElement) {
751
  const canvas = document.getElementById('audio-visualizer');
752
  const ctx = canvas.getContext('2d');
 
754
 
755
  // Set canvas dimensions
756
  function resizeCanvas() {
757
+ const dpr = window.devicePixelRatio || 1;
758
+ canvas.width = (container.clientWidth - 32) * dpr;
759
+ canvas.height = (container.clientHeight - 40) * dpr;
760
+ canvas.style.width = `${container.clientWidth - 32}px`;
761
+ canvas.style.height = `${container.clientHeight - 40}px`;
762
+ ctx.scale(dpr, dpr);
763
  }
764
 
765
  resizeCanvas();
766
  window.addEventListener('resize', resizeCanvas);
767
 
768
  // Create audio context and analyzer
 
 
769
  try {
770
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
771
+ const audioContext = new AudioContext();
772
+ const analyzer = audioContext.createAnalyser();
773
  analyzer.fftSize = 256;
774
  analyzer.smoothingTimeConstant = 0.8;
775
 
776
+ const source = audioContext.createMediaElementSource(audioElement);
 
777
  source.connect(analyzer);
778
  analyzer.connect(audioContext.destination);
779
 
 
780
  const bufferLength = analyzer.frequencyBinCount;
781
  const dataArray = new Uint8Array(bufferLength);
782
 
 
783
  let currentPreset = 'bars';
784
  let isPlaying = true;
785
+ let animationFrame;
786
 
787
+ function setCurrentVisualizerPreset(preset) {
788
  currentPreset = preset;
789
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
790
 
791
+ function drawVisualizer() {
 
792
  if (!isPlaying) {
793
+ animationFrame = requestAnimationFrame(drawVisualizer);
794
  return;
795
  }
796
 
 
 
797
  analyzer.getByteFrequencyData(dataArray);
 
798
  ctx.clearRect(0, 0, canvas.width, canvas.height);
799
 
800
  // Draw based on current preset
 
812
  drawCircular(ctx, dataArray, canvas.width, canvas.height);
813
  break;
814
  }
815
+
816
+ animationFrame = requestAnimationFrame(drawVisualizer);
817
  }
818
 
819
+ drawVisualizer();
820
 
821
+ // Cleanup on page unload
822
+ window.addEventListener('beforeunload', () => {
823
+ cancelAnimationFrame(animationFrame);
824
+ if (audioContext.state !== 'closed') {
825
+ audioContext.close();
826
+ }
827
+ });
828
+
829
  } catch (error) {
830
  console.error('Error initializing audio visualizer:', error);
 
831
  createFallbackVisualizer(ctx, canvas.width, canvas.height);
832
  }
833
  }
834
 
835
+ // Draw different visualizer types
836
  function drawBars(ctx, dataArray, width, height) {
837
  const barWidth = (width / dataArray.length) * 2.5;
838
  let x = 0;
839
+ const centerY = height / 2;
840
 
841
  for (let i = 0; i < dataArray.length; i++) {
842
+ const barHeight = (dataArray[i] / 255) * height * 0.8;
843
+ const hue = (i / dataArray.length) * 360;
844
 
845
+ // Create gradient for each bar
846
+ const gradient = ctx.createLinearGradient(0, centerY + barHeight/2, 0, centerY - barHeight/2);
847
+ gradient.addColorStop(0, `hsla(${hue}, 80%, 40%, 0.3)`);
848
+ gradient.addColorStop(0.5, `hsla(${hue}, 100%, 60%, 0.8)`);
849
+ gradient.addColorStop(1, `hsla(${hue}, 80%, 40%, 0.3)`);
850
 
851
  ctx.fillStyle = gradient;
852
+ ctx.fillRect(x, centerY - barHeight/2, barWidth, barHeight);
853
 
854
  // Glow effect
855
+ ctx.shadowColor = `hsla(${hue}, 100%, 60%, 0.7)`;
856
+ ctx.shadowBlur = 8;
857
 
858
  x += barWidth + 1;
859
  }
 
862
  }
863
 
864
  function drawWave(ctx, dataArray, width, height) {
865
+ const centerY = height / 2;
866
+ const amplitude = height * 0.4;
867
+
868
  ctx.beginPath();
869
+ ctx.moveTo(0, centerY);
870
 
871
  for (let i = 0; i < dataArray.length; i++) {
872
  const x = (i / dataArray.length) * width;
873
+ const y = centerY + ((dataArray[i] - 128) / 128) * amplitude;
874
 
875
  if (i === 0) {
876
  ctx.moveTo(x, y);
 
881
 
882
  // Gradient stroke
883
  const gradient = ctx.createLinearGradient(0, 0, width, 0);
884
+ gradient.addColorStop(0, '#ff00cc');
885
  gradient.addColorStop(0.5, '#00ffcc');
886
+ gradient.addColorStop(1, '#3333ff');
887
 
888
  ctx.strokeStyle = gradient;
889
  ctx.lineWidth = 3;
 
902
 
903
  ctx.fillStyle = fillGradient;
904
  ctx.fill();
905
+
906
+ // Particles on peaks
907
+ for (let i = 0; i < dataArray.length; i += 5) {
908
+ if (dataArray[i] > 200) {
909
+ const x = (i / dataArray.length) * width;
910
+ const y = centerY + ((dataArray[i] - 128) / 128) * amplitude;
911
+ const size = (dataArray[i] / 255) * 6 + 2;
912
+
913
+ ctx.beginPath();
914
+ ctx.arc(x, y, size, 0, Math.PI * 2);
915
+ ctx.fillStyle = `rgba(255, 255, 255, ${dataArray[i] / 255})`;
916
+ ctx.fill();
917
+ }
918
+ }
919
  }
920
 
921
  function drawParticles(ctx, dataArray, width, height) {
922
  const centerX = width / 2;
923
  const centerY = height / 2;
924
+ const maxRadius = Math.min(width, height) * 0.35;
925
+
926
+ // Background particles
927
+ for (let i = 0; i < 50; i++) {
928
+ const angle = Math.random() * Math.PI * 2;
929
+ const distance = Math.random() * maxRadius * 1.2;
930
+ const size = Math.random() * 3 + 1;
931
+ const hue = Math.random() * 360;
932
+
933
+ ctx.beginPath();
934
+ ctx.arc(
935
+ centerX + Math.cos(angle) * distance,
936
+ centerY + Math.sin(angle) * distance,
937
+ size,
938
+ 0,
939
+ Math.PI * 2
940
+ );
941
+ ctx.fillStyle = `hsla(${hue}, 80%, 60%, ${Math.random() * 0.3})`;
942
+ ctx.fill();
943
+ }
944
 
945
+ // Audio-reactive particles
946
+ for (let i = 0; i < dataArray.length; i += 3) {
947
  const angle = (i / dataArray.length) * Math.PI * 2;
948
+ const distance = (dataArray[i] / 255) * maxRadius;
949
  const x = centerX + Math.cos(angle) * distance;
950
  const y = centerY + Math.sin(angle) * distance;
951
  const size = (dataArray[i] / 255) * 8 + 2;
 
 
952
  const hue = (i / dataArray.length) * 360;
 
953
 
954
  ctx.beginPath();
955
  ctx.arc(x, y, size, 0, Math.PI * 2);
956
+ ctx.fillStyle = `hsla(${hue}, 80%, 70%, ${dataArray[i] / 255})`;
957
  ctx.fill();
958
+
959
+ // Trail effect
960
+ for (let j = 1; j < 5; j++) {
961
+ const trailSize = size * (1 - j/10);
962
+ const trailX = centerX + Math.cos(angle) * (distance - j * 5);
963
+ const trailY = centerY + Math.sin(angle) * (distance - j * 5);
964
+
965
+ ctx.beginPath();
966
+ ctx.arc(trailX, trailY, trailSize, 0, Math.PI * 2);
967
+ ctx.fillStyle = `hsla(${hue}, 80%, 70%, ${dataArray[i] / 255 * 0.3})`;
968
+ ctx.fill();
969
+ }
970
  }
971
 
972
  // Center glow
973
+ const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, maxRadius * 0.4);
974
  gradient.addColorStop(0, 'rgba(0, 255, 204, 0.8)');
975
  gradient.addColorStop(1, 'rgba(0, 255, 204, 0)');
976
 
977
  ctx.fillStyle = gradient;
978
  ctx.beginPath();
979
+ ctx.arc(centerX, centerY, maxRadius * 0.4, 0, Math.PI * 2);
980
  ctx.fill();
981
  }
982
 
 
986
  const maxRadius = Math.min(width, height) * 0.4;
987
 
988
  ctx.save();
989
+ ctx.translate(centerX, centerY);
990
+
991
+ // Rotate over time
992
+ const rotation = Date.now() / 2000;
993
+ ctx.rotate(rotation);
994
 
995
  // Draw multiple rings
996
  for (let ring = 0; ring < 3; ring++) {
997
+ const ringRadius = maxRadius * (1 - ring * 0.25);
998
 
999
  ctx.beginPath();
1000
+ ctx.arc(0, 0, ringRadius, 0, Math.PI * 2);
1001
 
1002
+ const gradient = ctx.createLinearGradient(-ringRadius, 0, ringRadius, 0);
1003
+ gradient.addColorStop(0, '#ff00cc');
1004
  gradient.addColorStop(0.5, '#00ffcc');
1005
+ gradient.addColorStop(1, '#3333ff');
1006
 
1007
  ctx.strokeStyle = gradient;
1008
+ ctx.lineWidth = 2 + ring;
1009
  ctx.stroke();
1010
  }
1011
 
1012
  // Draw frequency bars around circle
1013
  const barCount = 64;
 
1014
 
1015
  for (let i = 0; i < barCount; i++) {
1016
  const angle = (i / barCount) * Math.PI * 2;
1017
  const barHeight = (dataArray[i % dataArray.length] / 255) * maxRadius * 0.3;
1018
+ const hue = (i / barCount) * 360;
1019
 
1020
  ctx.save();
 
1021
  ctx.rotate(angle);
1022
 
1023
  const gradient = ctx.createLinearGradient(0, 0, 0, -barHeight);
1024
+ gradient.addColorStop(0, `hsla(${hue}, 80%, 40%, 0.3)`);
1025
+ gradient.addColorStop(1, `hsla(${hue}, 100%, 70%, 0.9)`);
1026
 
1027
  ctx.fillStyle = gradient;
1028
+ ctx.fillRect(0, -barHeight, 4, barHeight);
1029
 
1030
  ctx.restore();
1031
  }
1032
 
1033
+ // Center pulse
1034
+ const pulseSize = (Math.sin(Date.now() / 300) * 0.3 + 0.7) * 20;
1035
+ const pulseGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, pulseSize);
1036
+ pulseGradient.addColorStop(0, 'rgba(0, 255, 204, 0.8)');
1037
+ pulseGradient.addColorStop(1, 'rgba(0, 255, 204, 0)');
 
1038
 
1039
+ ctx.fillStyle = pulseGradient;
1040
+ ctx.beginPath();
1041
+ ctx.arc(0, 0, pulseSize, 0, Math.PI * 2);
1042
+ ctx.fill();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1043
 
1044
+ ctx.restore();
1045
  }
1046
 
1047
+ // Apply UI enhancements
1048
+ function applyUIEnhancements() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1049
  // Add gradient text to important elements
1050
  document.querySelectorAll('.Type__TypeElement-sc-goli3j-0').forEach(text => {
1051
  if (text.textContent.length > 3 && text.textContent.length < 30) {
 
1053
  }
1054
  });
1055
 
1056
+ // Add touch feedback to interactive elements
1057
+ document.querySelectorAll('[role="button"], [role="link"], .main-trackList-row').forEach(element => {
1058
+ element.addEventListener('touchstart', function(e) {
1059
+ if (!this.classList.contains('pulse-active')) {
1060
+ this.classList.add('pulse-active');
1061
+ const pulse = document.createElement('div');
1062
+ pulse.className = 'pulse-indicator';
1063
+ this.appendChild(pulse);
1064
+
1065
+ setTimeout(() => {
1066
+ pulse.remove();
1067
+ this.classList.remove('pulse-active');
1068
+ }, 600);
1069
+ }
1070
+ }, { passive: true });
1071
+ });
1072
 
1073
  // MutationObserver for dynamic content
1074
  const uiObserver = new MutationObserver(mutations => {
1075
  mutations.forEach(mutation => {
1076
  if (mutation.addedNodes.length) {
1077
  // Re-apply enhancements to new elements
1078
+ document.querySelectorAll('.Type__TypeElement-sc-goli3j-0').forEach(text => {
1079
+ if (text.textContent.length > 3 && text.textContent.length < 30 && !text.classList.contains('glow-text')) {
1080
+ text.classList.add('glow-text');
1081
+ }
1082
+ });
1083
+
1084
+ if (isMobile) {
1085
+ document.querySelectorAll('[role="button"], [role="link"], .main-trackList-row').forEach(element => {
1086
+ if (!element.hasAttribute('data-touch-listener')) {
1087
+ element.setAttribute('data-touch-listener', 'true');
1088
+ element.addEventListener('touchstart', function(e) {
1089
+ if (!this.classList.contains('pulse-active')) {
1090
+ this.classList.add('pulse-active');
1091
+ const pulse = document.createElement('div');
1092
+ pulse.className = 'pulse-indicator';
1093
+ this.appendChild(pulse);
1094
+
1095
+ setTimeout(() => {
1096
+ pulse.remove();
1097
+ this.classList.remove('pulse-active');
1098
+ }, 600);
1099
+ }
1100
+ }, { passive: true });
1101
  }
1102
  });
1103
+ }
1104
  }
1105
  });
1106
  });
 
1115
  uiObserver.disconnect();
1116
  });
1117
  }
1118
+
1119
+ // Setup responsive adjustments
1120
+ function setupResponsiveAdjustments() {
1121
+ function adjustLayout() {
1122
+ const isCurrentlyMobile = window.innerWidth < 768;
1123
+
1124
+ if (isCurrentlyMobile && !isMobile) {
1125
+ // Switch to mobile mode
1126
+ document.documentElement.style.setProperty('--mobile-radius', '28px');
1127
+ document.documentElement.style.setProperty('--desktop-radius', '16px');
1128
+ showMobileToast('📱 Mobile Mode Activated');
1129
+ } else if (!isCurrentlyMobile && isMobile) {
1130
+ // Switch to desktop mode
1131
+ document.documentElement.style.setProperty('--mobile-radius', '24px');
1132
+ document.documentElement.style.setProperty('--desktop-radius', '12px');
1133
+ showMobileToast('🖥 Desktop Mode Activated');
1134
+ }
1135
+ }
1136
+
1137
+ window.addEventListener('resize', adjustLayout, { passive: true });
1138
+ adjustLayout();
1139
+ }
1140
+
1141
+ // Mobile utility functions
1142
+ function triggerHapticFeedback(type = 'light') {
1143
+ if (navigator.vibrate) {
1144
+ switch(type) {
1145
+ case 'light':
1146
+ navigator.vibrate(10);
1147
+ break;
1148
+ case 'medium':
1149
+ navigator.vibrate(30);
1150
+ break;
1151
+ case 'heavy':
1152
+ navigator.vibrate(50);
1153
+ break;
1154
+ case 'success':
1155
+ navigator.vibrate([20, 10, 20]);
1156
+ break;
1157
+ case 'error':
1158
+ navigator.vibrate([50, 30, 50]);
1159
+ break;
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+ function showMobileToast(message, duration = 2000) {
1165
+ if (!isMobile) return;
1166
+
1167
+ let toast = document.getElementById('mobile-toast');
1168
+ if (!toast) {
1169
+ toast = document.createElement('div');
1170
+ toast.id = 'mobile-toast';
1171
+ toast.style.cssText = `
1172
+ position: fixed;
1173
+ bottom: 20%;
1174
+ left: 50%;
1175
+ transform: translateX(-50%);
1176
+ background: rgba(0, 0, 0, 0.85);
1177
+ color: white;
1178
+ padding: 12px 24px;
1179
+ border-radius: 24px;
1180
+ backdrop-filter: blur(10px);
1181
+ font-size: 16px;
1182
+ z-index: 99999;
1183
+ opacity: 0;
1184
+ transition: opacity 0.3s ease;
1185
+ border: 1px solid rgba(0, 255, 204, 0.3);
1186
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1187
+ `;
1188
+ document.body.appendChild(toast);
1189
+ }
1190
+
1191
+ toast.textContent = message;
1192
+ toast.style.opacity = '1';
1193
+
1194
+ setTimeout(() => {
1195
+ toast.style.opacity = '0';
1196
+ }, duration);
1197
+ }
1198
+
1199
+ function navigateNextTrack() {
1200
+ const nextBtn = document.querySelector('.main-skipForwardButton-button') ||
1201
+ document.querySelector('[aria-label="Next"]');
1202
+ if (nextBtn) nextBtn.click();
1203
+ }
1204
+
1205
+ function navigatePreviousTrack() {
1206
+ const prevBtn = document.querySelector('.main-skipBackButton-button') ||
1207
+ document.querySelector('[aria-label="Previous"]');
1208
+ if (prevBtn) prevBtn.click();
1209
+ }
1210
+
1211
+ function setupFullscreenVisualizer() {
1212
+ const canvas = document.getElementById('fullscreen-visualizer');
1213
+ const ctx = canvas.getContext('2d');
1214
+
1215
+ function resizeCanvas() {
1216
+ const dpr = window.devicePixelRatio || 1;
1217
+ canvas.width = (canvas.clientWidth) * dpr;
1218
+ canvas.height = (canvas.clientHeight) * dpr;
1219
+ ctx.scale(dpr, dpr);
1220
+ }
1221
+
1222
+ resizeCanvas();
1223
+ window.addEventListener('resize', resizeCanvas);
1224
+
1225
+ // Simple animation for fullscreen
1226
+ let animationFrame;
1227
+ const colors = ['#ff00cc', '#3333ff', '#00ffcc'];
1228
+ let colorIndex = 0;
1229
+
1230
+ function animate() {
1231
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1232
+
1233
+ // Draw gradient background
1234
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
1235
+ gradient.addColorStop(0, colors[colorIndex]);
1236
+ gradient.addColorStop(1, colors[(colorIndex + 1) % colors.length]);
1237
+ ctx.fillStyle = gradient;
1238
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1239
+
1240
+ // Draw some shapes
1241
+ for (let i = 0; i < 20; i++) {
1242
+ const x = Math.sin(Date.now() / 1000 + i) * (canvas.width / 4) + canvas.width / 2;
1243
+ const y = Math.cos(Date.now() / 1200 + i) * (canvas.height / 4) + canvas.height / 2;
1244
+ const size = Math.abs(Math.sin(Date.now() / 2000 + i)) * 30 + 10;
1245
+
1246
+ ctx.beginPath();
1247
+ ctx.arc(x, y, size, 0, Math.PI * 2);
1248
+ ctx.fillStyle = `rgba(255, 255, 255, ${0.3 + Math.abs(Math.sin(Date.now() / 1000 + i)) * 0.7})`;
1249
+ ctx.fill();
1250
+ }
1251
+
1252
+ colorIndex = (colorIndex + 0.01) % colors.length;
1253
+ animationFrame = requestAnimationFrame(animate);
1254
+ }
1255
+
1256
+ animate();
1257
+
1258
+ // Cleanup on close
1259
+ document.getElementById('fullscreen-close').addEventListener('click', () => {
1260
+ cancelAnimationFrame(animationFrame);
1261
+ }, { once: true });
1262
+ }
1263
+
1264
+ // Initialize the script
1265
+ if (document.readyState !== 'loading') {
1266
+ initEnhancedSpotify();
1267
+ } else {
1268
+ document.addEventListener('DOMContentLoaded', initEnhancedSpotify);
1269
+ }
1270
  })();