myetv-player 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1655 @@
1
+ /*!
2
+ * MYETV Radio Plugin v1.1
3
+ * Themeable Radio Tuner for the MYETV Video Player Open Source
4
+ * Features: API integration, Live Viewers & Stats History Polling
5
+ * https://www.myetv.tv | https://oskarcosimo.com
6
+ */
7
+ (function (global) {
8
+ 'use strict';
9
+
10
+ // ============================================================
11
+ // CSS STYLES
12
+ // ============================================================
13
+ var RADIO_CSS = [
14
+ /* BASE STRUCTURE - Z-INDEX BOOSTED TO 9999 TO PREVENT PLAYER LOADER OVERLAP */
15
+ '.myetv-radio-overlay{position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:2;font-family:"Courier New",monospace;padding:50px 16px;box-sizing:border-box;overflow:hidden;transition:background 0.5s, color 0.5s;}',
16
+ '.myetv-radio-overlay::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(to right,transparent,#b8860b 30%,#f0c040 50%,#b8860b 70%,transparent);}',
17
+
18
+ /* THEME: VINTAGE (Analog Dial) */
19
+ '.theme-vintage{background:linear-gradient(160deg,#0d0600 0%,#1f0e00 40%,#0d0600 100%);color:#f0c040;}',
20
+ '.theme-vintage .radio-analog-dial{background:#050200;border:1px solid #3a2000;box-shadow:inset 0 3px 10px rgba(0,0,0,0.9);}',
21
+ '.theme-vintage .radio-needle{background:linear-gradient(to bottom,#ff6600,#cc2200);box-shadow:0 0 8px rgba(255,80,0,0.7);}',
22
+ '.theme-vintage .radio-tick{color:#5c3a00;}',
23
+ '.theme-vintage .radio-tick.active{color:#f0c040;}',
24
+ '.theme-vintage .radio-station-name{text-shadow:0 0 10px rgba(240,192,64,0.4);}',
25
+
26
+ /* THEME: DIGITAL (Car Stereo LCD) */
27
+ '.theme-digital{background:#050505 !important; color:#00d8ff !important;}',
28
+ '.theme-digital::before{background:linear-gradient(to right,transparent,#005577 30%,#00d8ff 50%,#005577 70%,transparent) !important;}',
29
+ '.theme-digital .radio-info-bar{display:none !important;}',
30
+ '.theme-digital .radio-dial-label{display:none !important;}',
31
+ '.theme-digital .radio-analog-dial{display:none !important;}',
32
+ '.theme-digital .radio-digital-display{display:flex !important;}',
33
+ '.theme-digital .radio-search-input, .theme-digital .radio-ch-input, .theme-digital .radio-search-btn{border-color:#005577 !important; color:#00d8ff !important; background:#00111a !important;}',
34
+ '.theme-digital .radio-search-btn:hover{background:#003344 !important;}',
35
+ '.theme-digital .radio-cb-btn{color:#00d8ff !important;}',
36
+ '.theme-digital .radio-modal-box{background:#050505 !important; border: 1px solid #005577 !important; color:#00d8ff !important;}',
37
+ '.theme-digital .radio-modal-header{border-bottom: 1px solid #003344 !important;}',
38
+ '.theme-digital .radio-modal-search{border-bottom: 1px solid #003344 !important;}',
39
+ '.theme-digital .radio-modal-item{border-bottom:1px solid #00111a !important;}',
40
+ '.theme-digital .radio-modal-item:hover{background:#00111a !important;}',
41
+ '.theme-digital .radio-modal-item.active{border-left-color:#00d8ff !important; background:rgba(0,216,255,0.1) !important; color:#ffffff !important;}',
42
+
43
+ /* DIGITAL LCD DISPLAY STYLES */
44
+ '.radio-digital-display{display:none; flex-direction:column; justify-content:space-between; background:#000000; border:2px solid #222222; border-radius:6px; padding:12px 16px; flex:1; box-shadow:inset 0 0 20px rgba(0,216,255,0.05), 0 5px 15px rgba(0,0,0,0.8); width:100%; box-sizing:border-box;}',
45
+ '.digital-top{display:flex; justify-content:space-between; align-items:center; font-size:12px; font-weight:bold; color:#00d8ff; opacity:0.8;}',
46
+ '.digital-status-wrap{display:flex; align-items:center; gap:8px;}',
47
+ '.digital-status-dot{width:8px; height:8px; border-radius:50%; background:#333; transition:all 0.3s;}',
48
+ '.digital-status-dot.playing{background:#00ff44; box-shadow:0 0 8px #00ff44;}',
49
+ '.digital-status-dot.buffering{background:#ffaa00; box-shadow:0 0 8px #ffaa00; animation:radio-blink 0.5s infinite;}',
50
+ '.digital-status-dot.error{background:#ff0000; box-shadow:0 0 8px #ff0000;}',
51
+ '.digital-badge{background:#00d8ff; color:#000000; padding:2px 6px; border-radius:2px; font-size:10px; letter-spacing:1px;}',
52
+ '.digital-top-right{display:flex; align-items:center; gap:6px;}',
53
+ '.digital-icon-box{background:#00d8ff; border-radius:2px; padding:2px; display:none; align-items:center; justify-content:center; width:14px; height:14px; overflow:hidden;}',
54
+ '.digital-icon-box img{width:100%; height:100%; object-fit:contain;}',
55
+ '.digital-main{font-size:28px; font-weight:bold; color:#00d8ff; margin:0; padding:10px 0; text-align:center; font-family:"Courier New", monospace, sans-serif; letter-spacing:2px; text-transform:uppercase; flex:1; display:flex; align-items:center; justify-content:center; line-height:normal; overflow:hidden; width:100%;}',
56
+ '.digital-main-text{display:inline-block; white-space:nowrap;}',
57
+ '.digital-main.is-scrolling{justify-content:flex-start;}',
58
+ '.digital-main.is-scrolling .digital-main-text{padding-left:100%; animation: radio-marquee 8s linear infinite;}',
59
+ '@keyframes radio-marquee{0%{transform:translateX(0);} 100%{transform:translateX(-100%);}}',
60
+ '.digital-sub{display:flex; justify-content:space-between; align-items:center; font-size:11px; color:#00d8ff;}',
61
+ '.digital-meta{opacity:0.7; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; max-width:60%;}',
62
+ '.digital-controls-row{display:flex; gap:6px;}',
63
+ '.digi-btn{background:transparent; border:1px solid #005577; color:#00d8ff; font-family:inherit; font-weight:bold; font-size:10px; padding:3px 8px; cursor:pointer; border-radius:3px; transition:all 0.2s;}',
64
+ '.digi-btn:hover{background:rgba(0,216,255,0.15); border-color:#00d8ff;}',
65
+
66
+ /* COMMON UI COMPONENTS */
67
+ '.radio-info-bar{text-align:center;margin-bottom:12px;width:100%;max-width:520px;}',
68
+ '.radio-channel-num{font-size:12px;color:#ff4400;letter-spacing:3px;margin-bottom:3px;text-transform:uppercase;}',
69
+ '.radio-station-name{font-size:18px;font-weight:bold;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
70
+ '.radio-station-meta{font-size:11px;color:#7a5200;margin-top:3px;letter-spacing:1px;}',
71
+ '.radio-favicon{width:18px;height:18px;border-radius:3px;vertical-align:middle;margin-right:6px;object-fit:contain;display:none; background:#00d8ff; padding:1px;}',
72
+
73
+ /* DIAL WRAPPER & CONTAINER */
74
+ '.radio-dial-wrapper{width:100%;max-width:520px;position:relative;margin-bottom:10px; display:flex; flex-direction:column; flex:1; min-height:60px; max-height:100px;}',
75
+ '.radio-analog-dial{position:relative;overflow:hidden;flex:1;min-height:50px;cursor:grab;border-radius:6px;transition:border-color 0.3s;}',
76
+ '.radio-dial-label{display:flex;justify-content:space-between;font-size:10px;color:inherit;opacity:0.6;margin-bottom:4px;padding:0 2px;}',
77
+ '.radio-analog-dial:active{cursor:grabbing;}',
78
+ '.radio-analog-dial.loading-state{cursor:wait;}',
79
+ '.radio-analog-dial.error-state{border-color:#ff0000 !important;}',
80
+
81
+ /* HIGH DEFINITION SCALE */
82
+ '.radio-scale{position:absolute;top:0;left:0;display:flex;align-items:flex-end;height:100%;will-change:transform;user-select:none;-webkit-user-select:none;}',
83
+ '.radio-tick{display:flex;flex-direction:column;align-items:center;justify-content:flex-end;width:20px;height:100%;flex-shrink:0;position:relative;}',
84
+ '.radio-tick::after{content:"";position:absolute;bottom:0;width:1px;height:15%;background:currentColor;opacity:0.3;}',
85
+ '.radio-tick.mid::after{height:25%;opacity:0.6;}',
86
+ '.radio-tick.major::after{height:40%;opacity:1;width:2px;}',
87
+ '.radio-tick-num{position:absolute;bottom:45%;font-size:9px;opacity:0.8;}',
88
+
89
+ /* NEEDLE */
90
+ '.radio-needle{position:absolute;left:50%;top:0;bottom:0;width:2px;transform:translateX(-50%);pointer-events:none;z-index:10;}',
91
+ '.radio-needle::before{content:"▼";position:absolute;top:-1px;left:50%;transform:translateX(-50%);font-size:11px;}',
92
+
93
+ /* STATS UI COMPONENTS */
94
+ '.radio-stats-bar{display:flex; justify-content:center; gap:15px; font-size:10px; margin-top:20px; opacity:0.8; z-index:10;}',
95
+ '.radio-stat-item{display:flex; align-items:center; gap:4px; background:rgba(0,0,0,0.4); padding:3px 8px; border-radius:4px; cursor:pointer;}',
96
+ '.radio-stat-item:hover{background:rgba(255,255,255,0.1);}',
97
+ '.radio-stat-live{color:#00ff44;}',
98
+ '.radio-stat-total{color:#00d8ff;}',
99
+
100
+ /* STATS MODAL */
101
+ '.radio-stats-modal-content{padding:15px; font-size:12px; max-height:75vh; overflow-y:auto; scrollbar-width:thin;}',
102
+ '.radio-stats-grid{display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:10px;}',
103
+ '.radio-stats-card{background:rgba(255,255,255,0.05); padding:10px; border-radius:4px; text-align:center; border:1px solid rgba(255,255,255,0.1);}',
104
+ '.radio-stats-value{font-size:18px; font-weight:bold; margin-top:5px; color:#f0c040;}',
105
+ '.theme-digital .radio-stats-value{color:#00d8ff;}',
106
+
107
+ /* SEARCH BAR */
108
+ '.radio-search-bar{display:flex;gap:6px;width:100%;max-width:520px;margin-top:8px;}',
109
+ '.radio-country-select{background:rgba(0,0,0,0.5);border:1px solid currentColor;border-radius:4px;color:inherit;font-family:inherit;font-size:12px;padding:5px;outline:none;opacity:0.8;transition:border-color 0.3s;cursor:pointer;text-transform:uppercase;max-width:70px;}',
110
+ '.radio-country-select option{background:#000;color:#fff;}',
111
+ '.theme-digital .radio-country-select{border-color:#005577 !important; color:#00d8ff !important; background:#00111a !important;}',
112
+ '.radio-search-input{flex:1;background:rgba(0,0,0,0.5);border:1px solid currentColor;border-radius:4px;color:inherit;font-family:inherit;font-size:12px;padding:5px 9px;outline:none;opacity:0.8; transition:border-color 0.3s;}',
113
+ '.radio-search-btn{background:rgba(255,255,255,0.1);border:1px solid currentColor;color:inherit;font-size:11px;padding:5px 10px;border-radius:4px;cursor:pointer;transition:background 0.2s;}',
114
+ '.radio-search-btn:hover{background:rgba(255,255,255,0.2);}',
115
+
116
+ /* FILTER PILLS */
117
+ '.radio-filter-row{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px;max-width:520px;width:100%;}',
118
+ '.radio-filter-pill{background:rgba(0,0,0,0.5);border:1px solid currentColor;color:inherit;font-size:10px;padding:3px 8px;border-radius:12px;cursor:pointer;opacity:0.7;}',
119
+
120
+ /* MODAL WINDOW */
121
+ '.radio-modal-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.88);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:50px 0;box-sizing:border-box;backdrop-filter:blur(4px);}',
122
+ '.radio-modal-box{width:100%;max-height:100%;background:#0d0600;display:flex;flex-direction:column;overflow:hidden;}',
123
+ '.radio-modal-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.1);}',
124
+ '.radio-modal-title{color:inherit;text-transform:uppercase;letter-spacing:2px;font-size:13px;}',
125
+ '.radio-modal-close{background:none;border:1px solid currentColor;color:inherit;font-size:16px;width:28px;height:28px;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s;opacity:0.8;}',
126
+ '.radio-modal-close:hover{opacity:1;}',
127
+ '.radio-modal-search{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.05);flex-shrink:0;}',
128
+ '.radio-modal-search input{width:100%;background:rgba(0,0,0,0.5);border:1px solid currentColor;border-radius:4px;color:inherit;font-family:inherit;font-size:12px;padding:6px 10px;outline:none;box-sizing:border-box;opacity:0.8;}',
129
+ '.radio-modal-list{flex:1;overflow-y:auto;padding:4px 0;scrollbar-width:thin;}',
130
+ '.radio-modal-item{display:flex;align-items:center;gap:10px;padding:10px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,0.05); transition:background 0.2s;}',
131
+ '.radio-modal-item:hover{background:rgba(255,255,255,0.05);}',
132
+ '.radio-modal-item.active{border-left:3px solid currentColor;background:rgba(255,255,255,0.1);}',
133
+ '.radio-modal-ch{font-size:11px;min-width:38px;opacity:0.6;}',
134
+ '.radio-modal-fav{width:16px;height:16px;border-radius:2px;object-fit:contain;flex-shrink:0;display:none; background:#00d8ff; padding:1px;}',
135
+ '.radio-modal-name{font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;}',
136
+ '.radio-modal-cc{font-size:10px;opacity:0.6;flex-shrink:0;}',
137
+ '.radio-modal-count{font-size:10px;padding:6px 14px;opacity:0.6;flex-shrink:0;}',
138
+
139
+ /* STATUS DOT */
140
+ '.radio-status{display:inline-block;width:8px;height:8px;border-radius:50%;background:#333;margin-right:8px;vertical-align:middle; transition:all 0.3s;}',
141
+ '.radio-status.playing{background:#00ff44;box-shadow:0 0 8px #00ff44;animation:radio-blink 2s infinite;}',
142
+ '.radio-status.buffering{background:#ffaa00;box-shadow:0 0 8px #ffaa00;animation:radio-blink 0.5s infinite;}',
143
+ '.radio-status.error{background:#ff0000;box-shadow:0 0 8px #ff0000;}',
144
+ '@keyframes radio-blink{0%,100%{opacity:1;}50%{opacity:0.4;}}',
145
+
146
+ /* CONTROL BAR EXTENSION - ZERO HEIGHT HACK */
147
+ '.radio-controls-center{display:flex !important; align-items:center !important; justify-content:center !important; gap:16px !important; flex:1 !important; height:0px !important; overflow:visible !important; margin:0 !important; padding:0 !important;}',
148
+ '.radio-cb-btn{appearance:none; background:transparent !important; border:none !important; color:#ffffff !important; cursor:pointer !important; display:flex !important; align-items:center !important; justify-content:center !important; gap:5px !important; font-size:11px !important; font-family:"Courier New",monospace !important; font-weight:bold !important; opacity:0.8 !important; padding:0 !important; margin:0 !important; outline:none !important; height:20px !important;}',
149
+ '.radio-cb-btn:hover{opacity:1 !important; color:#00d8ff !important;}',
150
+ '.radio-cb-btn svg{width:14px !important; height:14px !important; display:block !important; fill:currentColor !important; margin:0 !important; padding:0 !important;}',
151
+ '.radio-cb-label{display:block !important; line-height:1 !important; margin:0 !important; padding:0 !important;}',
152
+
153
+ /* CONTROL BAR EXTENSION & PROGRESS BAR HIDER */
154
+ '.radio-controls-center{position:absolute !important; left:50% !important; top:50% !important; transform:translate(-50%, -50%) !important; display:flex !important; align-items:center !important; justify-content:center !important; gap:16px !important; margin:0 !important; padding:0 !important; z-index:10;}',
155
+ '.progress-container, .myetv-progress, .player-progress, .controls-progress, [class*="progress"] { display: none !important; opacity: 0 !important; pointer-events: none !important; }',
156
+
157
+ /* HIDE BUTTONS */
158
+ '.pip-btn { display: none !important; }',
159
+ '.settings-expandable-wrapper:has([data-action="speed_expand"]) { display: none !important; }',
160
+ '.settings-option[data-action="speed_expand"] { display: none !important; }',
161
+
162
+ /* vintage theme responsive */
163
+ '@media(max-height: 480px) {',
164
+ '.radio-info-bar { margin-bottom: 5px !important; }',
165
+ '.radio-dial-wrapper { min-height: 60px !important; max-height: 100px !important; margin-bottom: 5px !important; }',
166
+ '.radio-analog-dial { min-height: 50px !important; }',
167
+ '.radio-search-bar { margin-top: 5px !important; }',
168
+ '.radio-tick-num { bottom: 25px !important; font-size: 8px !important; }',
169
+ '.radio-tick.major::after { height: 25px !important; }',
170
+ '.radio-tick.mid::after { height: 15px !important; }',
171
+ '.radio-tick::after { height: 8px !important; }',
172
+ '}',
173
+
174
+ /* AUDIOWAVE CANVAS */
175
+ '.radio-wave-canvas{position:absolute; bottom:10px; left:5%; width:90%; height:25px; z-index:1; pointer-events:none; opacity:0.3; transition:opacity 0.3s;}',
176
+ '.theme-vintage .radio-wave-canvas{bottom:15px; height:30px; opacity:0.15; z-index:1;}',
177
+ '.theme-digital .radio-wave-canvas{bottom:40px; left:16px; width:calc(100% - 32px); height:20px; opacity:0.25; z-index:1;}',
178
+
179
+ /* RESPONSIVE */
180
+ '@media(max-width:480px){',
181
+ '.radio-ch-label{display:none;}',
182
+ '.radio-ch-input{width:42px;font-size:11px;}',
183
+ '.radio-cb-btn span.radio-cb-label{display:none;}',
184
+ '.radio-analog-dial{min-height:60px;}',
185
+ '.radio-digital-display{min-height:75px; padding:6px 12px;}',
186
+ '.digital-main{font-size:18px;}',
187
+ '.radio-filter-row{display:none;}',
188
+ '}'
189
+ ].join('');
190
+
191
+ // ============================================================
192
+ // HELPER METHODS
193
+ // ============================================================
194
+ function _generateClientId() {
195
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
196
+ }
197
+
198
+ // ============================================================
199
+ // CONSTRUCTOR
200
+ // ============================================================
201
+ function RadioPlugin(player, options) {
202
+ this.player = player;
203
+
204
+ this.options = {
205
+ apiUrl: '',
206
+ proxyUrl: '',
207
+ filter: { search: '', country: '', language: '' },
208
+ theme: (options && options.theme) ? options.theme : 'digital',
209
+ startChannel: 1,
210
+ startCountry: '',
211
+ showAllCountryOption: true,
212
+ defaultIcon: '',
213
+ tagIcons: {},
214
+ statsApiUrl: (options && options.statsApiUrl) ? options.statsApiUrl : '',
215
+ statsPollInterval: (options && options.statsPollInterval) ? options.statsPollInterval : 15000,
216
+ clientId: _generateClientId()
217
+ };
218
+
219
+ if (options) {
220
+ for (var k in options) {
221
+ if (!options.hasOwnProperty(k)) continue;
222
+ if (k === 'filter' && typeof options.filter === 'object' && options.filter) {
223
+ for (var fk in options.filter) {
224
+ if (options.filter.hasOwnProperty(fk)) this.options.filter[fk] = options.filter[fk];
225
+ }
226
+ } else {
227
+ this.options[k] = options[k];
228
+ }
229
+ }
230
+ }
231
+
232
+ /* Internal State Arrays */
233
+ this.stations = [];
234
+ this.baseCountryList = [];
235
+ this.filteredList = [];
236
+
237
+ this.rememberChannel = localStorage.getItem('myetv_radio_remember') === 'true';
238
+ var savedCh = parseInt(localStorage.getItem('myetv_radio_last_ch'), 10);
239
+ var savedCountry = localStorage.getItem('myetv_radio_last_country');
240
+
241
+ this.activeCountry = (this.options.startCountry || '').toUpperCase();
242
+ this.targetAbsoluteCh = -1;
243
+ this.currentChannel = Math.max(1, parseInt(this.options.startChannel) || 1);
244
+
245
+ if (this.rememberChannel) {
246
+ if (!isNaN(savedCh)) this.targetAbsoluteCh = savedCh;
247
+ if (savedCountry !== null) this.activeCountry = savedCountry.toUpperCase();
248
+ }
249
+
250
+ /* Stats Variables */
251
+ this._statsIntervalId = null;
252
+ this.currentStats = { live: 0, total: 0, daily: 0, weekly: 0, monthly: 0, yearly: 0 };
253
+
254
+ this._wakeLock = null;
255
+ this._isRadioPlaying = (player.options && player.options.autoplay) || (player.video && player.video.autoplay) || false;
256
+ this._tuneId = 0;
257
+
258
+ this.TICK_W = 20;
259
+ this.isDragging = false;
260
+ this.startX = 0;
261
+ this.offsetX = 0;
262
+ this.lastOffset = 0;
263
+
264
+ /* Timeouts */
265
+ this._searchTimeout = null;
266
+ this._bufferTimeout = null;
267
+ this._tuneTimeout = null;
268
+
269
+ /* DOM Elements */
270
+ this.overlay = null;
271
+ this.dialContainer = null;
272
+ this.scaleEl = null;
273
+ this.channelNumEl = null;
274
+ this.stationNameEl = null;
275
+ this.stationMetaEl = null;
276
+ this.statusDot = null;
277
+ this.digiChEl = null;
278
+ this.digiMainEl = null;
279
+ this.digiMetaEl = null;
280
+ this.digiStatusDot = null;
281
+ this.searchInput = null;
282
+ this.errMsgEl = null;
283
+ this.faviconEl = null;
284
+ this.countrySelectEl = null;
285
+
286
+ this.liveStatsEl = null;
287
+ this.totalStatsEl = null;
288
+ this._statsModalOverlay = null;
289
+ }
290
+
291
+ // ============================================================
292
+ // INITIALIZATION
293
+ // ============================================================
294
+ RadioPlugin.prototype.setup = function () {
295
+ this._injectCSS();
296
+ this._buildOverlay();
297
+ this._hookPlayerEvents();
298
+ this._bindKeyboard();
299
+
300
+ var self = this;
301
+ setTimeout(function () {
302
+ self._buildControlBar();
303
+ self._buildModal();
304
+ self._buildStatsModal();
305
+
306
+ if (typeof self.player.populateSettingsMenu === 'function') {
307
+ self.player.populateSettingsMenu();
308
+ }
309
+ self._injectSettingsMenu();
310
+
311
+ }, 100);
312
+
313
+ this._loadStations();
314
+
315
+ document.addEventListener('visibilitychange', function () {
316
+ if (document.visibilityState === 'visible') {
317
+ self._manageWakeLock(self._isRadioPlaying);
318
+
319
+ if (self.dialContainer && self.dialContainer.classList.contains('error-state')) {
320
+ var s = self.filteredList[self.currentChannel - 1];
321
+ if (s) {
322
+ self._setDigitalName('RECONNECTING...');
323
+ self._setStatus('buffering');
324
+ self.dialContainer.classList.remove('error-state');
325
+
326
+ setTimeout(function () {
327
+ self._prepareStream(s, true);
328
+ }, 1500);
329
+ }
330
+ }
331
+ }
332
+ });
333
+ this._startWaveAnimation();
334
+ };
335
+
336
+ RadioPlugin.prototype._injectCSS = function () {
337
+ var existingStyle = document.getElementById('myetv-radio-plugin-css');
338
+ if (existingStyle) { existingStyle.textContent = RADIO_CSS; return; }
339
+ var s = document.createElement('style');
340
+ s.id = 'myetv-radio-plugin-css';
341
+ s.textContent = RADIO_CSS;
342
+ document.head.appendChild(s);
343
+ };
344
+
345
+ // ============================================================
346
+ // BUILD OVERLAY UI
347
+ // ============================================================
348
+ RadioPlugin.prototype._buildOverlay = function () {
349
+ var container = this.player.container;
350
+ if (!container) return;
351
+
352
+ var ov = document.createElement('div');
353
+ ov.className = 'myetv-radio-overlay theme-' + this.options.theme;
354
+
355
+ /* --- Header Info (Vintage Theme) --- */
356
+ var infoBar = document.createElement('div');
357
+ infoBar.className = 'radio-info-bar';
358
+
359
+ var chNum = document.createElement('div');
360
+ chNum.className = 'radio-channel-num';
361
+ chNum.innerHTML = '<span class="radio-status"></span>CH ' + this.currentChannel;
362
+ this.channelNumEl = chNum;
363
+
364
+ var stName = document.createElement('div');
365
+ stName.className = 'radio-station-name';
366
+ stName.textContent = 'Loading stations...';
367
+ this.stationNameEl = stName;
368
+
369
+ var stMeta = document.createElement('div');
370
+ stMeta.className = 'radio-station-meta';
371
+ stMeta.innerHTML = '<img class="radio-favicon" src=""/><span class="radio-meta-text"></span>';
372
+ this.stationMetaEl = stMeta;
373
+ this.faviconEl = stMeta.querySelector('.radio-favicon');
374
+ this.statusDot = chNum.querySelector('.radio-status');
375
+
376
+ infoBar.appendChild(chNum);
377
+ infoBar.appendChild(stName);
378
+ infoBar.appendChild(stMeta);
379
+ ov.appendChild(infoBar);
380
+
381
+ /* --- Tuning Dial Wrapper --- */
382
+ var dialWrapper = document.createElement('div');
383
+ dialWrapper.className = 'radio-dial-wrapper';
384
+ var dialLabel = document.createElement('div');
385
+ dialLabel.className = 'radio-dial-label';
386
+ dialLabel.innerHTML = '<span>&#9668; PREV</span><span>DRAG TO TUNE</span><span>NEXT &#9658;</span>';
387
+ dialWrapper.appendChild(dialLabel);
388
+
389
+ /* 1. ANALOG DIAL (Vintage) */
390
+ var dialContainer = document.createElement('div');
391
+ dialContainer.className = 'radio-analog-dial loading-state';
392
+
393
+ var scale = document.createElement('div');
394
+ scale.className = 'radio-scale';
395
+ this.scaleEl = scale;
396
+
397
+ var needle = document.createElement('div');
398
+ needle.className = 'radio-needle';
399
+
400
+ var errMsg = document.createElement('div');
401
+ errMsg.className = 'radio-error-msg';
402
+ errMsg.style.cssText = 'display:none; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:11px; font-weight:bold; letter-spacing:2px; pointer-events:none; text-shadow:0 0 5px black; color:red;';
403
+ this.errMsgEl = errMsg;
404
+
405
+ dialContainer.appendChild(scale);
406
+ dialContainer.appendChild(needle);
407
+ dialContainer.appendChild(errMsg);
408
+ dialWrapper.appendChild(dialContainer);
409
+
410
+ /* 2. DIGITAL STEREO DISPLAY (Digital) */
411
+ var digitalDisplay = document.createElement('div');
412
+ digitalDisplay.className = 'radio-digital-display';
413
+ digitalDisplay.innerHTML = `
414
+ <div class="digital-top">
415
+ <div class="digital-status-wrap">
416
+ <div class="digital-status-dot"></div>
417
+ <span class="digital-ch">CH --</span>
418
+ </div>
419
+ <div class="digital-top-right">
420
+ <div class="digital-icon-box"><img class="digi-top-icon" src="" /></div>
421
+ <span class="digital-badge">DAB+ / NET</span>
422
+ </div>
423
+ </div>
424
+ <div class="digital-main"><span class="digital-main-text">LOADING...</span></div>
425
+ <div class="digital-sub">
426
+ <span class="digital-meta">PLEASE WAIT</span>
427
+ <div class="digital-controls-row">
428
+ <button class="digi-btn prev">◁ TUNE</button>
429
+ <button class="digi-btn next">TUNE ▷</button>
430
+ <button class="digi-btn list">RADIO LIST ≣</button>
431
+ </div>
432
+ </div>
433
+ `;
434
+
435
+ this.digiChEl = digitalDisplay.querySelector('.digital-ch');
436
+ this.digiMainEl = digitalDisplay.querySelector('.digital-main');
437
+ this.digiMainTextEl = digitalDisplay.querySelector('.digital-main-text');
438
+ this.digiMetaEl = digitalDisplay.querySelector('.digital-meta');
439
+ this.digiStatusDot = digitalDisplay.querySelector('.digital-status-dot');
440
+ this.digiTopIconBoxEl = digitalDisplay.querySelector('.digital-icon-box');
441
+ this.digiTopIconEl = digitalDisplay.querySelector('.digi-top-icon');
442
+
443
+ var dPrev = digitalDisplay.querySelector('.digi-btn.prev');
444
+ var dNext = digitalDisplay.querySelector('.digi-btn.next');
445
+ var dList = digitalDisplay.querySelector('.digi-btn.list');
446
+
447
+ var self = this;
448
+ dPrev.addEventListener('click', function (e) {
449
+ e.stopPropagation();
450
+ self._positionTo(Math.max(1, self.currentChannel - 1), self._isRadioPlaying);
451
+ });
452
+ dNext.addEventListener('click', function (e) {
453
+ e.stopPropagation();
454
+ var max = self.filteredList.length || 1;
455
+ self._positionTo(Math.min(max, self.currentChannel + 1), self._isRadioPlaying);
456
+ });
457
+ dList.addEventListener('click', function (e) {
458
+ e.stopPropagation();
459
+ self._openModal();
460
+ });
461
+
462
+ dialWrapper.appendChild(digitalDisplay);
463
+
464
+ /* --- Audiowave Canvas --- */
465
+ var waveCanvas = document.createElement('canvas');
466
+ waveCanvas.className = 'radio-wave-canvas';
467
+ dialWrapper.appendChild(waveCanvas);
468
+ this.waveCanvas = waveCanvas;
469
+ this.waveCtx = waveCanvas.getContext('2d');
470
+
471
+ /* --- Stats Bar --- */
472
+ var statsBar = document.createElement('div');
473
+ statsBar.className = 'radio-stats-bar';
474
+
475
+ var liveStats = document.createElement('div');
476
+ liveStats.className = 'radio-stat-item radio-stat-live';
477
+ liveStats.innerHTML = '<span class="radio-status playing" style="width:6px;height:6px;margin:0;"></span> <span class="val">0</span> LIVE';
478
+ this.liveStatsEl = liveStats.querySelector('.val');
479
+
480
+ // Total views only in the main bar
481
+ var totalStats = document.createElement('div');
482
+ totalStats.className = 'radio-stat-item radio-stat-total';
483
+ totalStats.innerHTML = '&#128065; <span class="val-t">0</span> VIEWS';
484
+ this.hitsStatsEl = totalStats.querySelector('.val-t');
485
+
486
+ var self = this;
487
+ totalStats.addEventListener('click', function (e) {
488
+ e.stopPropagation();
489
+ self._openStatsModal();
490
+ });
491
+
492
+ liveStats.addEventListener('click', function (e) {
493
+ e.stopPropagation();
494
+ self._openStatsModal();
495
+ });
496
+
497
+ statsBar.appendChild(liveStats);
498
+ statsBar.appendChild(totalStats);
499
+ dialWrapper.appendChild(statsBar);
500
+
501
+ ov.appendChild(dialWrapper);
502
+
503
+ /* --- Search Bar & Country Select --- */
504
+ var searchBar = document.createElement('div');
505
+ searchBar.className = 'radio-search-bar';
506
+
507
+ var countrySelect = document.createElement('select');
508
+ countrySelect.className = 'radio-country-select';
509
+ this.countrySelectEl = countrySelect;
510
+
511
+ var searchInput = document.createElement('input');
512
+ searchInput.type = 'text';
513
+ searchInput.className = 'radio-search-input';
514
+ searchInput.placeholder = 'Search station name...';
515
+ this.searchInput = searchInput;
516
+
517
+ var searchBtn = document.createElement('button');
518
+ searchBtn.className = 'radio-search-btn';
519
+ searchBtn.textContent = 'SEARCH';
520
+
521
+ searchBar.appendChild(countrySelect);
522
+ searchBar.appendChild(searchInput);
523
+ searchBar.appendChild(searchBtn);
524
+ ov.appendChild(searchBar);
525
+
526
+ /* Insert overlay */
527
+ var controls = this.player.controls;
528
+ if (controls && controls.parentNode === container) {
529
+ container.insertBefore(ov, controls);
530
+ } else {
531
+ container.appendChild(ov);
532
+ }
533
+
534
+ this.overlay = ov;
535
+ this.dialContainer = dialContainer;
536
+
537
+ this._bindDialEvents(dialContainer);
538
+ this._bindSearchEvents(searchInput, searchBtn);
539
+ };
540
+
541
+ RadioPlugin.prototype._setDigitalName = function (name) {
542
+ if (!this.digiMainEl || !this.digiMainTextEl) return;
543
+
544
+ this.digiMainTextEl.textContent = name;
545
+ this.digiMainEl.classList.remove('is-scrolling');
546
+
547
+ var self = this;
548
+ setTimeout(function () {
549
+ if (self.digiMainTextEl.scrollWidth > self.digiMainEl.clientWidth) {
550
+ self.digiMainEl.classList.add('is-scrolling');
551
+ }
552
+ }, 50);
553
+ };
554
+
555
+ // ============================================================
556
+ // SIMULATED AUDIOWAVE VISUALIZER
557
+ // ============================================================
558
+ RadioPlugin.prototype._startWaveAnimation = function () {
559
+ var self = this;
560
+ var canvas = this.waveCanvas;
561
+ var ctx = this.waveCtx;
562
+ var bars = 32;
563
+ var phase = 0;
564
+
565
+ function draw() {
566
+ requestAnimationFrame(draw);
567
+ if (!canvas.offsetWidth) return;
568
+ if (canvas.width !== canvas.offsetWidth) canvas.width = canvas.offsetWidth;
569
+ if (canvas.height !== canvas.offsetHeight) canvas.height = canvas.offsetHeight;
570
+
571
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
572
+
573
+ var isDigital = self.options.theme === 'digital';
574
+ ctx.fillStyle = isDigital ? '#00d8ff' : '#ff6600';
575
+
576
+ var barWidth = Math.floor(canvas.width / bars);
577
+ var gap = 2;
578
+
579
+ for (var i = 0; i < bars; i++) {
580
+ var height = 2;
581
+
582
+ if (self._isRadioPlaying) {
583
+ var noise = Math.sin(phase + i * 0.4) * Math.cos(phase * 0.7 + i * 0.2);
584
+ var beat = Math.random() * 0.4;
585
+ var normalized = Math.abs(noise + beat);
586
+ height = Math.max(2, normalized * canvas.height);
587
+ }
588
+
589
+ var x = i * barWidth;
590
+ var y = canvas.height - height;
591
+ ctx.fillRect(x + gap / 2, y, barWidth - gap, height);
592
+ }
593
+
594
+ if (self._isRadioPlaying) phase += 0.15;
595
+ }
596
+ draw();
597
+ };
598
+
599
+ // ============================================================
600
+ // SCALE GENERATION
601
+ // ============================================================
602
+ RadioPlugin.prototype._buildScale = function (total) {
603
+ if (!this.scaleEl) return;
604
+ this.scaleEl.innerHTML = '';
605
+ for (var i = 1; i <= total; i++) {
606
+ var tick = document.createElement('div');
607
+ var type = (i % 10 === 0) ? ' major' : (i % 5 === 0 ? ' mid' : '');
608
+ tick.className = 'radio-tick' + type;
609
+ tick.dataset.ch = i;
610
+ if (i % 10 === 0) {
611
+ var label = document.createElement('span');
612
+ label.className = 'radio-tick-num';
613
+ label.textContent = i;
614
+ tick.appendChild(label);
615
+ }
616
+ this.scaleEl.appendChild(tick);
617
+ }
618
+ var padding = this.dialContainer.offsetWidth / 2;
619
+ this.scaleEl.style.paddingLeft = padding + 'px';
620
+ this.scaleEl.style.paddingRight = padding + 'px';
621
+ };
622
+
623
+ // ============================================================
624
+ // NAVIGATION & SAFE DEBOUNCE LOGIC
625
+ // ============================================================
626
+ RadioPlugin.prototype._positionTo = function (ch, autoPlay, skipDelay) {
627
+ var list = this.filteredList;
628
+ ch = Math.max(1, Math.min(ch, list.length || 1));
629
+ this.currentChannel = ch;
630
+ this._tuneId++;
631
+
632
+ var playIntent = autoPlay || this._isRadioPlaying || (this.player.video && !this.player.video.paused);
633
+ this._isRadioPlaying = playIntent;
634
+
635
+ var targetX = -((ch - 1) * this.TICK_W);
636
+ if (this.scaleEl) {
637
+ this.scaleEl.style.transition = this.isDragging ? 'none' : 'transform 0.2s ease-out';
638
+ this.scaleEl.style.transform = 'translateX(' + targetX + 'px)';
639
+
640
+ var prev = this.scaleEl.querySelector('.radio-tick.active');
641
+ if (prev) prev.classList.remove('active');
642
+ var cur = this.scaleEl.querySelector('[data-ch="' + ch + '"]');
643
+ if (cur) cur.classList.add('active');
644
+ }
645
+ this.offsetX = targetX;
646
+
647
+ var s = list[ch - 1];
648
+ if (s) {
649
+ var absoluteCh = this.stations.indexOf(s) + 1;
650
+ var displayCh = this.baseCountryList.indexOf(s) + 1;
651
+
652
+ if (this.rememberChannel) {
653
+ localStorage.setItem('myetv_radio_last_ch', absoluteCh);
654
+ }
655
+
656
+ var stationName = s.name || 'UNKNOWN';
657
+
658
+ if (this.stationNameEl) this.stationNameEl.textContent = stationName;
659
+ if (this.channelNumEl) {
660
+ this.channelNumEl.innerHTML = '<span class="radio-status"></span>CH ' + displayCh;
661
+ this.statusDot = this.channelNumEl.querySelector('.radio-status');
662
+ }
663
+
664
+ if (this.digiChEl) this.digiChEl.textContent = 'CH ' + displayCh;
665
+ this._setDigitalName(stationName);
666
+
667
+ var metaParts = [];
668
+ if (s.countrycode) metaParts.push(s.countrycode.toUpperCase());
669
+ if (s.tags) metaParts.push(s.tags.split(',')[0].trim());
670
+ var metaString = metaParts.join(' · ');
671
+
672
+ var iconUrl = this._getStationIcon(s);
673
+
674
+ if (this.digiTopIconBoxEl && this.digiTopIconEl) {
675
+ if (iconUrl) {
676
+ this.digiTopIconBoxEl.style.display = 'flex';
677
+ this.digiTopIconEl.onload = function () { this.style.display = 'block'; };
678
+ this.digiTopIconEl.onerror = function () { this.parentElement.style.display = 'none'; };
679
+ this.digiTopIconEl.src = iconUrl;
680
+ } else {
681
+ this.digiTopIconBoxEl.style.display = 'none';
682
+ this.digiTopIconEl.src = '';
683
+ }
684
+ }
685
+
686
+ if (this.stationMetaEl) {
687
+ this.stationMetaEl.querySelector('.radio-meta-text').textContent = metaString;
688
+ if (this.faviconEl) {
689
+ if (iconUrl) {
690
+ this.faviconEl.style.display = 'none';
691
+ this.faviconEl.onload = function () { this.style.display = 'inline-block'; };
692
+ this.faviconEl.onerror = function () { this.style.display = 'none'; };
693
+ this.faviconEl.src = iconUrl;
694
+ } else {
695
+ this.faviconEl.style.display = 'none';
696
+ this.faviconEl.src = '';
697
+ }
698
+ }
699
+ }
700
+ if (this.digiMetaEl) this.digiMetaEl.textContent = metaString || 'FM RADIO';
701
+ if (typeof this.player.setVideoTitle === 'function') this.player.setVideoTitle(stationName);
702
+
703
+ if ('mediaSession' in navigator) {
704
+ navigator.mediaSession.metadata = new MediaMetadata({
705
+ title: stationName,
706
+ artist: metaString || 'MYETV Radio',
707
+ album: 'Radio Player',
708
+ artwork: (iconUrl) ? [{ src: iconUrl, sizes: '192x192', type: 'image/png' }] : []
709
+ });
710
+ }
711
+
712
+ if (this._tuneTimeout && typeof this._tuneTimeout !== 'string') {
713
+ clearTimeout(this._tuneTimeout);
714
+ }
715
+ this._clearBufferTimeout();
716
+ this._clearError();
717
+
718
+ this._tuneTimeout = "zapping";
719
+
720
+ var video = this.player.video;
721
+ if (video) {
722
+ try { video.pause(); } catch (e) { }
723
+ }
724
+
725
+ if (skipDelay) {
726
+ this._tuneTimeout = null;
727
+ this._prepareStream(s, playIntent);
728
+ } else {
729
+ this._setStatus('buffering');
730
+ var self = this;
731
+ this._tuneTimeout = setTimeout(function () {
732
+ self._tuneTimeout = null;
733
+ self._prepareStream(s, playIntent);
734
+ }, 1200);
735
+ }
736
+
737
+ // Start polling stats for the current station
738
+ this._startStatsPolling(s);
739
+ }
740
+ };
741
+
742
+ RadioPlugin.prototype._bindDialEvents = function (container) {
743
+ var self = this;
744
+ container.addEventListener('pointerdown', function (e) {
745
+ self.isDragging = true;
746
+ self.startX = e.clientX;
747
+ self.lastOffset = self.offsetX;
748
+ if (self.scaleEl) self.scaleEl.style.transition = 'none';
749
+ container.setPointerCapture(e.pointerId);
750
+ });
751
+
752
+ container.addEventListener('pointermove', function (e) {
753
+ if (!self.isDragging) return;
754
+ self.offsetX = self.lastOffset + (e.clientX - self.startX);
755
+ if (self.scaleEl) self.scaleEl.style.transform = 'translateX(' + self.offsetX + 'px)';
756
+
757
+ var ch = Math.max(1, Math.round(-self.offsetX / self.TICK_W) + 1);
758
+ ch = Math.min(ch, self.filteredList.length || 1);
759
+ var s = self.filteredList[ch - 1];
760
+
761
+ if (s) {
762
+ var displayCh = self.baseCountryList.indexOf(s) + 1;
763
+ var sName = s.name || 'UNKNOWN';
764
+ if (self.stationNameEl) self.stationNameEl.textContent = sName;
765
+ self._setDigitalName(sName);
766
+ if (self.channelNumEl) self.channelNumEl.innerHTML = '<span class="radio-status"></span>CH ' + displayCh;
767
+ if (self.digiChEl) self.digiChEl.textContent = 'CH ' + displayCh;
768
+ }
769
+ });
770
+
771
+ container.addEventListener('pointerup', function () {
772
+ if (!self.isDragging) return;
773
+ self.isDragging = false;
774
+ var ch = Math.max(1, Math.round(-self.offsetX / self.TICK_W) + 1);
775
+ self._positionTo(ch, self._isRadioPlaying);
776
+ });
777
+
778
+ container.addEventListener('pointercancel', function () {
779
+ self.isDragging = false;
780
+ });
781
+ };
782
+
783
+ // ============================================================
784
+ // DATA & STREAM
785
+ // ============================================================
786
+ RadioPlugin.prototype._buildApiUrl = function () {
787
+ var url = this.options.apiUrl;
788
+ var params = [];
789
+ if (this.options.filter.language) params.push('language=' + encodeURIComponent(this.options.filter.language));
790
+ if (params.length) url += (url.indexOf('?') !== -1 ? '&' : '?') + params.join('&');
791
+ return url;
792
+ };
793
+
794
+ RadioPlugin.prototype._updateBaseList = function () {
795
+ var cf = (this.activeCountry || '').toLowerCase();
796
+ this.baseCountryList = !cf ? this.stations.slice() : this.stations.filter(function (s) {
797
+ return (s.countrycode || '').toLowerCase() === cf;
798
+ });
799
+ };
800
+
801
+ RadioPlugin.prototype._applyTextSearch = function (q) {
802
+ var low = (q || '').toLowerCase();
803
+ this.filteredList = !low ? this.baseCountryList.slice() : this.baseCountryList.filter(function (s) {
804
+ return (s.name || '').toLowerCase().indexOf(low) !== -1;
805
+ });
806
+ };
807
+
808
+ RadioPlugin.prototype._loadStations = function () {
809
+ var self = this;
810
+ fetch(this._buildApiUrl())
811
+ .then(function (r) { return r.json(); })
812
+ .then(function (data) {
813
+ self.stations = data || [];
814
+
815
+ if (self.countrySelectEl && self.countrySelectEl.options.length === 0) {
816
+ var uniqueCountries = [];
817
+ self.stations.forEach(function (s) {
818
+ var cc = (s.countrycode || '').toUpperCase();
819
+ if (cc && uniqueCountries.indexOf(cc) === -1) uniqueCountries.push(cc);
820
+ });
821
+ uniqueCountries.sort();
822
+
823
+ if (self.options.showAllCountryOption) {
824
+ var optAll = document.createElement('option');
825
+ optAll.value = ''; optAll.textContent = 'ALL';
826
+ self.countrySelectEl.appendChild(optAll);
827
+ }
828
+
829
+ uniqueCountries.forEach(function (cc) {
830
+ var opt = document.createElement('option');
831
+ opt.value = cc; opt.textContent = cc;
832
+ self.countrySelectEl.appendChild(opt);
833
+ });
834
+
835
+ var initC = self.activeCountry;
836
+ if (initC && uniqueCountries.indexOf(initC) !== -1) {
837
+ self.countrySelectEl.value = initC;
838
+ } else if (!self.options.showAllCountryOption && uniqueCountries.length > 0) {
839
+ self.countrySelectEl.value = uniqueCountries[0];
840
+ self.activeCountry = uniqueCountries[0];
841
+ } else {
842
+ self.countrySelectEl.value = '';
843
+ self.activeCountry = '';
844
+ }
845
+ }
846
+
847
+ self._updateBaseList();
848
+ self._applyTextSearch(self.options.filter.search || (self.searchInput ? self.searchInput.value.trim() : ''));
849
+
850
+ var targetRel = Math.max(1, parseInt(self.options.startChannel) || 1);
851
+
852
+ if (self.targetAbsoluteCh !== -1 && self.stations.length > 0) {
853
+ var targetAbsIndex = Math.min(Math.max(1, self.targetAbsoluteCh), self.stations.length) - 1;
854
+ var targetStation = self.stations[targetAbsIndex];
855
+ var foundIdx = self.filteredList.indexOf(targetStation);
856
+ if (foundIdx !== -1) targetRel = foundIdx + 1;
857
+ }
858
+
859
+ setTimeout(function () {
860
+ self._buildScale(self.filteredList.length);
861
+ self._positionTo(targetRel, false, true);
862
+ }, 100);
863
+ })
864
+ .catch(function (err) {
865
+ if (self.stationNameEl) self.stationNameEl.textContent = 'API Unavailable';
866
+ if (self.digiMainEl) self.digiMainEl.textContent = 'NO SIGNAL';
867
+ });
868
+ };
869
+
870
+ RadioPlugin.prototype._resolveUrl = function (url) {
871
+ if (!url) return '';
872
+ if (this.options.proxyUrl && url.indexOf('http://') === 0) return this.options.proxyUrl + encodeURIComponent(url);
873
+ return url;
874
+ };
875
+
876
+ RadioPlugin.prototype._getStationIcon = function (station) {
877
+ if (station && station.tags) {
878
+ var firstTag = station.tags.split(',')[0].trim().toLowerCase();
879
+ if (this.options.tagIcons && this.options.tagIcons[firstTag]) {
880
+ return this.options.tagIcons[firstTag];
881
+ }
882
+ }
883
+ return this.options.defaultIcon || (station ? station.favicon : '') || '';
884
+ };
885
+
886
+ RadioPlugin.prototype._prepareStream = function (station, play) {
887
+ var video = this.player.video;
888
+ if (!video || !station) return;
889
+
890
+ var src = this._resolveUrl(station.url_resolved || station.url || '');
891
+ if (!src) {
892
+ this._setError('INVALID URL');
893
+ return;
894
+ }
895
+
896
+ try {
897
+ video.pause();
898
+ video.removeAttribute('src');
899
+ video.load();
900
+ } catch (e) { }
901
+
902
+ this._clearError();
903
+ this._isRadioPlaying = play;
904
+
905
+ var self = this;
906
+ var currentTuneId = this._tuneId;
907
+
908
+ setTimeout(function () {
909
+ if (self._tuneId !== currentTuneId) return;
910
+
911
+ video.src = src;
912
+ video.load();
913
+
914
+ if (play) {
915
+ self._setStatus('buffering');
916
+ self._bufferTimeout = setTimeout(function () {
917
+ if (video.readyState < 3) self._setError('STREAM UNAVAILABLE');
918
+ }, 10000);
919
+
920
+ var playPromise = video.play();
921
+ if (playPromise !== undefined) {
922
+ playPromise.then(function () {
923
+ if (self._tuneId !== currentTuneId) return;
924
+ self._clearBufferTimeout();
925
+ self._clearError();
926
+ self._setStatus('playing');
927
+
928
+ // [CRITICAL FIX] Force screen wake lock immediately when stream successfully starts
929
+ self._manageWakeLock(true);
930
+
931
+ }).catch(function (err) {
932
+ if (self._tuneId !== currentTuneId) return;
933
+ if (err.name !== 'AbortError') {
934
+ self._setError('PLAYBACK ERROR');
935
+ // Release wake lock on playback error
936
+ self._manageWakeLock(false);
937
+ }
938
+ });
939
+ } else {
940
+ // Fallback for older engines that do not return a Promise
941
+ self._setStatus('playing');
942
+ self._manageWakeLock(true);
943
+ }
944
+ } else {
945
+ // Explicitly release wake lock if we prepare a stream without playing it
946
+ self._manageWakeLock(false);
947
+ }
948
+ }, 50);
949
+ };
950
+
951
+ RadioPlugin.prototype._setStatus = function (state) {
952
+ if (this.statusDot) this.statusDot.className = 'radio-status' + (state ? ' ' + state : '');
953
+ if (this.digiStatusDot) this.digiStatusDot.className = 'digital-status-dot' + (state ? ' ' + state : '');
954
+ };
955
+
956
+ RadioPlugin.prototype._setError = function (msg) {
957
+ this._setStatus('error');
958
+ if (this.dialContainer) {
959
+ this.dialContainer.classList.remove('loading-state');
960
+ this.dialContainer.classList.add('error-state');
961
+ }
962
+ if (this.errMsgEl) {
963
+ this.errMsgEl.textContent = msg || 'ERROR';
964
+ this.errMsgEl.style.display = 'block';
965
+ }
966
+ if (this.digiMainEl && this.digiMainTextEl) {
967
+ this.digiMainEl.classList.remove('is-scrolling');
968
+ this.digiMainTextEl.textContent = msg || 'CONNECTION ERROR';
969
+ }
970
+ if (this.player && this.player.video) {
971
+ try {
972
+ this.player.video.pause();
973
+ this.player.video.removeAttribute('src');
974
+ this.player.video.load();
975
+ this.player.video.dispatchEvent(new Event('pause'));
976
+ this.player.video.dispatchEvent(new Event('abort'));
977
+ } catch (e) { }
978
+ }
979
+ };
980
+
981
+ RadioPlugin.prototype._clearError = function () {
982
+ if (this.dialContainer) this.dialContainer.classList.remove('error-state');
983
+ if (this.errMsgEl) this.errMsgEl.style.display = 'none';
984
+
985
+ var s = this.filteredList[this.currentChannel - 1];
986
+ if (s) {
987
+ this._setDigitalName(s.name || 'UNKNOWN');
988
+ }
989
+ };
990
+
991
+ RadioPlugin.prototype._clearBufferTimeout = function () {
992
+ if (this._bufferTimeout) {
993
+ clearTimeout(this._bufferTimeout);
994
+ this._bufferTimeout = null;
995
+ }
996
+ };
997
+
998
+ RadioPlugin.prototype._manageWakeLock = function (play) {
999
+ var self = this;
1000
+
1001
+ // 1. Try Native Android Bridge first
1002
+ if (window.AndroidBridge && typeof window.AndroidBridge.setKeepScreenOn === 'function') {
1003
+ window.AndroidBridge.setKeepScreenOn(play);
1004
+ return;
1005
+ }
1006
+
1007
+ // 2. Try Standard HTML5 WakeLock API
1008
+ if ('wakeLock' in navigator) {
1009
+ if (play) {
1010
+ if (!this._wakeLock) {
1011
+ navigator.wakeLock.request('screen').then(function (lock) {
1012
+ self._wakeLock = lock;
1013
+ }).catch(function (err) {
1014
+ // WakeLock API failed or is not permitted in this WebView
1015
+ console.warn('MYETV Radio: Standard WakeLock failed. Using fallback.', err);
1016
+ self._fallbackNoSleep(true);
1017
+ });
1018
+ }
1019
+ } else {
1020
+ if (this._wakeLock) {
1021
+ this._wakeLock.release().catch(function () { });
1022
+ this._wakeLock = null;
1023
+ }
1024
+ self._fallbackNoSleep(false);
1025
+ }
1026
+ } else {
1027
+ // WakeLock API not supported, use fallback directly
1028
+ this._fallbackNoSleep(play);
1029
+ }
1030
+ };
1031
+
1032
+ // Helper method to keep screen awake using an invisible looping video
1033
+ RadioPlugin.prototype._fallbackNoSleep = function (play) {
1034
+ if (!this._noSleepVideo) {
1035
+ // Create a tiny, muted, invisible video element
1036
+ this._noSleepVideo = document.createElement('video');
1037
+ this._noSleepVideo.setAttribute('playsinline', '');
1038
+ this._noSleepVideo.setAttribute('muted', '');
1039
+ this._noSleepVideo.setAttribute('loop', '');
1040
+
1041
+ // Base64 string of a 1x1 blank MP4 video
1042
+ this._noSleepVideo.src = 'data:video/mp4;base64,AAAAHGZ0eXBpc29tAAACAGlzb21pc28yYXZjMQAAAAhmcmVlAAAAG21kYXQAAAGzABAHAAABthkQIvQcREiEXEQAAAA+bW9vdgAAAExtdmhkAAAAAM9cO3DPXDtwAAACWAAAAKAAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAzB0cmFrAAAAXHRraGQAAAADz1w7cM9cO3AAAAABAAAAAAAACgAAAAAAAAAAAAAAAAABAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAEAAQAAAAAAJmVkdHMAAAAcZWxzdAAAAAAAAAABAAAACgAAAAAAAQAAAAAAuG1kaWEAAABgbWRoZAAAAADPXDtwz1w7cAAAABAAAAAKVU5EAAAAAAAhaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAAxbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcYnJlZgAAAAAAAAABAAAADHVybCAAAAABAAABHG10YnQAAABQc3RzZAAAAAAAAAABAAAAVGhhdmMxAQAABgAA//8AABwAAAAAABIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBQAAABhzdHRzAAAAAAAAAAEAAAABAAQAAAAAABRzdHNzAAAAAAAAAAEAAAABAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAACRzdHN6AAAAAAAAAAAAAAABAAAAHgAAAAEAAAAQc3RjbwAAAAAAAAABAAAAHAAAAEd1ZHRhAAAAP21ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAAKGlsc3QAAAAQwq10b28AAAAMZGF0YQAAAAEAAAAA';
1043
+
1044
+ // Hide the video completely
1045
+ this._noSleepVideo.style.display = 'none';
1046
+ this._noSleepVideo.style.position = 'absolute';
1047
+ this._noSleepVideo.style.width = '1px';
1048
+ this._noSleepVideo.style.height = '1px';
1049
+
1050
+ document.body.appendChild(this._noSleepVideo);
1051
+ }
1052
+
1053
+ if (play) {
1054
+ // Ignore potential play promise errors
1055
+ this._noSleepVideo.play().catch(function(e) {
1056
+ console.warn('MYETV Radio: Fallback video playback blocked', e);
1057
+ });
1058
+ } else {
1059
+ this._noSleepVideo.pause();
1060
+ }
1061
+ };
1062
+
1063
+ // ============================================================
1064
+ // STATS MANAGEMENT
1065
+ // ============================================================
1066
+ RadioPlugin.prototype._startStatsPolling = function (station) {
1067
+ var self = this;
1068
+
1069
+ // Clear existing polling
1070
+ if (this._statsIntervalId) {
1071
+ clearInterval(this._statsIntervalId);
1072
+ this._statsIntervalId = null;
1073
+ }
1074
+
1075
+ if (!this.options.statsApiUrl || !station) {
1076
+ this.currentStats = { live: 0, total: 0, daily: 0, weekly: 0, monthly: 0, yearly: 0 };
1077
+ this._updateStatsUI();
1078
+ return;
1079
+ }
1080
+
1081
+ /* * Generate a truly unique station ID to prevent stat overlaps
1082
+ * across different countries or dynamic lists.
1083
+ * We combine the country code and a sanitized version of the station name.
1084
+ */
1085
+ var country = (station.countrycode || 'global').toLowerCase();
1086
+ var safeName = (station.name || 'unknown').toLowerCase().replace(/[^a-z0-9]/g, '_');
1087
+ var uniqueStationId = country + '_' + safeName;
1088
+
1089
+ // Fetch initial stats
1090
+ this._fetchStats(uniqueStationId);
1091
+
1092
+ // Start polling interval
1093
+ this._statsIntervalId = setInterval(function () {
1094
+ self._fetchStats(uniqueStationId);
1095
+ }, this.options.statsPollInterval);
1096
+ };
1097
+
1098
+ RadioPlugin.prototype._fetchStats = function (stationId) {
1099
+ var self = this;
1100
+ var url = this.options.statsApiUrl + '?action=update_and_get&station_id=' + stationId + '&client_id=' + this.options.clientId;
1101
+
1102
+ if (this._isRadioPlaying) {
1103
+ url += '&status=playing';
1104
+ }
1105
+
1106
+ fetch(url)
1107
+ .then(function (response) {
1108
+ if (!response.ok) throw new Error('HTTP status ' + response.status);
1109
+ return response.json();
1110
+ })
1111
+ .then(function (res) {
1112
+ // Supportiamo sia res.data (nuovo PHP) che res.stats (vecchio PHP)
1113
+ var payload = res.data || res.stats;
1114
+ if (res && res.success && payload) {
1115
+ self.currentStats = payload;
1116
+ self._updateStatsUI();
1117
+ }
1118
+ })
1119
+ .catch(function (err) {
1120
+ console.warn('MYETV Radio: Problema di lettura statistiche. Il PHP potrebbe restituire un errore invece di un JSON valido.', err);
1121
+ });
1122
+ };
1123
+
1124
+ RadioPlugin.prototype._updateStatsUI = function () {
1125
+ if (!this.currentStats) return;
1126
+
1127
+ // Update live counter
1128
+ if (this.liveStatsEl) {
1129
+ this.liveStatsEl.textContent = this.currentStats.live || 0;
1130
+ }
1131
+
1132
+ // Update total hits counter in the main UI bar
1133
+ if (this.hitsStatsEl) {
1134
+ this.hitsStatsEl.textContent = this.currentStats.total_hits || 0;
1135
+ }
1136
+ };
1137
+
1138
+ // ============================================================
1139
+ // SEARCH EVENTS
1140
+ // ============================================================
1141
+ RadioPlugin.prototype._bindSearchEvents = function (input, btn) {
1142
+ var self = this;
1143
+
1144
+ var doLocalSearch = function () {
1145
+ var q = input.value.trim();
1146
+ self._applyTextSearch(q);
1147
+ self._buildScale(self.filteredList.length);
1148
+ self._positionTo(1, false, true);
1149
+ };
1150
+
1151
+ btn.addEventListener('click', doLocalSearch);
1152
+ input.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLocalSearch(); });
1153
+ input.addEventListener('input', function () {
1154
+ if (self._searchTimeout) clearTimeout(self._searchTimeout);
1155
+ self._searchTimeout = setTimeout(doLocalSearch, 450);
1156
+ });
1157
+
1158
+ if (this.countrySelectEl) {
1159
+ this.countrySelectEl.addEventListener('change', function () {
1160
+ self.activeCountry = this.value;
1161
+ self.currentChannel = 1;
1162
+ self.targetAbsoluteCh = -1;
1163
+
1164
+ self._updateBaseList();
1165
+ self._applyTextSearch(self.searchInput ? self.searchInput.value.trim() : '');
1166
+
1167
+ if (self.rememberChannel) {
1168
+ localStorage.setItem('myetv_radio_last_country', self.activeCountry);
1169
+ var firstStation = self.filteredList[0];
1170
+ if (firstStation) {
1171
+ localStorage.setItem('myetv_radio_last_ch', self.stations.indexOf(firstStation) + 1);
1172
+ }
1173
+ }
1174
+
1175
+ if (self.dialContainer) self.dialContainer.classList.add('loading-state');
1176
+ self._setDigitalName('LOADING...');
1177
+
1178
+ if (self.player.video) {
1179
+ try { self.player.video.pause(); } catch (e) { }
1180
+ }
1181
+
1182
+ self._buildScale(self.filteredList.length);
1183
+ self._positionTo(1, false, true);
1184
+ });
1185
+ }
1186
+ };
1187
+
1188
+ // ============================================================
1189
+ // KEYBOARD EVENTS (OVERRIDE NATIVE SEEKING)
1190
+ // ============================================================
1191
+ RadioPlugin.prototype._bindKeyboard = function () {
1192
+ var self = this;
1193
+ this._keydownHandler = function (e) {
1194
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1195
+
1196
+ if (e.key === 'ArrowRight') {
1197
+ e.preventDefault();
1198
+ e.stopPropagation();
1199
+ self._positionTo(Math.min(self.filteredList.length, self.currentChannel + 1), self._isRadioPlaying);
1200
+ } else if (e.key === 'ArrowLeft') {
1201
+ e.preventDefault();
1202
+ e.stopPropagation();
1203
+ self._positionTo(Math.max(1, self.currentChannel - 1), self._isRadioPlaying);
1204
+ }
1205
+ };
1206
+ document.addEventListener('keydown', this._keydownHandler, true);
1207
+ };
1208
+
1209
+ // ============================================================
1210
+ // CONTROL BAR & MODALS
1211
+ // ============================================================
1212
+ RadioPlugin.prototype._buildControlBar = function () {
1213
+ var self = this;
1214
+ var controls = this.player.controls;
1215
+ if (!controls) return;
1216
+ var main = controls.querySelector('.controls-main');
1217
+ if (!main) return;
1218
+
1219
+ main.style.position = 'relative';
1220
+
1221
+ var progressBar = controls.querySelector('[class*="progress"]');
1222
+ if (progressBar) progressBar.style.display = 'none';
1223
+
1224
+ var center = document.createElement('div');
1225
+ center.className = 'radio-controls-center';
1226
+
1227
+ var btnList = document.createElement('button');
1228
+ btnList.className = 'radio-cb-btn';
1229
+ btnList.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/></svg><span class="radio-cb-label">RADIO LIST</span>';
1230
+
1231
+ center.appendChild(btnList);
1232
+
1233
+ var right = main.querySelector('.controls-right');
1234
+ if (right) main.insertBefore(center, right);
1235
+ else main.appendChild(center);
1236
+
1237
+ btnList.addEventListener('click', function (e) { e.stopPropagation(); self._openModal(); });
1238
+ };
1239
+
1240
+ RadioPlugin.prototype._injectSettingsMenu = function () {
1241
+ var self = this;
1242
+ var settingsMenu = this.player.container.querySelector('.settings-menu');
1243
+ if (!settingsMenu) return;
1244
+
1245
+ if (settingsMenu.querySelector('.radio-custom-settings')) return;
1246
+
1247
+ var wrap = document.createElement('div');
1248
+ wrap.className = 'radio-custom-settings';
1249
+ wrap.style.cssText = 'padding:0; margin:0; border-top:1px solid rgba(255,255,255,0.05); margin-top:4px; display:flex; flex-direction:column;';
1250
+
1251
+ var title = document.createElement('div');
1252
+ title.style.cssText = 'padding:10px 16px 4px 16px; font-size:11px; color:#00d8ff; font-weight:bold; letter-spacing:1px; text-transform:uppercase;';
1253
+ title.textContent = 'Radio Options';
1254
+ wrap.appendChild(title);
1255
+
1256
+ var memRow = document.createElement('div');
1257
+ memRow.className = 'settings-option';
1258
+ memRow.style.cssText = 'cursor:pointer; display:flex; justify-content:space-between; align-items:center;';
1259
+
1260
+ var memLabel = document.createElement('span');
1261
+ memLabel.className = 'settings-option-label';
1262
+ memLabel.textContent = 'Remember Channel';
1263
+
1264
+ var memCb = document.createElement('input');
1265
+ memCb.type = 'checkbox';
1266
+ memCb.checked = this.rememberChannel;
1267
+ memCb.style.cursor = 'pointer';
1268
+
1269
+ memCb.addEventListener('change', function (e) {
1270
+ self.rememberChannel = e.target.checked;
1271
+ localStorage.setItem('myetv_radio_remember', self.rememberChannel ? 'true' : 'false');
1272
+
1273
+ if (self.rememberChannel) {
1274
+ var s = self.filteredList[self.currentChannel - 1];
1275
+ var absoluteCh = s ? self.stations.indexOf(s) + 1 : 1;
1276
+ localStorage.setItem('myetv_radio_last_ch', absoluteCh);
1277
+ localStorage.setItem('myetv_radio_last_country', self.activeCountry);
1278
+ } else {
1279
+ localStorage.removeItem('myetv_radio_last_ch');
1280
+ localStorage.removeItem('myetv_radio_last_country');
1281
+ }
1282
+ });
1283
+
1284
+ memRow.addEventListener('click', function (e) {
1285
+ if (e.target !== memCb) {
1286
+ memCb.checked = !memCb.checked;
1287
+ var event = new Event('change');
1288
+ memCb.dispatchEvent(event);
1289
+ }
1290
+ });
1291
+
1292
+ memRow.appendChild(memLabel);
1293
+ memRow.appendChild(memCb);
1294
+ wrap.appendChild(memRow);
1295
+
1296
+ settingsMenu.appendChild(wrap);
1297
+ };
1298
+
1299
+ RadioPlugin.prototype._buildModal = function () {
1300
+ var self = this;
1301
+ var container = this.player.container;
1302
+ if (!container) return;
1303
+
1304
+ var overlay = document.createElement('div');
1305
+ overlay.className = 'radio-modal-overlay';
1306
+ overlay.style.display = 'none';
1307
+
1308
+ var box = document.createElement('div');
1309
+ box.className = 'radio-modal-box';
1310
+ box.style.color = '#f0c040';
1311
+
1312
+ var header = document.createElement('div');
1313
+ header.className = 'radio-modal-header';
1314
+ var title = document.createElement('span');
1315
+ title.className = 'radio-modal-title';
1316
+ title.textContent = 'Station List';
1317
+ var closeBtn = document.createElement('button');
1318
+ closeBtn.className = 'radio-modal-close';
1319
+ closeBtn.innerHTML = '&times;';
1320
+ header.appendChild(title);
1321
+ header.appendChild(closeBtn);
1322
+
1323
+ var searchWrap = document.createElement('div');
1324
+ searchWrap.className = 'radio-modal-search';
1325
+ var searchIn = document.createElement('input');
1326
+ searchIn.type = 'text';
1327
+ searchIn.placeholder = 'Filter station name...';
1328
+ searchWrap.appendChild(searchIn);
1329
+
1330
+ var countEl = document.createElement('div');
1331
+ countEl.className = 'radio-modal-count';
1332
+ this._modalCountEl = countEl;
1333
+
1334
+ var list = document.createElement('div');
1335
+ list.className = 'radio-modal-list';
1336
+ this._modalListEl = list;
1337
+
1338
+ box.appendChild(header);
1339
+ box.appendChild(searchWrap);
1340
+ box.appendChild(countEl);
1341
+ box.appendChild(list);
1342
+ overlay.appendChild(box);
1343
+
1344
+ if (this.overlay) {
1345
+ this.overlay.appendChild(overlay);
1346
+ } else {
1347
+ container.appendChild(overlay);
1348
+ }
1349
+
1350
+ this._modalOverlay = overlay;
1351
+
1352
+ closeBtn.addEventListener('click', function () { self._closeModal(); });
1353
+ overlay.addEventListener('click', function (e) { if (e.target === overlay) self._closeModal(); });
1354
+
1355
+ var t = null;
1356
+ searchIn.addEventListener('input', function () {
1357
+ if (t) clearTimeout(t);
1358
+ t = setTimeout(function () { self._renderModalList(searchIn.value.trim()); }, 300);
1359
+ });
1360
+ };
1361
+
1362
+ RadioPlugin.prototype._renderModalList = function (filter) {
1363
+ var list = this._modalListEl;
1364
+ if (!list) return;
1365
+ list.innerHTML = '';
1366
+
1367
+ var q = (filter || '').toLowerCase();
1368
+ var rows = q ? this.filteredList.filter(function (s) {
1369
+ return (s.name || '').toLowerCase().indexOf(q) !== -1 || String(s.channel_id).indexOf(q) !== -1;
1370
+ }) : this.filteredList;
1371
+
1372
+ if (this._modalCountEl) this._modalCountEl.textContent = rows.length + ' stations';
1373
+
1374
+ var self = this;
1375
+ var frag = document.createDocumentFragment();
1376
+ var max = Math.min(rows.length, 2000);
1377
+
1378
+ for (var i = 0; i < max; i++) {
1379
+ (function (s) {
1380
+ var actualIndex = self.filteredList.indexOf(s) + 1;
1381
+ var displayCh = self.baseCountryList.indexOf(s) + 1;
1382
+
1383
+ var item = document.createElement('div');
1384
+ item.className = 'radio-modal-item' + (actualIndex === self.currentChannel ? ' active' : '');
1385
+
1386
+ var ch = document.createElement('span');
1387
+ ch.className = 'radio-modal-ch';
1388
+ ch.textContent = 'CH ' + displayCh;
1389
+
1390
+ var fav = document.createElement('img');
1391
+ fav.className = 'radio-modal-fav';
1392
+ var iconUrl = self._getStationIcon(s);
1393
+
1394
+ if (iconUrl) {
1395
+ fav.style.display = 'none';
1396
+ fav.onload = function () { this.style.display = 'block'; };
1397
+ fav.onerror = function () { this.style.display = 'none'; };
1398
+ fav.src = iconUrl;
1399
+ }
1400
+
1401
+ var name = document.createElement('span');
1402
+ name.className = 'radio-modal-name';
1403
+ name.textContent = s.name || 'Unknown';
1404
+
1405
+ var cc = document.createElement('span');
1406
+ cc.className = 'radio-modal-cc';
1407
+ cc.textContent = s.countrycode ? s.countrycode.toUpperCase() : '';
1408
+
1409
+ item.appendChild(ch);
1410
+ item.appendChild(fav);
1411
+ item.appendChild(name);
1412
+ item.appendChild(cc);
1413
+
1414
+ item.addEventListener('click', function () {
1415
+ self._positionTo(actualIndex, true, true);
1416
+ self._closeModal();
1417
+ });
1418
+ frag.appendChild(item);
1419
+ })(rows[i]);
1420
+ }
1421
+ list.appendChild(frag);
1422
+
1423
+ setTimeout(function () {
1424
+ var active = list.querySelector('.radio-modal-item.active');
1425
+ if (active) active.scrollIntoView({ block: 'center' });
1426
+ }, 50);
1427
+ };
1428
+
1429
+ RadioPlugin.prototype._openModal = function () {
1430
+ if (!this._modalOverlay) return;
1431
+ this._renderModalList('');
1432
+ this._modalOverlay.style.display = 'flex';
1433
+ };
1434
+
1435
+ RadioPlugin.prototype._closeModal = function () {
1436
+ if (this._modalOverlay) this._modalOverlay.style.display = 'none';
1437
+ };
1438
+
1439
+ // ============================================================
1440
+ // STATS MODAL
1441
+ // ============================================================
1442
+ RadioPlugin.prototype._buildStatsModal = function () {
1443
+ var self = this;
1444
+ var container = this.player.container;
1445
+ if (!container) return;
1446
+
1447
+ var overlay = document.createElement('div');
1448
+ overlay.className = 'radio-modal-overlay';
1449
+ overlay.style.display = 'none';
1450
+
1451
+ var box = document.createElement('div');
1452
+ box.className = 'radio-modal-box';
1453
+ box.style.color = '#f0c040';
1454
+ box.style.maxWidth = '400px';
1455
+ box.style.margin = '0 auto';
1456
+
1457
+ var header = document.createElement('div');
1458
+ header.className = 'radio-modal-header';
1459
+ var title = document.createElement('span');
1460
+ title.className = 'radio-modal-title';
1461
+ title.textContent = 'Channel Analytics';
1462
+ this._statsModalTitle = title;
1463
+
1464
+ var closeBtn = document.createElement('button');
1465
+ closeBtn.className = 'radio-modal-close';
1466
+ closeBtn.innerHTML = '&times;';
1467
+ header.appendChild(title);
1468
+ header.appendChild(closeBtn);
1469
+
1470
+ var content = document.createElement('div');
1471
+ content.className = 'radio-stats-modal-content';
1472
+ this._statsModalContent = content;
1473
+
1474
+ box.appendChild(header);
1475
+ box.appendChild(content);
1476
+ overlay.appendChild(box);
1477
+
1478
+ if (this.overlay) {
1479
+ this.overlay.appendChild(overlay);
1480
+ } else {
1481
+ container.appendChild(overlay);
1482
+ }
1483
+
1484
+ this._statsModalOverlay = overlay;
1485
+
1486
+ closeBtn.addEventListener('click', function () { self._closeStatsModal(); });
1487
+ overlay.addEventListener('click', function (e) { if (e.target === overlay) self._closeStatsModal(); });
1488
+ };
1489
+
1490
+ // ============================================================
1491
+ // STATS MODAL
1492
+ // ============================================================
1493
+ RadioPlugin.prototype._openStatsModal = function () {
1494
+ if (!this._statsModalOverlay) return;
1495
+
1496
+ var s = this.filteredList[this.currentChannel - 1];
1497
+ if (s && this._statsModalTitle) {
1498
+ this._statsModalTitle.textContent = 'Analytics: ' + (s.name || 'Station');
1499
+ }
1500
+
1501
+ if (this._statsModalContent) {
1502
+ // Retrieve current stats or set defaults
1503
+ var data = this.currentStats || {};
1504
+ var stats = data.stats || {
1505
+ daily: { unique: 0, hits: 0 },
1506
+ weekly: { unique: 0, hits: 0 },
1507
+ monthly: { unique: 0, hits: 0 },
1508
+ yearly: { unique: 0, hits: 0 }
1509
+ };
1510
+
1511
+ // Helper function to build stat cards dynamically
1512
+ var formatCard = function (label, u, h) {
1513
+ return '<div class="radio-stats-card">' +
1514
+ '<div style="font-weight:bold; margin-bottom:4px;">' + label + '</div>' +
1515
+ '<div style="font-size:9px; opacity:0.7;">UNIQUE / TOTAL</div>' +
1516
+ '<div class="radio-stats-value">' + (u || 0) + ' <span style="font-size:12px; opacity:0.5;">/</span> ' + (h || 0) + '</div>' +
1517
+ '</div>';
1518
+ };
1519
+
1520
+ // Build the HTML grid
1521
+ this._statsModalContent.innerHTML =
1522
+ '<div style="text-align:center; opacity:0.8; margin-bottom:10px;">Viewer History</div>' +
1523
+ '<div class="radio-stats-grid">' +
1524
+ '<div class="radio-stats-card">' +
1525
+ '<div style="font-weight:bold; margin-bottom:4px;">LIVE NOW</div>' +
1526
+ '<div class="radio-stats-value" style="color:#00ff44; padding-top:10px;">' + (data.live || 0) + '</div>' +
1527
+ '</div>' +
1528
+ '<div class="radio-stats-card">' +
1529
+ '<div style="font-weight:bold; margin-bottom:4px;">ALL TIME</div>' +
1530
+ '<div style="font-size:9px; opacity:0.7;">UNIQUE / TOTAL</div>' +
1531
+ '<div class="radio-stats-value" style="color:#00d8ff;">' + (data.total_unique || 0) + ' <span style="font-size:12px; opacity:0.5;">/</span> ' + (data.total_hits || 0) + '</div>' +
1532
+ '</div>' +
1533
+ formatCard('TODAY', stats.daily.unique, stats.daily.hits) +
1534
+ formatCard('THIS WEEK', stats.weekly.unique, stats.weekly.hits) +
1535
+ formatCard('THIS MONTH', stats.monthly.unique, stats.monthly.hits) +
1536
+ formatCard('THIS YEAR', stats.yearly.unique, stats.yearly.hits) +
1537
+ '</div>';
1538
+ }
1539
+
1540
+ this._statsModalOverlay.style.display = 'flex';
1541
+ };
1542
+
1543
+ RadioPlugin.prototype._closeStatsModal = function () {
1544
+ if (this._statsModalOverlay) this._statsModalOverlay.style.display = 'none';
1545
+ };
1546
+
1547
+ // ============================================================
1548
+ // PLAYER HOOKS
1549
+ // ============================================================
1550
+ RadioPlugin.prototype._hookPlayerEvents = function () {
1551
+ var self = this;
1552
+ this.player.addEventListener('played', function () {
1553
+ self._isRadioPlaying = true;
1554
+ self._clearBufferTimeout();
1555
+ self._clearError();
1556
+ self._setStatus('playing');
1557
+ self._manageWakeLock(true);
1558
+
1559
+ // Re-trigger stats polling to register status=playing immediately
1560
+ var s = self.filteredList[self.currentChannel - 1];
1561
+ if (s) self._startStatsPolling(s);
1562
+ });
1563
+
1564
+ this.player.addEventListener('paused', function () {
1565
+ if (self._tuneTimeout) return;
1566
+ self._isRadioPlaying = false;
1567
+ self._clearBufferTimeout();
1568
+ self._setStatus('');
1569
+ self._manageWakeLock(false);
1570
+
1571
+ // Re-trigger to update live status
1572
+ var s = self.filteredList[self.currentChannel - 1];
1573
+ if (s) self._startStatsPolling(s);
1574
+ });
1575
+
1576
+ this.player.addEventListener('ended', function () {
1577
+ if (self._tuneTimeout) return;
1578
+ self._isRadioPlaying = false;
1579
+ self._clearBufferTimeout();
1580
+ self._setStatus('');
1581
+ self._manageWakeLock(false);
1582
+ });
1583
+
1584
+ var video = this.player.video;
1585
+ if (video) {
1586
+ video.addEventListener('waiting', function () {
1587
+ if (self._tuneTimeout) return;
1588
+ self._setStatus('buffering');
1589
+ self._bufferTimeout = setTimeout(function () {
1590
+ if (!video.paused && video.readyState < 3) self._setError('STREAM UNAVAILABLE');
1591
+ }, 8000);
1592
+ });
1593
+
1594
+ video.addEventListener('error', function () {
1595
+ if (self._tuneTimeout) return;
1596
+ if (!video.hasAttribute('src')) return;
1597
+ self._clearBufferTimeout();
1598
+ self._setError('STREAM UNAVAILABLE');
1599
+ });
1600
+
1601
+ video.addEventListener('stalled', function () {
1602
+ if (self._tuneTimeout) return;
1603
+ if (!self._bufferTimeout && video.hasAttribute('src')) {
1604
+ self._bufferTimeout = setTimeout(function () { self._setError('STREAM UNAVAILABLE'); }, 8000);
1605
+ }
1606
+ });
1607
+ }
1608
+ };
1609
+
1610
+ // ============================================================
1611
+ // PUBLIC API
1612
+ // ============================================================
1613
+ RadioPlugin.prototype.tune = function (ch) { this._positionTo(parseInt(ch) || 1, false, true); return this; };
1614
+ RadioPlugin.prototype.play = function () { this._isRadioPlaying = true; var s = this.filteredList[this.currentChannel - 1]; if (s) { this._prepareStream(s, true); this._startStatsPolling(s); } return this; };
1615
+ RadioPlugin.prototype.stop = function () {
1616
+ this._isRadioPlaying = false;
1617
+ var v = this.player.video;
1618
+ if (v) { v.pause(); v.removeAttribute('src'); v.load(); }
1619
+ this._setStatus('');
1620
+
1621
+ // Explicitly release the wake lock when stopping the plugin manually
1622
+ this._manageWakeLock(false);
1623
+
1624
+ if (this._statsIntervalId) clearInterval(this._statsIntervalId);
1625
+ return this;
1626
+ };
1627
+ RadioPlugin.prototype.setTheme = function (theme) {
1628
+ this.options.theme = theme;
1629
+ if (this.overlay) {
1630
+ this.overlay.classList.remove('theme-vintage', 'theme-digital');
1631
+ this.overlay.classList.add('theme-' + theme);
1632
+ }
1633
+ return this;
1634
+ };
1635
+ RadioPlugin.prototype.dispose = function () {
1636
+ this.stop();
1637
+ if (this._keydownHandler) document.removeEventListener('keydown', this._keydownHandler, true);
1638
+ if (this._searchTimeout) clearTimeout(this._searchTimeout);
1639
+ if (this._tuneTimeout) clearTimeout(this._tuneTimeout);
1640
+ if (this._statsIntervalId) clearInterval(this._statsIntervalId);
1641
+ if (this.overlay && this.overlay.parentNode) this.overlay.parentNode.removeChild(this.overlay);
1642
+ };
1643
+
1644
+ // ============================================================
1645
+ // REGISTER
1646
+ // ============================================================
1647
+ if (typeof global.registerMYETVPlugin === 'function') {
1648
+ global.registerMYETVPlugin('radio', RadioPlugin);
1649
+ } else {
1650
+ document.addEventListener('DOMContentLoaded', function () {
1651
+ if (typeof global.registerMYETVPlugin === 'function') global.registerMYETVPlugin('radio', RadioPlugin);
1652
+ });
1653
+ }
1654
+
1655
+ })(window);