Player-pwa-js-2

JavaScript ParaNerd 1 Views Size: 16.36 KB Posted on: Dec 10, 25 @ 9:17 PM
  1. 1constructor(container, config) {
  2. 2
  3. 3 this.container = container;
  4. 4
  5. 5 this.config = config;
  6. 6
  7. 7 this.audio = null;
  8. 8
  9. 9 this.isPlaying = false;
  10. 10
  11. 11 this.currentVolume = 1;
  12. 12
  13. 13 this.elapsed = 0;
  14. 14
  15. 15 this.metadata = null;
  16. 16
  17. 17 this.serviceWorker = null;
  18. 18
  19. 19 this.isInitialized = false;
  20. 20
  21. 21 this.retryCount = 0;
  22. 22
  23. 23 this.maxRetries = 3;
  24. 24
  25. 25 this.parsedHistory = null;
  26. 26
  27. 27 this.lastMetadataFetch = 0;
  28. 28
  29. 29 this.metadataInterval = null;
  30. 30
  31. 31 this.elapsedInterval = null;
  32. 32
  33. 33 this.deferredPrompt = null;
  34. 34
  35. 35
  36. 36
  37. 37 this.init();
  38. 38
  39. 39 }
  40. 40
  41. 41 async init() {
  42. 42
  43. 43 console.log('Initializing Persistent Radio Player PWA...');
  44. 44
  45. 45
  46. 46
  47. 47 // Setup PWA features
  48. 48
  49. 49 await this.setupPWA();
  50. 50
  51. 51
  52. 52
  53. 53 // Register enhanced service worker
  54. 54
  55. 55 await this.registerServiceWorker();
  56. 56
  57. 57
  58. 58
  59. 59 // Setup audio with persistence
  60. 60
  61. 61 this.setupPersistentAudio();
  62. 62
  63. 63 this.setupElements();
  64. 64
  65. 65 this.attachEventListeners();
  66. 66
  67. 67 this.setupServiceWorkerCommunication();
  68. 68
  69. 69 this.setupMediaSession();
  70. 70
  71. 71 this.setupWebAudio();
  72. 72
  73. 73
  74. 74
  75. 75 // Load previous state
  76. 76
  77. 77 await this.loadAudioState();
  78. 78
  79. 79
  80. 80
  81. 81 // Start metadata and time tracking
  82. 82
  83. 83 this.startMetadataPolling();
  84. 84
  85. 85 this.startTimeTracking();
  86. 86
  87. 87
  88. 88
  89. 89 // Setup PWA install prompt
  90. 90
  91. 91 this.setupInstallPrompt();
  92. 92
  93. 93
  94. 94
  95. 95 this.isInitialized = true;
  96. 96
  97. 97 console.log('Persistent Radio Player PWA initialized successfully');
  98. 98
  99. 99 }
  100. 100
  101. 101 async setupPWA() {
  102. 102
  103. 103 // Check if running as PWA
  104. 104
  105. 105 this.isPWA = window.matchMedia('(display-mode: standalone)').matches ||
  106. 106
  107. 107 window.navigator.standalone === true;
  108. 108
  109. 109
  110. 110
  111. 111 if (this.isPWA) {
  112. 112
  113. 113 console.log('Running as PWA');
  114. 114
  115. 115 document.body.classList.add('pwa-mode');
  116. 116
  117. 117 }
  118. 118
  119. 119
  120. 120
  121. 121 // Handle PWA display mode changes
  122. 122
  123. 123 window.matchMedia('(display-mode: standalone)').addEventListener('change', (e) => {
  124. 124
  125. 125 if (e.matches) {
  126. 126
  127. 127 document.body.classList.add('pwa-mode');
  128. 128
  129. 129 } else {
  130. 130
  131. 131 document.body.classList.remove('pwa-mode');
  132. 132
  133. 133 }
  134. 134
  135. 135 });
  136. 136
  137. 137 }
  138. 138
  139. 139 async registerServiceWorker() {
  140. 140
  141. 141 if ('serviceWorker' in navigator) {
  142. 142
  143. 143 try {
  144. 144
  145. 145 const registration = await navigator.serviceWorker.register(
  146. 146
  147. 147 this.config.serviceWorkerUrl,
  148. 148
  149. 149 { scope: '/' }
  150. 150
  151. 151 );
  152. 152
  153. 153 this.serviceWorker = registration;
  154. 154
  155. 155 console.log('PWA Service Worker registered successfully:', registration);
  156. 156
  157. 157
  158. 158
  159. 159 // Handle service worker updates
  160. 160
  161. 161 registration.addEventListener('updatefound', () => {
  162. 162
  163. 163 const newWorker = registration.installing;
  164. 164
  165. 165 newWorker.addEventListener('statechange', () => {
  166. 166
  167. 167 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
  168. 168
  169. 169 this.showUpdateAvailable();
  170. 170
  171. 171 }
  172. 172
  173. 173 });
  174. 174
  175. 175 });
  176. 176
  177. 177
  178. 178
  179. 179 await navigator.serviceWorker.ready;
  180. 180
  181. 181 return registration;
  182. 182
  183. 183 } catch (error) {
  184. 184
  185. 185 console.error('Service Worker registration failed:', error);
  186. 186
  187. 187 return null;
  188. 188
  189. 189 }
  190. 190
  191. 191 }
  192. 192
  193. 193 }
  194. 194
  195. 195 setupInstallPrompt() {
  196. 196
  197. 197 // Listen for install prompt
  198. 198
  199. 199 window.addEventListener('beforeinstallprompt', (e) => {
  200. 200
  201. 201 console.log('PWA install prompt available');
  202. 202
  203. 203 e.preventDefault();
  204. 204
  205. 205 this.deferredPrompt = e;
  206. 206
  207. 207 this.showInstallPrompt();
  208. 208
  209. 209 });
  210. 210
  211. 211 // Handle successful installation
  212. 212
  213. 213 window.addEventListener('appinstalled', () => {
  214. 214
  215. 215 console.log('PWA installed successfully');
  216. 216
  217. 217 this.hideInstallPrompt();
  218. 218
  219. 219 this.deferredPrompt = null;
  220. 220
  221. 221 this.showInstallSuccess();
  222. 222
  223. 223 });
  224. 224
  225. 225 }
  226. 226
  227. 227 showInstallPrompt() {
  228. 228
  229. 229 const installButton = this.container.querySelector('.install-button');
  230. 230
  231. 231 const installBanner = this.container.querySelector('.install-banner');
  232. 232
  233. 233
  234. 234
  235. 235 if (installButton) {
  236. 236
  237. 237 installButton.style.display = 'block';
  238. 238
  239. 239 installButton.addEventListener('click', () => this.promptInstall());
  240. 240
  241. 241 }
  242. 242
  243. 243
  244. 244
  245. 245 if (installBanner) {
  246. 246
  247. 247 // Show banner after 30 seconds if not dismissed
  248. 248
  249. 249 setTimeout(() => {
  250. 250
  251. 251 if (this.deferredPrompt && !localStorage.getItem('pwa-install-dismissed')) {
  252. 252
  253. 253 installBanner.style.display = 'block';
  254. 254
  255. 255 }
  256. 256
  257. 257 }, 30000);
  258. 258
  259. 259 }
  260. 260
  261. 261 }
  262. 262
  263. 263 hideInstallPrompt() {
  264. 264
  265. 265 const installButton = this.container.querySelector('.install-button');
  266. 266
  267. 267 const installBanner = this.container.querySelector('.install-banner');
  268. 268
  269. 269
  270. 270
  271. 271 if (installButton) {
  272. 272
  273. 273 installButton.style.display = 'none';
  274. 274
  275. 275 }
  276. 276
  277. 277
  278. 278
  279. 279 if (installBanner) {
  280. 280
  281. 281 installBanner.style.display = 'none';
  282. 282
  283. 283 }
  284. 284
  285. 285 }
  286. 286
  287. 287 async promptInstall() {
  288. 288
  289. 289 if (!this.deferredPrompt) {
  290. 290
  291. 291 this.showInstallInfo();
  292. 292
  293. 293 return;
  294. 294
  295. 295 }
  296. 296
  297. 297 try {
  298. 298
  299. 299 const result = await this.deferredPrompt.prompt();
  300. 300
  301. 301 console.log('Install prompt result:', result.outcome);
  302. 302
  303. 303
  304. 304
  305. 305 if (result.outcome === 'accepted') {
  306. 306
  307. 307 console.log('User accepted the install prompt');
  308. 308
  309. 309 } else {
  310. 310
  311. 311 console.log('User dismissed the install prompt');
  312. 312
  313. 313 localStorage.setItem('pwa-install-dismissed', Date.now());
  314. 314
  315. 315 }
  316. 316
  317. 317 } catch (error) {
  318. 318
  319. 319 console.error('Install prompt failed:', error);
  320. 320
  321. 321 this.showInstallInfo();
  322. 322
  323. 323 }
  324. 324
  325. 325 this.deferredPrompt = null;
  326. 326
  327. 327 this.hideInstallPrompt();
  328. 328
  329. 329 }
  330. 330
  331. 331 showInstallInfo() {
  332. 332
  333. 333 const userAgent = navigator.userAgent.toLowerCase();
  334. 334
  335. 335 let message = 'To install this app:\n\n';
  336. 336
  337. 337
  338. 338
  339. 339 if (userAgent.includes('chrome') || userAgent.includes('edge')) {
  340. 340
  341. 341 message += '1. Click the menu (⋮) in your browser\n2. Select "Install Paranormal FM"';
  342. 342
  343. 343 } else if (userAgent.includes('firefox')) {
  344. 344
  345. 345 message += '1. Click the menu (≡) in your browser\n2. Select "Install This Site as an App"';
  346. 346
  347. 347 } else if (userAgent.includes('safari')) {
  348. 348
  349. 349 message += '1. Click the Share button\n2. Select "Add to Home Screen"';
  350. 350
  351. 351 } else {
  352. 352
  353. 353 message += '1. Look for "Add to Home Screen" in your browser menu\n2. Or check the address bar for an install icon';
  354. 354
  355. 355 }
  356. 356
  357. 357
  358. 358
  359. 359 alert(message);
  360. 360
  361. 361 }
  362. 362
  363. 363 showInstallSuccess() {
  364. 364
  365. 365 const toast = document.createElement('div');
  366. 366
  367. 367 toast.className = 'pwa-toast pwa-toast-success';
  368. 368
  369. 369 toast.innerHTML = `
  370. 370
  371. 371 <div class="pwa-toast-content">
  372. 372
  373. 373 <i class="fas fa-check-circle"></i>
  374. 374
  375. 375 <span>Paranormal FM installed successfully!</span>
  376. 376
  377. 377 </div>
  378. 378
  379. 379 `;
  380. 380
  381. 381
  382. 382
  383. 383 document.body.appendChild(toast);
  384. 384
  385. 385 setTimeout(() => toast.classList.add('show'), 100);
  386. 386
  387. 387 setTimeout(() => {
  388. 388
  389. 389 toast.classList.remove('show');
  390. 390
  391. 391 setTimeout(() => document.body.removeChild(toast), 300);
  392. 392
  393. 393 }, 3000);
  394. 394
  395. 395 }
  396. 396
  397. 397 showUpdateAvailable() {
  398. 398
  399. 399 const updateBanner = this.container.querySelector('.update-banner');
  400. 400
  401. 401 if (updateBanner) {
  402. 402
  403. 403 updateBanner.style.display = 'block';
  404. 404
  405. 405
  406. 406
  407. 407 const updateButton = updateBanner.querySelector('.update-banner-button');
  408. 408
  409. 409 updateButton.addEventListener('click', () => {
  410. 410
  411. 411 this.applyUpdate();
  412. 412
  413. 413 });
  414. 414
  415. 415 }
  416. 416
  417. 417 }
  418. 418
  419. 419 applyUpdate() {
  420. 420
  421. 421 if (this.serviceWorker && this.serviceWorker.waiting) {
  422. 422
  423. 423 this.serviceWorker.waiting.postMessage({ type: 'SKIP_WAITING' });
  424. 424
  425. 425 window.location.reload();
  426. 426
  427. 427 }
  428. 428
  429. 429 }
  430. 430
  431. 431 // Enhanced setupElements method with PWA features
  432. 432
  433. 433 setupElements() {
  434. 434
  435. 435 this.playButton = this.container.querySelector('.play-button');
  436. 436
  437. 437 this.volumeButton = this.container.querySelector('.volume-button');
  438. 438
  439. 439 this.volumeSlider = this.container.querySelector('.volume-slider');
  440. 440
  441. 441 this.songNameDiv = this.container.querySelector('.song-name');
  442. 442
  443. 443 this.artistNameDiv = this.container.querySelector('.artist-name');
  444. 444
  445. 445 this.radioNameDiv = this.container.querySelector('.radio-name');
  446. 446
  447. 447 this.artworkImage = this.container.querySelector('.artwork-image');
  448. 448
  449. 449 this.artworkPlaceholder = this.container.querySelector('.artwork-placeholder');
  450. 450
  451. 451 this.elapsedTimeSpan = this.container.querySelector('.elapsed-time');
  452. 452
  453. 453 this.durationTimeSpan = this.container.querySelector('.duration-time');
  454. 454
  455. 455 this.timeBarProgress = this.container.querySelector('.time-bar-progress');
  456. 456
  457. 457 this.liveContainer = this.container.querySelector('.live-container');
  458. 458
  459. 459 this.historyModal = document.querySelector('.history-modal');
  460. 460
  461. 461 this.historyList = document.querySelector('.history-list');
  462. 462
  463. 463
  464. 464
  465. 465 // PWA specific elements
  466. 466
  467. 467 this.installButton = this.container.querySelector('.install-button');
  468. 468
  469. 469 this.installBanner = this.container.querySelector('.install-banner');
  470. 470
  471. 471 this.updateBanner = this.container.querySelector('.update-banner');
  472. 472
  473. 473
  474. 474
  475. 475 // Set initial values
  476. 476
  477. 477 if (this.volumeSlider) {
  478. 478
  479. 479 this.volumeSlider.value = this.currentVolume * 100;
  480. 480
  481. 481 }
  482. 482
  483. 483
  484. 484
  485. 485 // Set default station name
  486. 486
  487. 487 if (this.songNameDiv) {
  488. 488
  489. 489 this.songNameDiv.textContent = this.config.stationName || 'Paranormal FM';
  490. 490
  491. 491 }
  492. 492
  493. 493 if (this.artistNameDiv) {
  494. 494
  495. 495 this.artistNameDiv.textContent = 'Click play to start streaming';
  496. 496
  497. 497 }
  498. 498
  499. 499 if (this.radioNameDiv) {
  500. 500
  501. 501 this.radioNameDiv.textContent = this.config.stationName || 'Paranormal FM';
  502. 502
  503. 503 }
  504. 504
  505. 505
  506. 506
  507. 507 this.updatePlayState();
  508. 508
  509. 509 this.updateVolumeState();
  510. 510
  511. 511 this.setupVolumeControls();
  512. 512
  513. 513 this.attachHistoryEventListeners();
  514. 514
  515. 515 this.setupInstallBannerEvents();
  516. 516
  517. 517 }
  518. 518
  519. 519 setupInstallBannerEvents() {
  520. 520
  521. 521 if (this.installBanner) {
  522. 522
  523. 523 const installNowButton = this.installBanner.querySelector('.install-now');
  524. 524
  525. 525 const installLaterButton = this.installBanner.querySelector('.install-later');
  526. 526
  527. 527
  528. 528
  529. 529 if (installNowButton) {
  530. 530
  531. 531 installNowButton.addEventListener('click', () => this.promptInstall());
  532. 532
  533. 533 }
  534. 534
  535. 535
  536. 536
  537. 537 if (installLaterButton) {
  538. 538
  539. 539 installLaterButton.addEventListener('click', () => {
  540. 540
  541. 541 this.installBanner.style.display = 'none';
  542. 542
  543. 543 localStorage.setItem('pwa-install-dismissed', Date.now());
  544. 544
  545. 545 });
  546. 546
  547. 547 }
  548. 548
  549. 549 }
  550. 550
  551. 551 }
  552. 552
  553. 553 // Enhanced setupMediaSession with PWA features
  554. 554
  555. 555 setupMediaSession() {
  556. 556
  557. 557 if ('mediaSession' in navigator) {
  558. 558
  559. 559 navigator.mediaSession.setActionHandler('play', () => this.startPlayback());
  560. 560
  561. 561 navigator.mediaSession.setActionHandler('pause', () => this.audio.pause());
  562. 562
  563. 563 navigator.mediaSession.setActionHandler('stop', () => {
  564. 564
  565. 565 this.audio.pause();
  566. 566
  567. 567 this.audio.currentTime = 0;
  568. 568
  569. 569 });
  570. 570
  571. 571
  572. 572
  573. 573 // PWA-specific actions
  574. 574
  575. 575 navigator.mediaSession.setActionHandler('previoustrack', () => {
  576. 576
  577. 577 // Could implement previous song functionality
  578. 578
  579. 579 console.log('Previous track requested');
  580. 580
  581. 581 });
  582. 582
  583. 583
  584. 584
  585. 585 navigator.mediaSession.setActionHandler('nexttrack', () => {
  586. 586
  587. 587 // Could implement next song functionality
  588. 588
  589. 589 console.log('Next track requested');
  590. 590
  591. 591 });
  592. 592
  593. 593
  594. 594
  595. 595 // Set initial metadata
  596. 596
  597. 597 this.updateMediaSessionMetadata({
  598. 598
  599. 599 title: 'Paranormal FM',
  600. 600
  601. 601 artist: 'Live Stream'
  602. 602
  603. 603 });
  604. 604
  605. 605 }
  606. 606
  607. 607 }
  608. 608
  609. 609 updateMediaSessionMetadata(songData) {
  610. 610
  611. 611 if ('mediaSession' in navigator) {
  612. 612
  613. 613 navigator.mediaSession.metadata = new MediaMetadata({
  614. 614
  615. 615 title: songData.title || 'Paranormal FM',
  616. 616
  617. 617 artist: songData.artist || 'Live Stream',
  618. 618
  619. 619 album: 'Paranormal FM Radio',
  620. 620
  621. 621 artwork: [
  622. 622
  623. 623 {
  624. 624
  625. 625 src: songData.art || this.config.manifestUrl.replace('manifest.json', 'assets/icons/icon-512x512.png'),
  626. 626
  627. 627 sizes: '512x512',
  628. 628
  629. 629 type: 'image/png'
  630. 630
  631. 631 },
  632. 632
  633. 633 {
  634. 634
  635. 635 src: songData.art || this.config.manifestUrl.replace('manifest.json', 'assets/icons/icon-256x256.png'),
  636. 636
  637. 637 sizes: '256x256',
  638. 638
  639. 639 type: 'image/png'
  640. 640
  641. 641 }
  642. 642
  643. 643 ]
  644. 644
  645. 645 });
  646. 646
  647. 647
  648. 648
  649. 649 // Update playback state
  650. 650
  651. 651 navigator.mediaSession.playbackState = this.isPlaying ? 'playing' : 'paused';
  652. 652
  653. 653 }
  654. 654
  655. 655 }
  656. 656
  657. 657 // Network status handling for PWA
  658. 658
  659. 659 setupNetworkHandling() {
  660. 660
  661. 661 window.addEventListener('online', () => {
  662. 662
  663. 663 console.log('Network: Online');
  664. 664
  665. 665 this.handleNetworkChange(true);
  666. 666
  667. 667 });
  668. 668
  669. 669 window.addEventListener('offline', () => {
  670. 670
  671. 671 console.log('Network: Offline');
  672. 672
  673. 673 this.handleNetworkChange(false);
  674. 674
  675. 675 });
  676. 676
  677. 677 }
  678. 678
  679. 679 handleNetworkChange(isOnline) {
  680. 680
  681. 681 if (isOnline) {
  682. 682
  683. 683 // Resume metadata polling when back online
  684. 684
  685. 685 if (!this.metadataInterval) {
  686. 686
  687. 687 this.startMetadataPolling();
  688. 688
  689. 689 }
  690. 690
  691. 691
  692. 692
  693. 693 // Try to resume audio if it was playing
  694. 694
  695. 695 if (this.isPlaying && this.audio.paused) {
  696. 696
  697. 697 this.startPlayback().catch(console.error);
  698. 698
  699. 699 }
  700. 700
  701. 701 } else {
  702. 702
  703. 703 // Show offline status
  704. 704
  705. 705 if (this.artistNameDiv) {
  706. 706
  707. 707 this.artistNameDiv.textContent = 'Offline - Check your connection';
  708. 708
  709. 709 }
  710. 710
  711. 711 }
  712. 712
  713. 713 }
  714. 714
  715. 715 // All other methods from the previous implementation remain the same...
  716. 716
  717. 717 // (setupPersistentAudio, setupServiceWorkerCommunication, loadAudioState, etc.)
  718. 718
  719. 719 // ... [Include all methods from the previous complete implementation] ...
  720. 720
  721. 721 cleanup() {
  722. 722
  723. 723 if (this.elapsedInterval) {
  724. 724
  725. 725 clearInterval(this.elapsedInterval);
  726. 726
  727. 727 this.elapsedInterval = null;
  728. 728
  729. 729 }
  730. 730
  731. 731 if (this.metadataInterval) {
  732. 732
  733. 733 clearInterval(this.metadataInterval);
  734. 734
  735. 735 this.metadataInterval = null;
  736. 736
  737. 737 }
  738. 738
  739. 739 if (this.audio && this.audio !== window.paranormalFMAudio) {
  740. 740
  741. 741 this.audio.pause();
  742. 742
  743. 743 }
  744. 744
  745. 745 this.broadcastAudioState();
  746. 746
  747. 747
  748. 748
  749. 749 // Hide PWA banners
  750. 750
  751. 751 this.hideInstallPrompt();
  752. 752
  753. 753 if (this.updateBanner) {
  754. 754
  755. 755 this.updateBanner.style.display = 'none';
  756. 756
  757. 757 }
  758. 758
  759. 759 }
  760. 760
  761. 761}
  762. 762
  763. 763// Initialize when DOM is ready
  764. 764
  765. 765document.addEventListener('DOMContentLoaded', () => {
  766. 766
  767. 767 const container = document.getElementById('radio-player');
  768. 768
  769. 769
  770. 770
  771. 771 if (container && window.radioPlayerConfig) {
  772. 772
  773. 773 // Cleanup existing player if any
  774. 774
  775. 775 if (window.paranormalFMPlayer) {
  776. 776
  777. 777 window.paranormalFMPlayer.cleanup();
  778. 778
  779. 779 }
  780. 780
  781. 781
  782. 782
  783. 783 window.paranormalFMPlayer = new PersistentRadioPlayerPWA(container, window.radioPlayerConfig);
  784. 784
  785. 785 console.log('Persistent Radio Player PWA initialized');
  786. 786
  787. 787 }
  788. 788
  789. 789});
  790. 790
  791. 791// Cleanup on page unload
  792. 792
  793. 793window.addEventListener('beforeunload', () => {
  794. 794
  795. 795 if (window.paranormalFMPlayer) {
  796. 796
  797. 797 window.paranormalFMPlayer.cleanup();
  798. 798
  799. 799 }
  800. 800
  801. 801});

Raw Paste

Comments 0
Login to post a comment.
  • No comments yet. Be the first.
Login to post a comment. Login or Register
We use cookies. To comply with GDPR in the EU and the UK we have to show you these.

We use cookies and similar technologies to keep this website functional (including spam protection via Google reCAPTCHA or Cloudflare Turnstile), and — with your consent — to measure usage and show ads. See Privacy.