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