/ index.html
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> 6 <title>Anton Amapiano Mix</title> 7 <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='80' font-size='80'%3E%F0%9F%8E%B6%3C/text%3E%3C/svg%3E" /> 8 <style> 9 @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Outfit:wght@300;400;500&display=swap'); 10 11 * { margin: 0; padding: 0; box-sizing: border-box; } 12 13 body { 14 min-height: 100vh; 15 min-height: 100dvh; 16 background: #0a0a0a; 17 color: #e8e8f0; 18 font-family: 'Outfit', sans-serif; 19 display: flex; 20 flex-direction: column; 21 align-items: center; 22 justify-content: center; 23 overflow-x: hidden; 24 overflow-y: auto; 25 } 26 27 /* Fullscreen slideshow background */ 28 .slideshow { 29 position: fixed; 30 inset: 0; 31 z-index: 0; 32 } 33 34 .slideshow img { 35 position: absolute; 36 inset: 0; 37 width: 100%; 38 height: 100%; 39 object-fit: cover; 40 opacity: 0; 41 transition: opacity 2s ease-in-out; 42 } 43 44 .slideshow img.active { 45 opacity: 1; 46 } 47 48 /* Dark overlay so text is readable */ 49 .slideshow::after { 50 content: ''; 51 position: absolute; 52 inset: 0; 53 background: radial-gradient(ellipse at center, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.75) 100%); 54 } 55 56 .container { 57 width: 100%; 58 max-width: 560px; 59 padding: 27px 32px; 60 display: flex; 61 flex-direction: column; 62 align-items: center; 63 gap: 27px; 64 position: relative; 65 z-index: 1; 66 } 67 68 /* Thumbnail strip showing all images */ 69 .thumb-strip { 70 display: flex; 71 gap: 10px; 72 justify-content: center; 73 } 74 75 .thumb-strip img { 76 width: 120px; 77 height: 68px; 78 object-fit: cover; 79 border-radius: 8px; 80 opacity: 0.4; 81 transition: opacity 0.4s, box-shadow 0.4s; 82 cursor: pointer; 83 border: 2px solid transparent; 84 } 85 86 .thumb-strip img.active { 87 opacity: 1; 88 border-color: rgba(255, 165, 0, 0.7); 89 box-shadow: 0 0 16px rgba(255, 165, 0, 0.4); 90 } 91 92 .title-block { text-align: center; } 93 94 .title-block .subtitle { 95 font-family: 'Bebas Neue', sans-serif; 96 font-size: 35px; 97 font-weight: 400; 98 letter-spacing: 10px; 99 text-transform: uppercase; 100 color: rgba(255, 200, 150, 0.7); 101 margin-bottom: 5px; 102 } 103 104 .title-block .title { 105 font-family: 'Bebas Neue', sans-serif; 106 font-size: 98px; 107 font-weight: 400; 108 letter-spacing: 10px; 109 text-transform: uppercase; 110 background: linear-gradient(135deg, #fff 0%, #ffb347 50%, #ff6b6b 100%); 111 -webkit-background-clip: text; 112 -webkit-text-fill-color: transparent; 113 background-clip: text; 114 } 115 116 .progress-container { width: 100%; } 117 118 .progress-bar { 119 width: 100%; 120 height: 33px; 121 cursor: pointer; 122 position: relative; 123 display: flex; 124 align-items: center; 125 } 126 127 .progress-track { 128 width: 100%; 129 height: 14px; 130 background: rgba(255,255,255,0.08); 131 border-radius: 7px; 132 position: relative; 133 } 134 135 .progress-fill { 136 height: 100%; 137 border-radius: 7px; 138 background: linear-gradient(90deg, #ff6b6b, #ffb347, #ffd700); 139 width: 0%; 140 position: relative; 141 pointer-events: none; 142 } 143 144 .progress-bar.dragging .progress-fill, 145 .progress-bar.dragging .buffer-fill { transition: none; } 146 147 .progress-thumb { 148 position: absolute; 149 top: 50%; 150 right: -16px; 151 transform: translateY(-50%); 152 width: 33px; 153 height: 33px; 154 background: #fff; 155 border-radius: 50%; 156 opacity: 0; 157 transition: opacity 0.15s; 158 box-shadow: 0 0 8px rgba(255,165,0,0.6); 159 cursor: grab; 160 pointer-events: auto; 161 z-index: 2; 162 } 163 164 .progress-bar:hover .progress-thumb, 165 .progress-bar.dragging .progress-thumb { opacity: 1; } 166 167 .progress-bar.dragging .progress-thumb { cursor: grabbing; transform: translateY(-50%) scale(1.2); } 168 169 .buffer-fill { 170 position: absolute; 171 top: 0; 172 left: 0; 173 height: 100%; 174 border-radius: 7px; 175 background: rgba(255,255,255,0.06); 176 width: 0%; 177 } 178 179 .time-row { 180 display: flex; 181 justify-content: space-between; 182 margin-top: 10px; 183 font-size: 28px; 184 color: rgba(255,200,150,0.5); 185 font-variant-numeric: tabular-nums; 186 } 187 188 .controls { 189 display: flex; 190 align-items: center; 191 gap: 56px; 192 } 193 194 .btn { 195 background: none; 196 border: none; 197 color: #e8e8f0; 198 cursor: pointer; 199 display: flex; 200 align-items: center; 201 justify-content: center; 202 transition: color 0.15s, transform 0.1s; 203 } 204 205 .btn:hover { color: #ffb347; } 206 .btn:active { transform: scale(0.93); } 207 208 .btn-play { 209 width: 210px; 210 height: 210px; 211 background: linear-gradient(135deg, #ff6b6b, #ff8e53); 212 border-radius: 50%; 213 box-shadow: 0 5px 27px rgba(255,107,107,0.4); 214 transition: transform 0.15s, box-shadow 0.15s; 215 } 216 217 .btn-play svg { 218 width: 88px; 219 height: 88px; 220 } 221 222 .btn-play:hover { 223 transform: scale(1.05); 224 box-shadow: 0 6px 28px rgba(255,107,107,0.6); 225 color: #fff; 226 } 227 228 .volume-container { 229 display: flex; 230 flex-direction: column; 231 align-items: center; 232 gap: 6px; 233 } 234 235 .volume-slider { 236 -webkit-appearance: slider-vertical; 237 appearance: slider-vertical; 238 writing-mode: vertical-lr; 239 direction: rtl; 240 width: 6px; 241 height: 140px; 242 background: rgba(255,255,255,0.15); 243 border-radius: 2px; 244 outline: none; 245 cursor: pointer; 246 } 247 248 .volume-slider::-webkit-slider-thumb { 249 -webkit-appearance: none; 250 width: 24px; 251 height: 24px; 252 background: #ffb347; 253 border-radius: 50%; 254 cursor: pointer; 255 } 256 257 .volume-slider::-moz-range-thumb { 258 width: 24px; 259 height: 24px; 260 background: #ffb347; 261 border-radius: 50%; 262 cursor: pointer; 263 border: none; 264 } 265 266 .status-msg { 267 font-size: 16px; 268 color: rgba(255,200,150,0.4); 269 min-height: 21px; 270 } 271 272 .badges-bottom { 273 position: fixed; 274 bottom: 16px; 275 left: 0; 276 right: 0; 277 display: flex; 278 justify-content: center; 279 gap: 12px; 280 flex-wrap: wrap; 281 padding: 0 16px; 282 z-index: 10; 283 } 284 285 .badge { 286 display: flex; 287 align-items: center; 288 gap: 8px; 289 opacity: 0.85; 290 transition: opacity 0.2s; 291 background: rgba(0,0,0,0.5); 292 padding: 10px 24px; 293 border-radius: 28px; 294 backdrop-filter: blur(8px); 295 -webkit-backdrop-filter: blur(8px); 296 } 297 298 .badge:hover { opacity: 1; } 299 300 .badge a { 301 display: flex; 302 align-items: center; 303 gap: 8px; 304 color: rgba(255,220,180,0.9); 305 text-decoration: none; 306 font-family: 'Bebas Neue', sans-serif; 307 font-size: 24px; 308 font-weight: 400; 309 letter-spacing: 3px; 310 } 311 312 .badge svg { flex-shrink: 0; width: 36px; height: 36px; } 313 314 /* Mobile: show images as a banner instead of fullscreen background */ 315 @media (orientation: portrait) { 316 body { 317 justify-content: flex-start; 318 } 319 320 .slideshow { 321 position: relative; 322 width: 100%; 323 aspect-ratio: 16 / 9; 324 flex-shrink: 0; 325 } 326 327 .slideshow img { 328 object-fit: contain; 329 background: #0a0a0a; 330 } 331 332 .slideshow::after { 333 display: none; 334 } 335 336 .container { 337 max-width: 100%; 338 padding: 16px 16px; 339 gap: 20px; 340 } 341 342 .thumb-strip { 343 width: 100%; 344 gap: 6px; 345 } 346 347 .thumb-strip img { 348 flex: 1; 349 width: 0; 350 height: auto; 351 aspect-ratio: 16 / 9; 352 } 353 354 .title-block .subtitle { 355 font-size: 42px; 356 letter-spacing: 12px; 357 } 358 359 .title-block .title { 360 font-size: 110px; 361 letter-spacing: 14px; 362 } 363 364 .btn-play { 365 width: 240px; 366 height: 240px; 367 } 368 369 .btn-play svg { 370 width: 100px; 371 height: 100px; 372 } 373 374 .volume-slider { 375 height: 200px; 376 } 377 378 .volume-slider::-webkit-slider-thumb { 379 width: 36px; 380 height: 36px; 381 } 382 383 .volume-slider::-moz-range-thumb { 384 width: 36px; 385 height: 36px; 386 } 387 388 .volume-container svg { 389 width: 36px; 390 height: 36px; 391 } 392 393 .controls { 394 gap: 60px; 395 } 396 } 397 </style> 398 </head> 399 <body> 400 401 <!-- Fullscreen crossfading slideshow --> 402 <div class="slideshow" id="slideshow"> 403 <img src="afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png" class="active" alt="" /> 404 <img src="Afro-futuristic_amapiano_DJ_at_an_open-air_beach_caf_poolside_e1ec3f33-58d1-45ed-9d2d-c1eb98601923_0.png" alt="" /> 405 <img src="amapiano_dj_--chaos_10_--ar_169_--raw_--sref_2397594818_32016_df8faf50-173e-4474-9fe0-7db83d64dac4_0.png" alt="" /> 406 <img src="group_of_fashionable_African_friends_relaxing_on_loungers_bes_80ecbab7-2bd2-4100-ab32-14a456ca3939_0.png" alt="" /> 407 </div> 408 409 <div class="container"> 410 <div class="thumb-strip" id="thumbStrip"> 411 <img src="afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png" class="active" alt="" /> 412 <img src="Afro-futuristic_amapiano_DJ_at_an_open-air_beach_caf_poolside_e1ec3f33-58d1-45ed-9d2d-c1eb98601923_0.png" alt="" /> 413 <img src="amapiano_dj_--chaos_10_--ar_169_--raw_--sref_2397594818_32016_df8faf50-173e-4474-9fe0-7db83d64dac4_0.png" alt="" /> 414 <img src="group_of_fashionable_African_friends_relaxing_on_loungers_bes_80ecbab7-2bd2-4100-ab32-14a456ca3939_0.png" alt="" /> 415 </div> 416 417 <div class="title-block"> 418 <div class="subtitle">chillmix.eth</div> 419 <div class="title">Amapiano</div> 420 </div> 421 422 <div class="progress-container"> 423 <div class="progress-bar" id="progressBar"> 424 <div class="progress-track"> 425 <div class="buffer-fill" id="bufferFill"></div> 426 <div class="progress-fill" id="progressFill"><div class="progress-thumb" id="progressThumb"></div></div> 427 </div> 428 </div> 429 <div class="time-row"> 430 <span id="currentTime">0:00</span> 431 <span id="duration">60:00</span> 432 </div> 433 </div> 434 435 <div class="controls"> 436 <button class="btn btn-play" id="playBtn" title="Play"> 437 <svg id="iconPlay" width="50" height="50" viewBox="0 0 24 24" fill="currentColor"> 438 <polygon points="6,3 20,12 6,21"/> 439 </svg> 440 <svg id="iconPause" width="50" height="50" viewBox="0 0 24 24" fill="currentColor" style="display:none"> 441 <rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/> 442 </svg> 443 </button> 444 445 <div class="volume-container"> 446 <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 447 <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/> 448 <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/> 449 </svg> 450 <input type="range" class="volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="0.8" /> 451 </div> 452 </div> 453 454 <div class="status-msg" id="statusMsg"></div> 455 </div> 456 457 <div class="badges-bottom"> 458 <div class="badge"> 459 <a href="https://www.ethswarm.org" target="_blank" rel="noopener"> 460 <svg width="28" height="28" viewBox="0 0 1319.31 1341.67" fill="#F79E1B" xmlns="http://www.w3.org/2000/svg"><polygon opacity="0.45" points="1125.21 136.97 1125.21 293.82 985.79 215.4 985.79 58.54"/><polygon opacity="0.6" points="985.79 215.4 985.79 372.25 1125.21 293.82 1125.21 136.97"/><polygon opacity="0.3" points="67.5 1092.22 346.35 1249.07 625.21 1092.22 346.35 935.36"/><polygon opacity="0.45" points="346.35 935.36 346.35 1249.07 67.5 1092.22 67.5 778.51"/><polygon opacity="0.45" points="625.21 778.51 625.21 1092.22 346.35 935.36 346.35 621.65"/><polygon opacity="0.6" points="346.35 935.36 346.35 1249.07 625.21 1092.22 625.21 778.51"/><polygon opacity="0.6" points="67.5 778.51 67.5 1092.22 346.35 935.36 346.35 621.65"/><polygon opacity="0.8" points="654.65 407.11 794.08 485.53 933.5 407.11 794.08 328.68"/><polygon opacity="0.8" points="67.5 778.51 346.35 935.36 625.21 778.51 346.35 621.65"/><polygon opacity="0.3" points="691.46 1092.22 970.31 1249.07 1249.16 1092.22 970.31 935.36"/><polygon opacity="0.45" points="970.31 935.36 970.31 1249.07 691.46 1092.22 691.46 778.51"/><polygon opacity="0.45" points="1249.16 778.51 1249.16 1092.22 970.31 935.36 970.31 621.65"/><polygon opacity="0.6" points="970.31 935.36 970.31 1249.07 1249.16 1092.22 1249.16 778.51"/><polygon opacity="0.6" points="691.46 778.51 691.46 1092.22 970.31 935.36 970.31 621.65"/><polygon opacity="0.8" points="691.46 778.51 970.31 935.36 1249.16 778.51 970.31 621.65"/><polygon opacity="0.6" points="654.65 250.25 654.65 407.11 794.08 328.68 794.08 171.82"/><polygon opacity="0.3" points="375.8 563.96 654.65 720.81 933.5 563.96 654.65 407.11"/><polygon opacity="0.45" points="654.65 407.11 654.65 720.81 375.8 563.96 375.8 250.25"/><polygon opacity="0.6" points="375.8 250.25 375.8 563.96 654.65 407.11 654.65 93.4"/><polygon opacity="0.6" points="794.08 485.53 794.08 328.68 654.65 407.11 654.65 563.96 654.65 720.81 794.08 642.39 933.5 563.96 933.5 407.11"/><polygon opacity="0.45" points="794.08 171.82 654.65 93.4 654.65 407.11 933.5 563.96 933.5 407.11 794.08 328.68"/><polygon opacity="0.8" points="654.65 93.4 794.08 171.82 654.65 250.25 654.65 407.11 375.8 250.25"/><polygon opacity="0.8" points="846.36 136.97 985.79 215.4 1125.21 136.97 985.79 58.54"/></svg> 461 Hosted on Swarm 462 </a> 463 </div> 464 <div class="badge"> 465 <a href="https://app.ens.domains/chillmix.eth" target="_blank" rel="noopener"> 466 <svg width="28" height="28" viewBox="0 0 80 92" fill="#5298FF" xmlns="http://www.w3.org/2000/svg"><path d="M38.9515 0.848681L13.7395 42.3331C13.5418 42.6584 13.0824 42.6947 12.8372 42.4035C10.6176 39.7681 2.3486 28.5562 12.5807 18.3372C21.9175 9.01238 33.8099 2.36398 38.2174 0.0661478C38.7175 -0.194552 39.2442 0.367137 38.9515 0.848681Z"/><path d="M37.5575 91.1735C38.0606 91.5257 38.6807 90.9251 38.3434 90.4122C32.712 81.8469 13.9923 53.3481 11.4064 49.0701C8.85586 44.8504 3.83929 37.8378 3.4208 31.8381C3.37904 31.2391 2.55081 31.1175 2.34247 31.6808C2.00646 32.5892 1.64873 33.6734 1.31533 34.912C-2.89355 50.5471 3.21904 67.138 16.4943 76.4302L37.5575 91.1736V91.1735Z"/><path d="M41.0206 90.4359L66.2325 48.9514C66.4303 48.6261 66.8897 48.5899 67.1349 48.8811C69.3545 51.5165 77.6235 62.7284 67.3914 72.9473C58.0546 82.2722 46.1622 88.9206 41.7547 91.2184C41.2547 91.4791 40.7279 90.9174 41.0206 90.4359Z"/><path d="M42.4425 0.105463C41.9394 -0.246724 41.3194 0.35389 41.6566 0.866798C47.288 9.43208 66.0077 37.9309 68.5936 42.2089C71.1442 46.4286 76.1607 53.4412 76.5792 59.4409C76.621 60.0398 77.4492 60.1614 77.6576 59.5982C77.9936 58.6898 78.3513 57.6056 78.6847 56.367C82.8936 40.7319 76.781 24.141 63.5057 14.8488L42.4425 0.105463Z"/></svg> 467 Named by ENS 468 </a> 469 </div> 470 <div class="badge"> 471 <a href="https://app.safe.global/home?safe=eth:0x44E4A48f65DB6503819259bFe0b73501a5A3a532" target="_blank" rel="noopener"> 472 <svg width="28" height="28" viewBox="0 0 24 24" fill="#12FF80" xmlns="http://www.w3.org/2000/svg"><path d="M21.757 11.998h-2.485c-.742 0-1.343.579-1.343 1.293v3.47c0 .714-.602 1.293-1.344 1.293H6.699c-.743 0-1.344.578-1.344 1.292v2.391c0 .714.601 1.293 1.344 1.293h10.458c.742 0 1.335-.579 1.335-1.293V19.82c0-.714.602-1.22 1.344-1.22h1.92c.743 0 1.344-.58 1.344-1.293v-4.03c0-.714-.601-1.278-1.343-1.278Z"/><path d="M5.355 7.249c0-.714.6-1.293 1.343-1.293h9.88c.743 0 1.344-.579 1.344-1.293v-2.39c0-.714-.601-1.293-1.344-1.293H6.125c-.742 0-1.343.579-1.343 1.293v1.842c0 .714-.602 1.292-1.344 1.292H1.526C.784 5.407.182 5.986.182 6.7v4.034c0 .714.604 1.264 1.346 1.264h2.485c.743 0 1.344-.579 1.344-1.293L5.355 7.25Z"/><path d="M10.472 9.485h2.387c.778 0 1.409.608 1.409 1.356v2.296c0 .748-.632 1.356-1.41 1.356h-2.386c-.778 0-1.409-.608-1.409-1.356v-2.296c0-.749.632-1.356 1.409-1.356Z"/></svg> 473 Governed by Safe 474 </a> 475 </div> 476 <div class="badge"> 477 <a href="https://app.radicle.xyz/nodes/seed.radicle.garden/rad:zhtbBPVoU1fnid91fX9i22tumbPF" target="_blank" rel="noopener"> 478 <svg width="28" height="28" viewBox="0 0 44 44" fill="#825AC7" xmlns="http://www.w3.org/2000/svg"><g shape-rendering="crispEdges"><rect x="8" y="0" width="4" height="4"/><rect x="32" y="0" width="4" height="4"/><rect x="12" y="4" width="4" height="4"/><rect x="28" y="4" width="4" height="4"/><rect x="12" y="8" width="4" height="4"/><rect x="16" y="8" width="4" height="4"/><rect x="20" y="8" width="4" height="4"/><rect x="24" y="8" width="4" height="4"/><rect x="28" y="8" width="4" height="4"/><rect x="8" y="12" width="4" height="4"/><rect x="12" y="12" width="4" height="4"/><rect x="16" y="12" width="4" height="4"/><rect x="20" y="12" width="4" height="4"/><rect x="24" y="12" width="4" height="4"/><rect x="28" y="12" width="4" height="4"/><rect x="32" y="12" width="4" height="4"/><rect x="4" y="16" width="4" height="4"/><rect x="8" y="16" width="4" height="4"/><rect x="20" y="16" width="4" height="4"/><rect x="24" y="16" width="4" height="4"/><rect x="36" y="16" width="4" height="4"/><rect x="4" y="20" width="4" height="4"/><rect x="8" y="20" width="4" height="4"/><rect x="20" y="20" width="4" height="4"/><rect x="24" y="20" width="4" height="4"/><rect x="36" y="20" width="4" height="4"/><rect x="0" y="24" width="4" height="4"/><rect x="4" y="24" width="4" height="4"/><rect x="8" y="24" width="4" height="4"/><rect x="12" y="24" width="4" height="4"/><rect x="16" y="24" width="4" height="4"/><rect x="20" y="24" width="4" height="4"/><rect x="24" y="24" width="4" height="4"/><rect x="28" y="24" width="4" height="4"/><rect x="32" y="24" width="4" height="4"/><rect x="36" y="24" width="4" height="4"/><rect x="40" y="24" width="4" height="4"/><rect x="8" y="28" width="4" height="4"/><rect x="16" y="28" width="4" height="4"/><rect x="24" y="28" width="4" height="4"/><rect x="32" y="28" width="4" height="4"/><rect x="8" y="32" width="4" height="4"/><rect x="16" y="32" width="4" height="4"/><rect x="24" y="32" width="4" height="4"/><rect x="32" y="32" width="4" height="4"/><rect x="16" y="36" width="4" height="4"/><rect x="24" y="36" width="4" height="4"/><rect x="12" y="40" width="4" height="4"/><rect x="16" y="40" width="4" height="4"/><rect x="24" y="40" width="4" height="4"/><rect x="28" y="40" width="4" height="4"/></g></svg> 479 Source on Radicle 480 </a> 481 </div> 482 </div> 483 484 <audio id="audio" preload="auto" src="AMAPIANO-Mix01d - 60min.mp3"></audio> 485 486 <script> 487 const $ = id => document.getElementById(id); 488 const audio = $('audio'); 489 const playBtn = $('playBtn'); 490 const iconPlay = $('iconPlay'); 491 const iconPause = $('iconPause'); 492 const progressBar = $('progressBar'); 493 const progressFill = $('progressFill'); 494 const bufferFill = $('bufferFill'); 495 const currentTimeEl = $('currentTime'); 496 const durationEl = $('duration'); 497 const volumeSlider = $('volumeSlider'); 498 const statusMsg = $('statusMsg'); 499 500 const STORAGE_KEY = 'anton-amapiano-position'; 501 const KNOWN_DURATION = 3600; 502 function getDuration() { return audio.duration && isFinite(audio.duration) ? audio.duration : KNOWN_DURATION; } 503 504 function fmt(s) { 505 if (!s || !isFinite(s)) return '0:00'; 506 const h = Math.floor(s / 3600); 507 const m = Math.floor((s % 3600) / 60); 508 const sec = Math.floor(s % 60); 509 if (h > 0) return h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0'); 510 return m + ':' + String(sec).padStart(2,'0'); 511 } 512 513 // --- Slideshow --- 514 const slides = document.querySelectorAll('#slideshow img'); 515 const thumbs = document.querySelectorAll('#thumbStrip img'); 516 let currentSlide = 0; 517 let slideshowTimer; 518 519 function showSlide(index) { 520 slides[currentSlide].classList.remove('active'); 521 thumbs[currentSlide].classList.remove('active'); 522 currentSlide = index % slides.length; 523 slides[currentSlide].classList.add('active'); 524 thumbs[currentSlide].classList.add('active'); 525 } 526 527 function nextSlide() { 528 showSlide(currentSlide + 1); 529 } 530 531 function resetSlideshowTimer() { 532 clearInterval(slideshowTimer); 533 slideshowTimer = setInterval(nextSlide, 60000); 534 } 535 536 // Click thumbnails to jump to that slide 537 thumbs.forEach((thumb, i) => { 538 thumb.addEventListener('click', () => { 539 showSlide(i); 540 resetSlideshowTimer(); 541 }); 542 }); 543 544 resetSlideshowTimer(); 545 546 // --- Position save/restore --- 547 function savePosition() { 548 if (audio.currentTime > 0) { 549 localStorage.setItem(STORAGE_KEY, JSON.stringify({ 550 time: audio.currentTime, 551 updated: Date.now() 552 })); 553 } 554 } 555 556 function getSavedPosition() { 557 try { 558 const d = JSON.parse(localStorage.getItem(STORAGE_KEY)); 559 return d && d.time > 5 ? d.time : 0; 560 } catch(e) { return 0; } 561 } 562 563 function setStatus(msg) { 564 statusMsg.textContent = msg; 565 console.log('[audio]', msg); 566 } 567 568 ['loadstart','loadeddata','loadedmetadata','canplay','canplaythrough','playing','waiting','stalled','suspend','error','abort','emptied','ended'].forEach(evt => { 569 audio.addEventListener(evt, () => { 570 const info = evt + ' | readyState=' + audio.readyState + ' | networkState=' + audio.networkState; 571 console.log('[audio event]', info); 572 if (evt === 'error' && audio.error) { 573 setStatus('Error: ' + audio.error.message + ' (code ' + audio.error.code + ')'); 574 } 575 if (evt === 'waiting' || evt === 'stalled') setStatus('Buffering...'); 576 if (evt === 'playing') setStatus(''); 577 }); 578 }); 579 580 const saved = getSavedPosition(); 581 if (saved > 0) { 582 function restorePosition() { 583 audio.currentTime = saved; 584 currentTimeEl.textContent = fmt(saved); 585 progressFill.style.width = (saved / getDuration()) * 100 + '%'; 586 } 587 if (audio.readyState >= 2) { 588 restorePosition(); 589 } else { 590 currentTimeEl.textContent = fmt(saved); 591 progressFill.style.width = (saved / getDuration()) * 100 + '%'; 592 audio.addEventListener('loadedmetadata', restorePosition, { once: true }); 593 } 594 } 595 596 // --- Play / Pause --- 597 function updatePlayIcon() { 598 iconPlay.style.display = audio.paused ? 'block' : 'none'; 599 iconPause.style.display = audio.paused ? 'none' : 'block'; 600 } 601 602 function doPlay() { 603 const p = audio.play(); 604 if (p && p.catch) p.catch(err => setStatus('Play failed: ' + err.message)); 605 } 606 607 playBtn.addEventListener('click', () => { 608 if (audio.paused) { 609 setStatus('Starting playback...'); 610 doPlay(); 611 } else { 612 audio.pause(); 613 } 614 }); 615 616 audio.addEventListener('play', updatePlayIcon); 617 audio.addEventListener('pause', updatePlayIcon); 618 619 // --- Duration --- 620 audio.addEventListener('loadedmetadata', () => { 621 durationEl.textContent = fmt(getDuration()); 622 setStatus('Ready'); 623 }); 624 625 // --- Progress + buffer --- 626 audio.addEventListener('timeupdate', () => { 627 const dur = getDuration(); 628 progressFill.style.width = (audio.currentTime / dur) * 100 + '%'; 629 currentTimeEl.textContent = fmt(audio.currentTime); 630 }); 631 632 audio.addEventListener('progress', () => { 633 if (audio.buffered.length > 0) { 634 const end = audio.buffered.end(audio.buffered.length - 1); 635 bufferFill.style.width = (end / getDuration()) * 100 + '%'; 636 } 637 }); 638 639 // --- Seek --- 640 let dragging = false; 641 642 function seekTo(clientX) { 643 const rect = progressBar.getBoundingClientRect(); 644 const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)); 645 const time = pct * getDuration(); 646 audio.currentTime = time; 647 progressFill.style.width = pct * 100 + '%'; 648 currentTimeEl.textContent = fmt(time); 649 } 650 651 progressBar.addEventListener('mousedown', (e) => { 652 dragging = true; 653 progressBar.classList.add('dragging'); 654 seekTo(e.clientX); 655 e.preventDefault(); 656 }); 657 658 document.addEventListener('mousemove', (e) => { if (dragging) seekTo(e.clientX); }); 659 document.addEventListener('mouseup', () => { if (dragging) { dragging = false; progressBar.classList.remove('dragging'); } }); 660 661 progressBar.addEventListener('touchstart', (e) => { 662 dragging = true; 663 progressBar.classList.add('dragging'); 664 seekTo(e.touches[0].clientX); 665 }, { passive: true }); 666 667 document.addEventListener('touchmove', (e) => { if (dragging) seekTo(e.touches[0].clientX); }, { passive: true }); 668 document.addEventListener('touchend', () => { if (dragging) { dragging = false; progressBar.classList.remove('dragging'); } }); 669 670 // --- Keyboard skip --- 671 document.addEventListener('keydown', (e) => { 672 if (e.key === 'ArrowLeft') { audio.currentTime = Math.max(0, audio.currentTime - 15); e.preventDefault(); } 673 if (e.key === 'ArrowRight') { audio.currentTime = Math.min(getDuration(), audio.currentTime + 30); e.preventDefault(); } 674 }); 675 676 // --- Volume --- 677 audio.volume = 0.8; 678 volumeSlider.addEventListener('input', () => { audio.volume = parseFloat(volumeSlider.value); }); 679 680 // --- Auto-save --- 681 setInterval(savePosition, 2000); 682 window.addEventListener('beforeunload', savePosition); 683 document.addEventListener('visibilitychange', savePosition); 684 audio.addEventListener('pause', savePosition); 685 686 // --- Media Session API --- 687 if ('mediaSession' in navigator) { 688 navigator.mediaSession.metadata = new MediaMetadata({ 689 title: 'Amapiano Chill Mix', 690 artist: 'Anton', 691 album: '60 Minute Mix', 692 artwork: [{ src: 'afrobeats_megastar_android_--chaos_10_--ar_169_--raw_--sref_3_b5460a51-3822-4345-ad8a-4b2b8b05c55d_1.png', sizes: '1456x816', type: 'image/png' }] 693 }); 694 695 navigator.mediaSession.setActionHandler('play', () => doPlay()); 696 navigator.mediaSession.setActionHandler('pause', () => audio.pause()); 697 navigator.mediaSession.setActionHandler('seekbackward', () => { audio.currentTime = Math.max(0, audio.currentTime - 15); }); 698 navigator.mediaSession.setActionHandler('seekforward', () => { audio.currentTime = Math.min(getDuration(), audio.currentTime + 30); }); 699 navigator.mediaSession.setActionHandler('seekto', (d) => { audio.currentTime = d.seekTime; }); 700 701 audio.addEventListener('timeupdate', () => { 702 navigator.mediaSession.setPositionState({ 703 duration: getDuration(), 704 playbackRate: audio.playbackRate, 705 position: audio.currentTime, 706 }); 707 }); 708 } 709 </script> 710 </body> 711 </html>