- 1<?php
- 2
- 3/**
- 4 * Plugin Name: Persistent Radio Player PWA
- 5 * Description: A persistent internet radio player for WordPress with PWA installation capability
- 6 * Version: 2.0.0
- 7 * Author: Your Name
- 8
- 9 */
- 10
- 11// Prevent direct access
- 12
- 13if (!defined('ABSPATH')) {
- 14
- 15 exit;
- 16
- 17}
- 18
- 19class PersistentRadioPlayerPWA {
- 20
- 21
- 22
- 23 private $config = [
- 24
- 25 'stream_url' => 'https://s76.radiolize.com:8050/radio.mp3',
- 26
- 27 'api_url' => 'https://s76.radiolize.com/api/nowplaying/18',
- 28
- 29 'station_name' => 'Paranormal FM',
- 30
- 31 'station_id' => 18,
- 32
- 33 'app_name' => 'Paranormal FM Radio',
- 34
- 35 'app_short_name' => 'Paranormal FM',
- 36
- 37 'app_description' => 'Listen to Paranormal FM - True Crime, Mystery & Supernatural Stories',
- 38
- 39 'theme_color' => '#000000',
- 40
- 41 'background_color' => '#000000',
- 42
- 43 'start_url' => '/'
- 44
- 45 ];
- 46
- 47
- 48
- 49 public function __construct() {
- 50
- 51 add_action('wp_enqueue_scripts', [$this, 'enqueue_scripts']);
- 52
- 53 add_action('wp_footer', [$this, 'render_player']);
- 54
- 55 add_action('wp_head', [$this, 'add_pwa_meta_tags']);
- 56
- 57 add_action('wp_head', [$this, 'add_service_worker_support']);
- 58
- 59 add_action('init', [$this, 'handle_pwa_requests']);
- 60
- 61 add_action('init', [$this, 'add_rewrite_rules']);
- 62
- 63 add_action('admin_menu', [$this, 'add_admin_menu']);
- 64
- 65
- 66
- 67 // PWA specific hooks
- 68
- 69 add_action('wp_ajax_get_radio_metadata', [$this, 'get_radio_metadata']);
- 70
- 71 add_action('wp_ajax_nopriv_get_radio_metadata', [$this, 'get_radio_metadata']);
- 72
- 73
- 74
- 75 register_activation_hook(__FILE__, [$this, 'flush_rewrite_rules']);
- 76
- 77 register_deactivation_hook(__FILE__, [$this, 'flush_rewrite_rules']);
- 78
- 79 }
- 80
- 81
- 82
- 83 public function add_rewrite_rules() {
- 84
- 85 // Service Worker
- 86
- 87 add_rewrite_rule('^paranormal-fm-sw\.js$', 'index.php?paranormal_fm_sw=1', 'top');
- 88
- 89 add_rewrite_tag('%paranormal_fm_sw%', '([^&]+)');
- 90
- 91
- 92
- 93 // Web App Manifest
- 94
- 95 add_rewrite_rule('^paranormal-fm-manifest\.json$', 'index.php?paranormal_fm_manifest=1', 'top');
- 96
- 97 add_rewrite_tag('%paranormal_fm_manifest%', '([^&]+)');
- 98
- 99
- 100
- 101 // Offline page
- 102
- 103 add_rewrite_rule('^paranormal-fm-offline\.html$', 'index.php?paranormal_fm_offline=1', 'top');
- 104
- 105 add_rewrite_tag('%paranormal_fm_offline%', '([^&]+)');
- 106
- 107 }
- 108
- 109
- 110
- 111 public function flush_rewrite_rules() {
- 112
- 113 flush_rewrite_rules();
- 114
- 115 }
- 116
- 117
- 118
- 119 public function handle_pwa_requests() {
- 120
- 121 if (get_query_var('paranormal_fm_sw')) {
- 122
- 123 $this->serve_service_worker();
- 124
- 125 exit;
- 126
- 127 }
- 128
- 129
- 130
- 131 if (get_query_var('paranormal_fm_manifest')) {
- 132
- 133 $this->serve_manifest();
- 134
- 135 exit;
- 136
- 137 }
- 138
- 139
- 140
- 141 if (get_query_var('paranormal_fm_offline')) {
- 142
- 143 $this->serve_offline_page();
- 144
- 145 exit;
- 146
- 147 }
- 148
- 149 }
- 150
- 151
- 152
- 153 private function serve_service_worker() {
- 154
- 155 header('Content-Type: application/javascript');
- 156
- 157 header('Service-Worker-Allowed: /');
- 158
- 159 header('Cache-Control: no-cache, no-store, must-revalidate');
- 160
- 161 header('Pragma: no-cache');
- 162
- 163 header('Expires: 0');
- 164
- 165
- 166
- 167 echo $this->get_enhanced_service_worker_content();
- 168
- 169 }
- 170
- 171
- 172
- 173 private function serve_manifest() {
- 174
- 175 header('Content-Type: application/json');
- 176
- 177 header('Cache-Control: public, max-age=86400'); // Cache for 24 hours
- 178
- 179
- 180
- 181 $manifest = [
- 182
- 183 'name' => $this->config['app_name'],
- 184
- 185 'short_name' => $this->config['app_short_name'],
- 186
- 187 'description' => $this->config['app_description'],
- 188
- 189 'start_url' => $this->config['start_url'],
- 190
- 191 'display' => 'standalone',
- 192
- 193 'background_color' => $this->config['background_color'],
- 194
- 195 'theme_color' => $this->config['theme_color'],
- 196
- 197 'orientation' => 'portrait-primary',
- 198
- 199 'scope' => '/',
- 200
- 201 'icons' => [
- 202
- 203 [
- 204
- 205 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-72x72.png',
- 206
- 207 'sizes' => '72x72',
- 208
- 209 'type' => 'image/png',
- 210
- 211 'purpose' => 'any'
- 212
- 213 ],
- 214
- 215 [
- 216
- 217 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-96x96.png',
- 218
- 219 'sizes' => '96x96',
- 220
- 221 'type' => 'image/png',
- 222
- 223 'purpose' => 'any'
- 224
- 225 ],
- 226
- 227 [
- 228
- 229 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-128x128.png',
- 230
- 231 'sizes' => '128x128',
- 232
- 233 'type' => 'image/png',
- 234
- 235 'purpose' => 'any'
- 236
- 237 ],
- 238
- 239 [
- 240
- 241 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-144x144.png',
- 242
- 243 'sizes' => '144x144',
- 244
- 245 'type' => 'image/png',
- 246
- 247 'purpose' => 'any'
- 248
- 249 ],
- 250
- 251 [
- 252
- 253 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-152x152.png',
- 254
- 255 'sizes' => '152x152',
- 256
- 257 'type' => 'image/png',
- 258
- 259 'purpose' => 'any'
- 260
- 261 ],
- 262
- 263 [
- 264
- 265 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-192x192.png',
- 266
- 267 'sizes' => '192x192',
- 268
- 269 'type' => 'image/png',
- 270
- 271 'purpose' => 'any maskable'
- 272
- 273 ],
- 274
- 275 [
- 276
- 277 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-384x384.png',
- 278
- 279 'sizes' => '384x384',
- 280
- 281 'type' => 'image/png',
- 282
- 283 'purpose' => 'any'
- 284
- 285 ],
- 286
- 287 [
- 288
- 289 'src' => plugin_dir_url(__FILE__) . 'assets/icons/icon-512x512.png',
- 290
- 291 'sizes' => '512x512',
- 292
- 293 'type' => 'image/png',
- 294
- 295 'purpose' => 'any maskable'
- 296
- 297 ]
- 298
- 299 ],
- 300
- 301 'categories' => ['music', 'entertainment', 'radio'],
- 302
- 303 'lang' => 'en',
- 304
- 305 'screenshots' => [
- 306
- 307 [
- 308
- 309 'src' => plugin_dir_url(__FILE__) . 'assets/screenshots/desktop.png',
- 310
- 311 'sizes' => '1280x720',
- 312
- 313 'type' => 'image/png',
- 314
- 315 'form_factor' => 'wide'
- 316
- 317 ],
- 318
- 319 [
- 320
- 321 'src' => plugin_dir_url(__FILE__) . 'assets/screenshots/mobile.png',
- 322
- 323 'sizes' => '375x667',
- 324
- 325 'type' => 'image/png',
- 326
- 327 'form_factor' => 'narrow'
- 328
- 329 ]
- 330
- 331 ],
- 332
- 333 'shortcuts' => [
- 334
- 335 [
- 336
- 337 'name' => 'Play Radio',
- 338
- 339 'short_name' => 'Play',
- 340
- 341 'description' => 'Start playing Paranormal FM',
- 342
- 343 'url' => '/?action=play',
- 344
- 345 'icons' => [
- 346
- 347 [
- 348
- 349 'src' => plugin_dir_url(__FILE__) . 'assets/icons/play-96x96.png',
- 350
- 351 'sizes' => '96x96'
- 352
- 353 ]
- 354
- 355 ]
- 356
- 357 ]
- 358
- 359 ]
- 360
- 361 ];
- 362
- 363
- 364
- 365 echo json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
- 366
- 367 }
- 368
- 369
- 370
- 371 private function serve_offline_page() {
- 372
- 373 header('Content-Type: text/html; charset=UTF-8');
- 374
- 375 header('Cache-Control: public, max-age=86400');
- 376
- 377
- 378
- 379 echo $this->get_offline_page_content();
- 380
- 381 }
- 382
- 383
- 384
- 385 private function get_offline_page_content() {
- 386
- 387 return '<!DOCTYPE html>
- 388
- 389<html lang="en">
- 390
- 391<head>
- 392
- 393 <meta charset="UTF-8">
- 394
- 395 <meta name="viewport" content="width=device-width, initial-scale=1.0">
- 396
- 397 <title>Offline - Paranormal FM</title>
- 398
- 399 <style>
- 400
- 401 body {
- 402
- 403 font-family: "Poppins", sans-serif;
- 404
- 405 background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%);
- 406
- 407 color: #ffffff;
- 408
- 409 margin: 0;
- 410
- 411 padding: 0;
- 412
- 413 height: 100vh;
- 414
- 415 display: flex;
- 416
- 417 flex-direction: column;
- 418
- 419 align-items: center;
- 420
- 421 justify-content: center;
- 422
- 423 text-align: center;
- 424
- 425 }
- 426
- 427 .offline-container {
- 428
- 429 max-width: 400px;
- 430
- 431 padding: 2rem;
- 432
- 433 }
- 434
- 435 .offline-icon {
- 436
- 437 font-size: 4rem;
- 438
- 439 margin-bottom: 1rem;
- 440
- 441 opacity: 0.7;
- 442
- 443 }
- 444
- 445 .offline-title {
- 446
- 447 font-size: 1.5rem;
- 448
- 449 font-weight: 600;
- 450
- 451 margin-bottom: 1rem;
- 452
- 453 }
- 454
- 455 .offline-message {
- 456
- 457 font-size: 1rem;
- 458
- 459 opacity: 0.8;
- 460
- 461 line-height: 1.6;
- 462
- 463 margin-bottom: 2rem;
- 464
- 465 }
- 466
- 467 .retry-button {
- 468
- 469 background: #ffffff;
- 470
- 471 color: #000000;
- 472
- 473 border: none;
- 474
- 475 padding: 12px 24px;
- 476
- 477 border-radius: 25px;
- 478
- 479 font-weight: 600;
- 480
- 481 cursor: pointer;
- 482
- 483 transition: all 0.3s ease;
- 484
- 485 }
- 486
- 487 .retry-button:hover {
- 488
- 489 background: #f0f0f0;
- 490
- 491 transform: translateY(-2px);
- 492
- 493 }
- 494
- 495 </style>
- 496
- 497</head>
- 498
- 499<body>
- 500
- 501 <div class="offline-container">
- 502
- 503 <div class="offline-icon">📻</div>
- 504
- 505 <h1 class="offline-title">You\'re Offline</h1>
- 506
- 507 <p class="offline-message">
- 508
- 509 It looks like you\'re not connected to the internet.
- 510
- 511 The radio stream requires an internet connection to play.
- 512
- 513 </p>
- 514
- 515 <button class="retry-button" onclick="window.location.reload()">
- 516
- 517 Try Again
- 518
- 519 </button>
- 520
- 521 </div>
- 522
- 523
- 524
- 525 <script>
- 526
- 527 // Check connection status and auto-retry
- 528
- 529 window.addEventListener("online", () => {
- 530
- 531 window.location.reload();
- 532
- 533 });
- 534
- 535 </script>
- 536
- 537</body>
- 538
- 539</html>';
- 540
- 541 }
- 542
- 543
- 544
- 545 private function get_enhanced_service_worker_content() {
- 546
- 547 return '
- 548
- 549// Enhanced Service Worker for Paranormal FM PWA
- 550
- 551const CACHE_NAME = "paranormal-fm-v2.0.0";
- 552
- 553const OFFLINE_URL = "/paranormal-fm-offline.html";
- 554
- 555// Cache resources
- 556
- 557const CACHE_URLS = [
- 558
- 559 "/",
- 560
- 561 "/paranormal-fm-offline.html",
- 562
- 563 "' . plugin_dir_url(__FILE__) . 'assets/player.js",
- 564
- 565 "' . plugin_dir_url(__FILE__) . 'assets/player.css",
- 566
- 567 "' . plugin_dir_url(__FILE__) . 'assets/icons/icon-192x192.png",
- 568
- 569 "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
- 570
- 571];
- 572
- 573// Audio state management
- 574
- 575let audioState = {
- 576
- 577 isPlaying: false,
- 578
- 579 volume: 1,
- 580
- 581 streamUrl: "' . $this->config['stream_url'] . '",
- 582
- 583 lastUpdate: Date.now(),
- 584
- 585 metadata: null
- 586
- 587};
- 588
- 589// Install event
- 590
- 591self.addEventListener("install", (event) => {
- 592
- 593 console.log("Paranormal FM PWA Service Worker installing...");
- 594
- 595
- 596
- 597 event.waitUntil(
- 598
- 599 caches.open(CACHE_NAME).then((cache) => {
- 600
- 601 console.log("Caching app shell");
- 602
- 603 return cache.addAll(CACHE_URLS);
- 604
- 605 }).then(() => {
- 606
- 607 return self.skipWaiting();
- 608
- 609 })
- 610
- 611 );
- 612
- 613});
- 614
- 615// Activate event
- 616
- 617self.addEventListener("activate", (event) => {
- 618
- 619 console.log("Paranormal FM PWA Service Worker activated");
- 620
- 621
- 622
- 623 event.waitUntil(
- 624
- 625 caches.keys().then((cacheNames) => {
- 626
- 627 return Promise.all(
- 628
- 629 cacheNames.map((cacheName) => {
- 630
- 631 if (cacheName !== CACHE_NAME) {
- 632
- 633 console.log("Deleting old cache:", cacheName);
- 634
- 635 return caches.delete(cacheName);
- 636
- 637 }
- 638
- 639 })
- 640
- 641 );
- 642
- 643 }).then(() => {
- 644
- 645 return self.clients.claim();
- 646
- 647 })
- 648
- 649 );
- 650
- 651});
- 652
- 653// Fetch event with network-first strategy for API calls
- 654
- 655self.addEventListener("fetch", (event) => {
- 656
- 657 const url = new URL(event.request.url);
- 658
- 659
- 660
- 661 // Handle API requests with network-first strategy
- 662
- 663 if (url.pathname.includes("/api/") || url.hostname.includes("radiolize.com")) {
- 664
- 665 event.respondWith(
- 666
- 667 fetch(event.request)
- 668
- 669 .then((response) => {
- 670
- 671 // Clone and cache successful API responses
- 672
- 673 if (response.ok) {
- 674
- 675 const responseClone = response.clone();
- 676
- 677 caches.open(CACHE_NAME).then((cache) => {
- 678
- 679 cache.put(event.request, responseClone);
- 680
- 681 });
- 682
- 683 }
- 684
- 685 return response;
- 686
- 687 })
- 688
- 689 .catch(() => {
- 690
- 691 // Fallback to cache for API requests
- 692
- 693 return caches.match(event.request);
- 694
- 695 })
- 696
- 697 );
- 698
- 699 return;
- 700
- 701 }
- 702
- 703
- 704
- 705 // Handle navigation requests
- 706
- 707 if (event.request.mode === "navigate") {
- 708
- 709 event.respondWith(
- 710
- 711 fetch(event.request)
- 712
- 713 .catch(() => {
- 714
- 715 return caches.match(OFFLINE_URL);
- 716
- 717 })
- 718
- 719 );
- 720
- 721 return;
- 722
- 723 }
- 724
- 725
- 726
- 727 // Handle other requests with cache-first strategy
- 728
- 729 event.respondWith(
- 730
- 731 caches.match(event.request)
- 732
- 733 .then((response) => {
- 734
- 735 if (response) {
- 736
- 737 return response;
- 738
- 739 }
- 740
- 741
- 742
- 743 return fetch(event.request).then((response) => {
- 744
- 745 // Cache successful responses
- 746
- 747 if (response.status === 200) {
- 748
- 749 const responseClone = response.clone();
- 750
- 751 caches.open(CACHE_NAME).then((cache) => {
- 752
- 753 cache.put(event.request, responseClone);
- 754
- 755 });
- 756
- 757 }
- 758
- 759 return response;
- 760
- 761 });
- 762
- 763 })
- 764
- 765 );
- 766
- 767});
- 768
- 769// Message handling for audio state and PWA functionality
- 770
- 771self.addEventListener("message", (event) => {
- 772
- 773 const { type, data } = event.data;
- 774
- 775
- 776
- 777 switch (type) {
- 778
- 779 case "AUDIO_STATE_UPDATE":
- 780
- 781 audioState = { ...audioState, ...data, lastUpdate: Date.now() };
- 782
- 783 broadcastToClients("AUDIO_STATE_SYNC", audioState);
- 784
- 785 break;
- 786
- 787
- 788
- 789 case "GET_AUDIO_STATE":
- 790
- 791 event.ports[0].postMessage({
- 792
- 793 type: "AUDIO_STATE_RESPONSE",
- 794
- 795 data: audioState
- 796
- 797 });
- 798
- 799 break;
- 800
- 801
- 802
- 803 case "SKIP_WAITING":
- 804
- 805 self.skipWaiting();
- 806
- 807 break;
- 808
- 809
- 810
- 811 case "CLAIM_CLIENTS":
- 812
- 813 self.clients.claim();
- 814
- 815 break;
- 816
- 817
- 818
- 819 case "GET_VERSION":
- 820
- 821 event.ports[0].postMessage({
- 822
- 823 type: "VERSION_RESPONSE",
- 824
- 825 data: { version: CACHE_NAME }
- 826
- 827 });
- 828
- 829 break;
- 830
- 831 }
- 832
- 833});
- 834
- 835// Background sync for metadata when online
- 836
- 837self.addEventListener("sync", (event) => {
- 838
- 839 if (event.tag === "metadata-sync") {
- 840
- 841 event.waitUntil(syncMetadata());
- 842
- 843 }
- 844
- 845});
- 846
- 847async function syncMetadata() {
- 848
- 849 try {
- 850
- 851 const response = await fetch("' . $this->config['api_url'] . '");
- 852
- 853 const data = await response.json();
- 854
- 855
- 856
- 857 audioState.metadata = data;
- 858
- 859 broadcastToClients("METADATA_SYNC", data);
- 860
- 861 console.log("Background metadata sync completed");
- 862
- 863 } catch (error) {
- 864
- 865 console.error("Background metadata sync failed:", error);
- 866
- 867 }
- 868
- 869}
- 870
- 871function broadcastToClients(type, data) {
- 872
- 873 self.clients.matchAll().then(clients => {
- 874
- 875 clients.forEach(client => {
- 876
- 877 client.postMessage({ type, data });
- 878
- 879 });
- 880
- 881 });
- 882
- 883}
- 884
- 885// Push notification handling (for future features)
- 886
- 887self.addEventListener("push", (event) => {
- 888
- 889 if (event.data) {
- 890
- 891 const data = event.data.json();
- 892
- 893 const options = {
- 894
- 895 body: data.body || "New content available",
- 896
- 897 icon: "/paranormal-fm-manifest.json",
- 898
- 899 badge: "' . plugin_dir_url(__FILE__) . 'assets/icons/badge-72x72.png",
- 900
- 901 vibrate: [200, 100, 200],
- 902
- 903 data: data.data || {},
- 904
- 905 actions: [
- 906
- 907 {
- 908
- 909 action: "open",
- 910
- 911 title: "Open App"
- 912
- 913 }
- 914
- 915 ]
- 916
- 917 };
- 918
- 919
- 920
- 921 event.waitUntil(
- 922
- 923 self.registration.showNotification(data.title || "Paranormal FM", options)
- 924
- 925 );
- 926
- 927 }
- 928
- 929});
- 930
- 931// Notification click handling
- 932
- 933self.addEventListener("notificationclick", (event) => {
- 934
- 935 event.notification.close();
- 936
- 937
- 938
- 939 if (event.action === "open" || !event.action) {
- 940
- 941 event.waitUntil(
- 942
- 943 clients.openWindow("/")
- 944
- 945 );
- 946
- 947 }
- 948
- 949});
- 950
- 951';
- 952
- 953 }
- 954
- 955
- 956
- 957 public function add_pwa_meta_tags() {
- 958
- 959 echo '
- 960
- 961<!-- PWA Meta Tags -->
- 962
- 963<meta name="theme-color" content="' . esc_attr($this->config['theme_color']) . '">
- 964
- 965<meta name="application-name" content="' . esc_attr($this->config['app_name']) . '">
- 966
- 967<meta name="apple-mobile-web-app-capable" content="yes">
- 968
- 969<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
- 970
- 971<meta name="apple-mobile-web-app-title" content="' . esc_attr($this->config['app_short_name']) . '">
- 972
- 973<meta name="msapplication-TileColor" content="' . esc_attr($this->config['theme_color']) . '">
- 974
- 975<meta name="msapplication-TileImage" content="' . plugin_dir_url(__FILE__) . 'assets/icons/icon-144x144.png">
- 976
- 977<!-- Web App Manifest -->
- 978
- 979<link rel="manifest" href="/paranormal-fm-manifest.json">
- 980
- 981<!-- Apple Touch Icons -->
- 982
- 983<link rel="apple-touch-icon" href="' . plugin_dir_url(__FILE__) . 'assets/icons/icon-180x180.png">
- 984
- 985<link rel="apple-touch-icon" sizes="152x152" href="' . plugin_dir_url(__FILE__) . 'assets/icons/icon-152x152.png">
- 986
- 987<link rel="apple-touch-icon" sizes="144x144" href="' . plugin_dir_url(__FILE__) . 'assets/icons/icon-144x144.png">
- 988
- 989<!-- Favicon -->
- 990
- 991<link rel="icon" type="image/png" sizes="32x32" href="' . plugin_dir_url(__FILE__) . 'assets/icons/favicon-32x32.png">
- 992
- 993<link rel="icon" type="image/png" sizes="16x16" href="' . plugin_dir_url(__FILE__) . 'assets/icons/favicon-16x16.png">
- 994
- 995';
- 996
- 997 }
- 998
- 999
- 1000
- 1001 public function add_service_worker_support() {
- 1002
- 1003 $sw_url = home_url('/paranormal-fm-sw.js');
- 1004
- 1005 echo "<script>
- 1006
- 1007 // PWA Service Worker Registration
- 1008
- 1009 if ('serviceWorker' in navigator) {
- 1010
- 1011 window.addEventListener('load', () => {
- 1012
- 1013 navigator.serviceWorker.register('{$sw_url}', { scope: '/' })
- 1014
- 1015 .then((registration) => {
- 1016
- 1017 console.log('Paranormal FM PWA Service Worker registered:', registration);
- 1018
- 1019
- 1020
- 1021 // Check for updates
- 1022
- 1023 registration.addEventListener('updatefound', () => {
- 1024
- 1025 const newWorker = registration.installing;
- 1026
- 1027 newWorker.addEventListener('statechange', () => {
- 1028
- 1029 if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
- 1030
- 1031 // Show update available notification
- 1032
- 1033 if (window.radioPlayer) {
- 1034
- 1035 window.radioPlayer.showUpdateAvailable();
- 1036
- 1037 }
- 1038
- 1039 }
- 1040
- 1041 });
- 1042
- 1043 });
- 1044
- 1045 })
- 1046
- 1047 .catch((error) => {
- 1048
- 1049 console.log('Service Worker registration failed:', error);
- 1050
- 1051 });
- 1052
- 1053 });
- 1054
- 1055 }
- 1056
- 1057
- 1058
- 1059 // PWA Install Prompt
- 1060
- 1061 let deferredPrompt;
- 1062
- 1063 window.addEventListener('beforeinstallprompt', (e) => {
- 1064
- 1065 console.log('PWA install prompt triggered');
- 1066
- 1067 e.preventDefault();
- 1068
- 1069 deferredPrompt = e;
- 1070
- 1071
- 1072
- 1073 // Show custom install button
- 1074
- 1075 if (window.radioPlayer) {
- 1076
- 1077 window.radioPlayer.showInstallPrompt();
- 1078
- 1079 }
- 1080
- 1081 });
- 1082
- 1083
- 1084
- 1085 window.addEventListener('appinstalled', (evt) => {
- 1086
- 1087 console.log('PWA was installed');
- 1088
- 1089 if (window.radioPlayer) {
- 1090
- 1091 window.radioPlayer.hideInstallPrompt();
- 1092
- 1093 }
- 1094
- 1095 });
- 1096
- 1097 </script>";
- 1098
- 1099 }
- 1100
- 1101
- 1102
- 1103 public function enqueue_scripts() {
- 1104
- 1105 if (is_admin()) return;
- 1106
- 1107
- 1108
- 1109 wp_enqueue_script(
- 1110
- 1111 'persistent-radio-player-pwa',
- 1112
- 1113 plugin_dir_url(__FILE__) . 'assets/player-pwa.js',
- 1114
- 1115 [],
- 1116
- 1117 '2.0.0',
- 1118
- 1119 true
- 1120
- 1121 );
- 1122
- 1123
- 1124
- 1125 wp_enqueue_style(
- 1126
- 1127 'persistent-radio-player-pwa',
- 1128
- 1129 plugin_dir_url(__FILE__) . 'assets/player-pwa.css',
- 1130
- 1131 [],
- 1132
- 1133 '2.0.0'
- 1134
- 1135 );
- 1136
- 1137
- 1138
- 1139 wp_enqueue_style(
- 1140
- 1141 'font-awesome',
- 1142
- 1143 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'
- 1144
- 1145 );
- 1146
- 1147
- 1148
- 1149 wp_localize_script('persistent-radio-player-pwa', 'radioPlayerConfig', [
- 1150
- 1151 'streamUrl' => $this->config['stream_url'],
- 1152
- 1153 'apiUrl' => admin_url('admin-ajax.php'),
- 1154
- 1155 'directApiUrl' => $this->config['api_url'],
- 1156
- 1157 'stationName' => $this->config['station_name'],
- 1158
- 1159 'stationId' => $this->config['station_id'],
- 1160
- 1161 'serviceWorkerUrl' => home_url('/paranormal-fm-sw.js'),
- 1162
- 1163 'manifestUrl' => home_url('/paranormal-fm-manifest.json'),
- 1164
- 1165 'offlineUrl' => home_url('/paranormal-fm-offline.html'),
- 1166
- 1167 'appName' => $this->config['app_name'],
- 1168
- 1169 'nonce' => wp_create_nonce('radio_player_nonce'),
- 1170
- 1171 'debug' => WP_DEBUG,
- 1172
- 1173 'isPWA' => true
- 1174
- 1175 ]);
- 1176
- 1177 }
- 1178
- 1179
- 1180
- 1181 public function render_player() {
- 1182
- 1183 if (is_admin()) return;
- 1184
- 1185 echo $this->get_player_html();
- 1186
- 1187 }
- 1188
- 1189
- 1190
- 1191 private function get_player_html() {
- 1192
- 1193 return '
- 1194
- 1195 <div id="radio-player" class="radio-player footer">
- 1196
- 1197 <div class="player-content">
- 1198
- 1199 <div class="player-left">
- 1200
- 1201 <div class="controls-container">
- 1202
- 1203 <div class="controls">
- 1204
- 1205 <button class="control-button play-button" title="Play">
- 1206
- 1207 <i class="fas fa-play"></i>
- 1208
- 1209 </button>
- 1210
- 1211 <button class="control-button volume-button" title="Volume">
- 1212
- 1213 <i class="fas fa-volume-up"></i>
- 1214
- 1215 </button>
- 1216
- 1217 <div class="volume-bar-container">
- 1218
- 1219 <input type="range" class="volume-slider" min="0" max="100" value="100">
- 1220
- 1221 </div>
- 1222
- 1223 <button class="control-button history-button" title="Song History">
- 1224
- 1225 <i class="fas fa-history"></i>
- 1226
- 1227 </button>
- 1228
- 1229 <button class="control-button install-button" title="Install App" style="display: none;">
- 1230
- 1231 <i class="fas fa-download"></i>
- 1232
- 1233 </button>
- 1234
- 1235 </div>
- 1236
- 1237 </div>
- 1238
- 1239 </div>
- 1240
- 1241
- 1242
- 1243 <div class="player-center">
- 1244
- 1245 <div class="album-artwork">
- 1246
- 1247 <img class="artwork-image" src="" alt="Album Art" style="display: none;" />
- 1248
- 1249 <div class="artwork-placeholder">♪</div>
- 1250
- 1251 </div>
- 1252
- 1253 <div class="player-info">
- 1254
- 1255 <div class="song-info">
- 1256
- 1257 <div class="song-name">' . esc_html($this->config['station_name']) . '</div>
- 1258
- 1259 <div class="artist-name">Click play to start streaming</div>
- 1260
- 1261 </div>
- 1262
- 1263 </div>
- 1264
- 1265 </div>
- 1266
- 1267
- 1268
- 1269 <div class="player-right">
- 1270
- 1271 <div class="time-info">
- 1272
- 1273 <span class="elapsed-time">0:00</span>
- 1274
- 1275 <span>/</span>
- 1276
- 1277 <span class="duration-time">0:00</span>
- 1278
- 1279 </div>
- 1280
- 1281 <div class="radio-name">' . esc_html($this->config['station_name']) . '</div>
- 1282
- 1283 <div class="live-container">
- 1284
- 1285 <div class="live-indicator"></div>
- 1286
- 1287 <div class="live-text">LIVE</div>
- 1288
- 1289 </div>
- 1290
- 1291 </div>
- 1292
- 1293 </div>
- 1294
- 1295
- 1296
- 1297 <div class="time-bar-container">
- 1298
- 1299 <div class="time-bar">
- 1300
- 1301 <div class="time-bar-progress"></div>
- 1302
- 1303 </div>
- 1304
- 1305 </div>
- 1306
- 1307
- 1308
- 1309 <!-- PWA Install Banner -->
- 1310
- 1311 <div class="install-banner" style="display: none;">
- 1312
- 1313 <div class="install-banner-content">
- 1314
- 1315 <div class="install-banner-icon">📱</div>
- 1316
- 1317 <div class="install-banner-text">
- 1318
- 1319 <div class="install-banner-title">Install Paranormal FM</div>
- 1320
- 1321 <div class="install-banner-description">Add to your home screen for quick access</div>
- 1322
- 1323 </div>
- 1324
- 1325 <div class="install-banner-actions">
- 1326
- 1327 <button class="install-banner-button install-now">Install</button>
- 1328
- 1329 <button class="install-banner-button install-later">Later</button>
- 1330
- 1331 </div>
- 1332
- 1333 </div>
- 1334
- 1335 </div>
- 1336
- 1337
- 1338
- 1339 <!-- Update Available Banner -->
- 1340
- 1341 <div class="update-banner" style="display: none;">
- 1342
- 1343 <div class="update-banner-content">
- 1344
- 1345 <div class="update-banner-text">
- 1346
- 1347 <div class="update-banner-title">Update Available</div>
- 1348
- 1349 <div class="update-banner-description">A new version is ready</div>
- 1350
- 1351 </div>
- 1352
- 1353 <button class="update-banner-button">Update</button>
- 1354
- 1355 </div>
- 1356
- 1357 </div>
- 1358
- 1359
- 1360
- 1361 <div class="history-modal" style="display: none;">
- 1362
- 1363 <div class="history-modal-overlay"></div>
- 1364
- 1365 <div class="history-modal-content">
- 1366
- 1367 <h3 class="history-modal-title">Song History</h3>
- 1368
- 1369 <button class="history-modal-close">
- 1370
- 1371 <i class="fas fa-times"></i>
- 1372
- 1373 </button>
- 1374
- 1375 <div class="history-list"></div>
- 1376
- 1377 </div>
- 1378
- 1379 </div>
- 1380
- 1381 </div>';
- 1382
- 1383 }
- 1384
- 1385
- 1386
- 1387 public function get_radio_metadata() {
- 1388
- 1389 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'radio_player_nonce')) {
- 1390
- 1391 wp_send_json_error(['message' => 'Security check failed']);
- 1392
- 1393 return;
- 1394
- 1395 }
- 1396
- 1397
- 1398
- 1399 $response = wp_remote_get($this->config['api_url'], [
- 1400
- 1401 'timeout' => 10,
- 1402
- 1403 'sslverify' => false,
- 1404
- 1405 'headers' => [
- 1406
- 1407 'User-Agent' => 'WordPress Radio Player PWA/2.0.0',
- 1408
- 1409 'Accept' => 'application/json'
- 1410
- 1411 ]
- 1412
- 1413 ]);
- 1414
- 1415
- 1416
- 1417 if (is_wp_error($response)) {
- 1418
- 1419 wp_send_json_error([
- 1420
- 1421 'message' => 'Failed to fetch metadata',
- 1422
- 1423 'error' => $response->get_error_message()
- 1424
- 1425 ]);
- 1426
- 1427 return;
- 1428
- 1429 }
- 1430
- 1431
- 1432
- 1433 $body = wp_remote_retrieve_body($response);
- 1434
- 1435 $data = json_decode($body, true);
- 1436
- 1437
- 1438
- 1439 if (json_last_error() !== JSON_ERROR_NONE) {
- 1440
- 1441 wp_send_json_error(['message' => 'Invalid JSON response']);
- 1442
- 1443 return;
- 1444
- 1445 }
- 1446
- 1447
- 1448
- 1449 $processed_data = $this->process_api_data($data);
- 1450
- 1451 wp_send_json_success($processed_data);
- 1452
- 1453 }
- 1454
- 1455
- 1456
- 1457 private function process_api_data($data) {
- 1458
- 1459 $processed = [
- 1460
- 1461 'station' => $data['station'] ?? null,
- 1462
- 1463 'listeners' => $data['listeners'] ?? null,
- 1464
- 1465 'live' => $data['live'] ?? null,
- 1466
- 1467 'is_online' => $data['is_online'] ?? true
- 1468
- 1469 ];
- 1470
- 1471
- 1472
- 1473 if (isset($data['now_playing'])) {
- 1474
- 1475 $now_playing = $data['now_playing'];
- 1476
- 1477 if (is_string($now_playing)) {
- 1478
- 1479 $now_playing = json_decode($now_playing, true);
- 1480
- 1481 }
- 1482
- 1483 $processed['now_playing'] = $now_playing;
- 1484
- 1485 }
- 1486
- 1487
- 1488
- 1489 if (isset($data['song_history'])) {
- 1490
- 1491 $song_history = $data['song_history'];
- 1492
- 1493 if (is_string($song_history)) {
- 1494
- 1495 $song_history = json_decode($song_history, true);
- 1496
- 1497 }
- 1498
- 1499 $processed['song_history'] = $song_history;
- 1500
- 1501 }
- 1502
- 1503
- 1504
- 1505 return $processed;
- 1506
- 1507 }
- 1508
- 1509
- 1510
- 1511 public function add_admin_menu() {
- 1512
- 1513 add_options_page(
- 1514
- 1515 'Radio Player PWA Settings',
- 1516
- 1517 'Radio Player PWA',
- 1518
- 1519 'manage_options',
- 1520
- 1521 'persistent-radio-player-pwa',
- 1522
- 1523 [$this, 'admin_page']
- 1524
- 1525 );
- 1526
- 1527 }
- 1528
- 1529
- 1530
- 1531 public function admin_page() {
- 1532
- 1533 ?>
- 1534
- 1535 <div class="wrap">
- 1536
- 1537 <h1>Radio Player PWA Settings</h1>
- 1538
- 1539
- 1540
- 1541 <div class="notice notice-info">
- 1542
- 1543 <p><strong>PWA Features Active!</strong> Your radio player now supports Progressive Web App installation.</p>
- 1544
- 1545 </div>
- 1546
- 1547
- 1548
- 1549 <h2>Current Configuration</h2>
- 1550
- 1551 <table class="form-table">
- 1552
- 1553 <tr>
- 1554
- 1555 <th>App Name:</th>
- 1556
- 1557 <td><?php echo esc_html($this->config['app_name']); ?></td>
- 1558
- 1559 </tr>
- 1560
- 1561 <tr>
- 1562
- 1563 <th>Stream URL:</th>
- 1564
- 1565 <td><?php echo esc_html($this->config['stream_url']); ?></td>
- 1566
- 1567 </tr>
- 1568
- 1569 <tr>
- 1570
- 1571 <th>API URL:</th>
- 1572
- 1573 <td><?php echo esc_html($this->config['api_url']); ?></td>
- 1574
- 1575 </tr>
- 1576
- 1577 <tr>
- 1578
- 1579 <th>Manifest URL:</th>
- 1580
- 1581 <td><a href="<?php echo home_url('/paranormal-fm-manifest.json'); ?>" target="_blank"><?php echo home_url('/paranormal-fm-manifest.json'); ?></a></td>
- 1582
- 1583 </tr>
- 1584
- 1585 <tr>
- 1586
- 1587 <th>Service Worker URL:</th>
- 1588
- 1589 <td><a href="<?php echo home_url('/paranormal-fm-sw.js'); ?>" target="_blank"><?php echo home_url('/paranormal-fm-sw.js'); ?></a></td>
- 1590
- 1591 </tr>
- 1592
- 1593 </table>
- 1594
- 1595
- 1596
- 1597 <h2>PWA Installation Instructions</h2>
- 1598
- 1599 <div class="card">
- 1600
- 1601 <h3>For Users:</h3>
- 1602
- 1603 <ul>
- 1604
- 1605 <li><strong>Chrome/Edge:</strong> Look for the install icon in the address bar or use the "Install Paranormal FM" button</li>
- 1606
- 1607 <li><strong>Firefox:</strong> Use the "Add to Home Screen" option in the menu</li>
- 1608
- 1609 <li><strong>Safari (iOS):</strong> Use "Add to Home Screen" from the share menu</li>
- 1610
- 1611 <li><strong>Android:</strong> Look for the "Add to Home Screen" prompt or banner</li>
- 1612
- 1613 </ul>
- 1614
- 1615 </div>
- 1616
- 1617
- 1618
- 1619 <h2>Test PWA Features</h2>
- 1620
- 1621 <button id="test-pwa" class="button button-primary">Test PWA Installation</button>
- 1622
- 1623 <button id="test-sw" class="button">Test Service Worker</button>
- 1624
- 1625 <button id="test-manifest" class="button">Test Manifest</button>
- 1626
- 1627 <div id="test-results" style="margin-top: 15px;"></div>
- 1628
- 1629
- 1630
- 1631 <script>
- 1632
- 1633 document.getElementById('test-pwa').addEventListener('click', function() {
- 1634
- 1635 const results = document.getElementById('test-results');
- 1636
- 1637 results.innerHTML = '<p>Testing PWA features...</p>';
- 1638
- 1639
- 1640
- 1641 let checks = [];
- 1642
- 1643
- 1644
- 1645 // Check HTTPS
- 1646
- 1647 if (location.protocol === 'https:') {
- 1648
- 1649 checks.push('✓ HTTPS: Required for PWA');
- 1650
- 1651 } else {
- 1652
- 1653 checks.push('✗ HTTPS: Required for PWA (currently HTTP)');
- 1654
- 1655 }
- 1656
- 1657
- 1658
- 1659 // Check Service Worker
- 1660
- 1661 if ('serviceWorker' in navigator) {
- 1662
- 1663 checks.push('✓ Service Worker: Supported');
- 1664
- 1665 navigator.serviceWorker.getRegistrations().then(registrations => {
- 1666
- 1667 if (registrations.length > 0) {
- 1668
- 1669 checks.push('✓ Service Worker: Registered');
- 1670
- 1671 } else {
- 1672
- 1673 checks.push('âš Service Worker: Not yet registered');
- 1674
- 1675 }
- 1676
- 1677 updateResults();
- 1678
- 1679 });
- 1680
- 1681 } else {
- 1682
- 1683 checks.push('✗ Service Worker: Not supported');
- 1684
- 1685 }
- 1686
- 1687
- 1688
- 1689 // Check Web App Manifest
- 1690
- 1691 const manifest = document.querySelector('link[rel="manifest"]');
- 1692
- 1693 if (manifest) {
- 1694
- 1695 checks.push('✓ Web App Manifest: Found');
- 1696
- 1697 } else {
- 1698
- 1699 checks.push('✗ Web App Manifest: Not found');
- 1700
- 1701 }
- 1702
- 1703
- 1704
- 1705 function updateResults() {
- 1706
- 1707 results.innerHTML = '<h4>PWA Readiness Check:</h4><ul><li>' + checks.join('</li><li>') + '</li></ul>';
- 1708
- 1709 }
- 1710
- 1711
- 1712
- 1713 setTimeout(updateResults, 1000);
- 1714
- 1715 });
- 1716
- 1717
- 1718
- 1719 document.getElementById('test-sw').addEventListener('click', function() {
- 1720
- 1721 fetch('<?php echo home_url('/paranormal-fm-sw.js'); ?>')
- 1722
- 1723 .then(response => {
- 1724
- 1725 document.getElementById('test-results').innerHTML = response.ok ?
- 1726
- 1727 '<p style="color: green;">✓ Service Worker accessible</p>' :
- 1728
- 1729 '<p style="color: red;">✗ Service Worker not accessible</p>';
- 1730
- 1731 })
- 1732
- 1733 .catch(() => {
- 1734
- 1735 document.getElementById('test-results').innerHTML = '<p style="color: red;">✗ Service Worker request failed</p>';
- 1736
- 1737 });
- 1738
- 1739 });
- 1740
- 1741
- 1742
- 1743 document.getElementById('test-manifest').addEventListener('click', function() {
- 1744
- 1745 fetch('<?php echo home_url('/paranormal-fm-manifest.json'); ?>')
- 1746
- 1747 .then(response => response.json())
- 1748
- 1749 .then(data => {
- 1750
- 1751 document.getElementById('test-results').innerHTML =
- 1752
- 1753 '<p style="color: green;">✓ Manifest accessible</p>' +
- 1754
- 1755 '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
- 1756
- 1757 })
- 1758
- 1759 .catch(() => {
- 1760
- 1761 document.getElementById('test-results').innerHTML = '<p style="color: red;">✗ Manifest request failed</p>';
- 1762
- 1763 });
- 1764
- 1765 });
- 1766
- 1767 </script>
- 1768
- 1769 </div>
- 1770
- 1771 <?php
- 1772
- 1773 }
- 1774
- 1775}
- 1776
- 1777new PersistentRadioPlayerPWA();
- 1778
- 1779?>
Raw Paste