Guymy97 commited on
Commit
00f585e
·
verified ·
1 Parent(s): 2312d69

Update via AnyCoder

Browse files
Files changed (1) hide show
  1. index.html +153 -214
index.html CHANGED
@@ -91,9 +91,8 @@
91
 
92
  main {
93
  max-width: 1200px; margin: 0 auto; padding: clamp(18px, 3vw, 28px);
94
- display: grid; grid-template-columns: 360px 1fr; gap: var(--gap);
95
  }
96
- @media (max-width: 980px) { main { grid-template-columns: 1fr; } }
97
 
98
  .card {
99
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
@@ -102,62 +101,6 @@
102
  box-shadow: var(--shadow);
103
  }
104
 
105
- .login { padding: clamp(18px, 3.2vw, 28px); position: sticky; top: calc(62px + 12px); align-self: start; }
106
- @media (max-width: 980px) { .login { position: relative; top: 0; } }
107
- .login-header { display: grid; gap: 8px; }
108
- .login-header h2 { margin: 0; font-size: clamp(18px, 2.6vw, 22px); }
109
- .subtitle { color: var(--muted); font-size: 13px; }
110
-
111
- .mac-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin: 16px 0 6px; }
112
- .mac-input { position: relative; }
113
- .mac-input input {
114
- width: 100%; padding: 14px 0; text-align: center; font-size: 18px; letter-spacing: 1px;
115
- border-radius: var(--radius-lg); border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
116
- background: color-mix(in oklab, var(--bg-soft) 70%, transparent); color: var(--txt);
117
- outline: none; transition: .2s border-color, .2s background;
118
- }
119
- .mac-input input:focus {
120
- border-color: color-mix(in oklab, var(--primary) 60%, transparent);
121
- background: color-mix(in oklab, var(--primary) 8%, transparent);
122
- box-shadow: 0 0 0 6px color-mix(in oklab, var(--primary) 10%, transparent);
123
- }
124
- .login .actions { display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
125
- .btn {
126
- display: inline-flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 12px;
127
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
128
- background: linear-gradient(180deg, color-mix(in oklab, var(--elev) 86%, transparent), transparent);
129
- color: var(--txt); font-weight: 700; cursor: pointer; transition: .2s transform, .2s border-color, .2s background, .2s color;
130
- }
131
- .btn:hover { transform: translateY(-1px); border-color: color-mix(in oklab, var(--txt) 16%, transparent); }
132
- .btn.primary {
133
- border-color: color-mix(in oklab, var(--primary) 60%, transparent);
134
- background: linear-gradient(180deg, color-mix(in oklab, var(--primary) 22%, transparent), transparent);
135
- color: white; box-shadow: 0 10px 18px color-mix(in oklab, var(--primary) 22%, transparent);
136
- }
137
- .btn.ghost { background: transparent; }
138
- .hint { font-size: 12px; color: var(--muted); margin-top: 6px; display: flex; align-items: center; gap: 6px; }
139
-
140
- .provider { margin-top: 18px; display: grid; gap: 10px; }
141
- .provider .row { display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center; }
142
- .select, .input { position: relative; }
143
- select, .input input {
144
- width: 100%; appearance: none; padding: 14px 16px; border-radius: var(--radius-lg);
145
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
146
- background: color-mix(in oklab, var(--bg-soft) 70%, transparent); color: var(--txt);
147
- outline: none; transition: .2s border-color, .2s background; font-weight: 600;
148
- }
149
- select:focus, .input input:focus {
150
- border-color: color-mix(in oklab, var(--accent) 60%, transparent);
151
- background: color-mix(in oklab, var(--accent) 8%, transparent);
152
- box-shadow: 0 0 0 6px color-mix(in oklab, var(--accent) 10%, transparent);
153
- }
154
- .select i { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--muted); pointer-events: none; }
155
-
156
- .divider { height: 1px; background: color-mix(in oklab, var(--txt) 10%, transparent); margin: 16px 0; }
157
-
158
- .status { display: grid; gap: 8px; padding: 10px 12px; background: color-mix(in oklab, var(--bg-soft) 60%, transparent); border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent); border-radius: var(--radius-md); }
159
- .status .line { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); }
160
-
161
  .content { display: grid; gap: var(--gap); }
162
  .hero {
163
  position: relative; overflow: clip; border-radius: var(--radius-xl);
@@ -243,14 +186,6 @@
243
  .tag { border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent); border-radius: 999px; padding: 6px 10px; background: color-mix(in oklab, var(--bg-soft) 60%, transparent); }
244
 
245
  .hidden { display: none !important; }
246
- .shake { animation: shake .22s 2; }
247
- @keyframes shake {
248
- 0% { transform: translateX(0) }
249
- 25% { transform: translateX(-3px) }
250
- 50% { transform: translateX(3px) }
251
- 75% { transform: translateX(-2px) }
252
- 100% { transform: translateX(0) }
253
- }
254
  </style>
255
  </head>
256
  <body>
@@ -260,7 +195,7 @@
260
  <div class="logo" aria-hidden="true"></div>
261
  <div>
262
  <h1>NeoIPTV</h1>
263
- <small>Connexion avec code MAC</small>
264
  </div>
265
  </div>
266
 
@@ -277,58 +212,12 @@
277
  </header>
278
 
279
  <main>
280
- <section class="card login" id="loginCard">
281
- <div class="login-header">
282
- <h2>Se connecter</h2>
283
- <div class="subtitle">Entrez votre adresse MAC pour lier votre abonnement IPTV</div>
284
- </div>
285
-
286
- <form id="macForm" novalidate>
287
- <div class="mac-grid" aria-label="Entrée code MAC">
288
- <div class="mac-input"><input inputmode="latin" maxlength="2" pattern="[0-9A-Fa-f]{2}" autocomplete="off" spellcheck="false" aria-label="Bloc 1"></div>
289
- <div class="mac-input"><input maxlength="2" pattern="[0-9A-Fa-f]{2}" aria-label="Bloc 2"></div>
290
- <div class="mac-input"><input maxlength="2" pattern="[0-9A-Fa-f]{2}" aria-label="Bloc 3"></div>
291
- <div class="mac-input"><input maxlength="2" pattern="[0-9A-Fa-f]{2}" aria-label="Bloc 4"></div>
292
- <div class="mac-input"><input maxlength="2" pattern="[0-9A-Fa-f]{2}" aria-label="Bloc 5"></div>
293
- <div class="mac-input"><input maxlength="2" pattern="[0-9A-Fa-f]{2}" aria-label="Bloc 6"></div>
294
- </div>
295
-
296
- <div class="actions">
297
- <button type="submit" class="btn primary"><i class='bx bx-log-in-circle'></i> Se connecter</button>
298
- <button type="button" id="pasteMac" class="btn ghost"><i class='bx bx-clipboard'></i> Coller</button>
299
- <button type="button" id="randomMac" class="btn ghost"><i class='bx bx-dice-6'></i> Exemple</button>
300
- </div>
301
- <div class="hint"><i class='bx bx-lock-alt'></i> Nous ne stockons aucune donnée sensible. La connexion MAC simule l’authentification côté client.</div>
302
-
303
- <div class="provider">
304
- <div class="row">
305
- <div class="select">
306
- <select id="providerSelect" aria-label="Fournisseur">
307
- <option value="custom">Fournisseur personnalisé</option>
308
- <option value="xtream">Xtream Codes</option>
309
- <option value="stalker">Stalker/Portal</option>
310
- </select>
311
- <i class='bx bx-chevron-down'></i>
312
- </div>
313
- <button type="button" id="saveConfig" class="btn"><i class='bx bx-save'></i> Sauver</button>
314
- </div>
315
- <div class="input"><input id="portalUrl" type="url" placeholder="URL du portail (ex: http://exemple.com/portal)" /></div>
316
- <div class="status" id="statusBox" aria-live="polite">
317
- <div class="line"><i class='bx bx-info-circle'></i> État: Déconnecté</div>
318
- <div class="line"><i class='bx bx-time'></i> Valide jusqu’à: —</div>
319
- </div>
320
- </div>
321
- </form>
322
-
323
- <div class="divider"></div>
324
- <div class="hint"><i class='bx bx-shield-quarter'></i> Astuce: Appuyez sur Tab pour passer au bloc suivant. Seuls les caractères 0-9 et A-F sont acceptés.</div>
325
- </section>
326
-
327
  <section class="content">
328
  <div class="hero">
329
  <div class="hero-inner">
330
  <h2>Vos chaînes, films et séries en un clic</h2>
331
- <p>Après connexion avec votre code MAC, explorez le guide TV, la VOD et les séries. Le lecteur est intégré et supporte HLS (m3u8).</p>
332
  </div>
333
  </div>
334
 
@@ -392,11 +281,11 @@
392
 
393
  <footer>
394
  <div class="credits">
395
- <span class="tag">Démo UI IPTV (MAC)</span>
396
  <span class="tag">HLS natif</span>
397
  <span class="tag">Responsive</span>
398
  </div>
399
- <div>Note: Cette application est une démonstration côté client. Pour une vraie intégration IPTV via code MAC (Stalker/Xtream), branchez vos API et flux sécurisés côté serveur.</div>
400
  </footer>
401
 
402
  <script>
@@ -415,100 +304,6 @@
415
  setTheme(now);
416
  });
417
 
418
- // MAC inputs behavior
419
- const macForm = document.getElementById('macForm');
420
- const macBlocks = [...macForm.querySelectorAll('.mac-input input')];
421
- const statusBox = document.getElementById('statusBox');
422
- const providerSelect = document.getElementById('providerSelect');
423
- const portalUrl = document.getElementById('portalUrl');
424
- const randomMacBtn = document.getElementById('randomMac');
425
- const pasteMacBtn = document.getElementById('pasteMac');
426
- const saveConfigBtn = document.getElementById('saveConfig');
427
-
428
- function formatHex(val) { return val.replace(/[^0-9a-f]/gi, '').toUpperCase().slice(0, 2); }
429
- macBlocks.forEach((inp, idx) => {
430
- inp.addEventListener('input', () => {
431
- const v = formatHex(inp.value); inp.value = v;
432
- if (v.length === 2 && idx < macBlocks.length - 1) macBlocks[idx + 1].focus();
433
- });
434
- inp.addEventListener('keydown', (e) => {
435
- if (e.key === 'Backspace' && inp.value.length === 0 && idx > 0) macBlocks[idx - 1].focus();
436
- if (e.key === '-' || e.key === ':') e.preventDefault();
437
- if (e.key === 'Enter') macForm.requestSubmit();
438
- });
439
- inp.addEventListener('paste', (e) => {
440
- const text = (e.clipboardData || window.clipboardData).getData('text');
441
- if (!text) return; e.preventDefault(); fillMacFromString(text);
442
- });
443
- });
444
-
445
- function macToString() { return macBlocks.map(b => (b.value || '00')).join(':'); }
446
- function setStatus(text, extra) {
447
- statusBox.innerHTML = `
448
- <div class="line"><i class='bx bx-info-circle'></i> ${text}</div>
449
- <div class="line"><i class='bx bx-time'></i> Valide jusqu’à: ${extra || '—'}</div>
450
- `;
451
- }
452
- function validMacString(s) { return /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/.test(s); }
453
- function fillMacFromString(text) {
454
- const cleaned = text.trim().replace(/-/g, ':').replace(/\./g, '').toUpperCase();
455
- let mac = cleaned;
456
- if (/^[0-9A-F]{12}$/.test(cleaned)) mac = cleaned.match(/.{1,2}/g).join(':');
457
- if (/^([0-9A-F]{2}[:]){5}[0-9A-F]{2}$/.test(mac)) {
458
- mac.split(':').forEach((pair, i) => macBlocks[i].value = pair);
459
- } else { flashInvalid(); }
460
- }
461
- function flashInvalid() {
462
- macForm.classList.remove('shake'); void macForm.offsetWidth; macForm.classList.add('shake');
463
- }
464
-
465
- randomMacBtn.addEventListener('click', () => {
466
- const oui = ["00:1A:79","00:1B:44","00:1C:B3","00:1D:D8","00:1E:68"];
467
- const rnd = () => Math.floor(Math.random() * 256).toString(16).padStart(2, '0').toUpperCase();
468
- const prefix = oui[Math.floor(Math.random() * oui.length)];
469
- const mac = `${prefix}:${rnd()}:${rnd()}:${rnd()}`;
470
- fillMacFromString(mac);
471
- });
472
-
473
- pasteMacBtn.addEventListener('click', async () => {
474
- try {
475
- const txt = await navigator.clipboard.readText();
476
- if (txt) fillMacFromString(txt);
477
- } catch (e) {
478
- alert('Impossible de lire le presse-papiers. Collez directement dans un bloc.');
479
- }
480
- });
481
-
482
- saveConfigBtn.addEventListener('click', () => {
483
- const cfg = { provider: providerSelect.value, portal: portalUrl.value };
484
- localStorage.setItem('iptv_cfg', JSON.stringify(cfg));
485
- setStatus('Configuration sauvegardée', new Date(Date.now() + 86400000).toLocaleString());
486
- });
487
-
488
- (function loadCfg(){
489
- const cfg = JSON.parse(localStorage.getItem('iptv_cfg') || '{"provider":"custom","portal":""}');
490
- providerSelect.value = cfg.provider || 'custom';
491
- portalUrl.value = cfg.portal || '';
492
- const macSaved = localStorage.getItem('iptv_mac');
493
- if (macSaved && validMacString(macSaved)) fillMacFromString(macSaved);
494
- })();
495
-
496
- macForm.addEventListener('submit', async (e) => {
497
- e.preventDefault();
498
- const mac = macToString();
499
- if (!validMacString(mac)) { flashInvalid(); setStatus('Code MAC invalide', '—'); return; }
500
- setStatus('Connexion en cours...', '—');
501
- await new Promise(r => setTimeout(r, 600));
502
- const last = parseInt(mac.split(':')[5], 16);
503
- if (Number.isFinite(last) && last % 2 === 0) {
504
- setStatus('Connecté', new Date(Date.now() + 7 * 86400000).toLocaleString());
505
- localStorage.setItem('iptv_mac', mac);
506
- document.body.dispatchEvent(new CustomEvent('iptv:connected', { detail: { mac } }));
507
- } else {
508
- setStatus('Accès refusé par le portail', '—'); flashInvalid();
509
- }
510
- });
511
-
512
  // Demo data
513
  const data = {
514
  live: [
@@ -542,14 +337,13 @@
542
  recent: document.getElementById('grid-recent')
543
  };
544
 
545
- // Make all tabs accessible without login (per request)
546
  tabs.forEach(tab => {
547
  tab.addEventListener('click', () => {
548
  tabs.forEach(t => t.classList.remove('active'));
549
  tab.classList.add('active');
550
  const chosen = tab.dataset.tab;
551
  Object.keys(grids).forEach(k => grids[k].classList.toggle('hidden', k !== chosen));
552
- // Persist selected tab
553
  localStorage.setItem('iptv_tab', chosen);
554
  applySearchAndFilters();
555
  });
@@ -586,4 +380,149 @@
586
  }
587
 
588
  // Render functions
589
- function
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  main {
93
  max-width: 1200px; margin: 0 auto; padding: clamp(18px, 3vw, 28px);
94
+ display: grid; grid-template-columns: 1fr; gap: var(--gap);
95
  }
 
96
 
97
  .card {
98
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
 
101
  box-shadow: var(--shadow);
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  .content { display: grid; gap: var(--gap); }
105
  .hero {
106
  position: relative; overflow: clip; border-radius: var(--radius-xl);
 
186
  .tag { border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent); border-radius: 999px; padding: 6px 10px; background: color-mix(in oklab, var(--bg-soft) 60%, transparent); }
187
 
188
  .hidden { display: none !important; }
 
 
 
 
 
 
 
 
189
  </style>
190
  </head>
191
  <body>
 
195
  <div class="logo" aria-hidden="true"></div>
196
  <div>
197
  <h1>NeoIPTV</h1>
198
+ <small style="display:none">Connexion avec code MAC</small>
199
  </div>
200
  </div>
201
 
 
212
  </header>
213
 
214
  <main>
215
+ <!-- Section de connexion supprimée comme demandé -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  <section class="content">
217
  <div class="hero">
218
  <div class="hero-inner">
219
  <h2>Vos chaînes, films et séries en un clic</h2>
220
+ <p>Explorez le guide TV, la VOD et les séries. Le lecteur est intégré et supporte HLS (m3u8).</p>
221
  </div>
222
  </div>
223
 
 
281
 
282
  <footer>
283
  <div class="credits">
284
+ <span class="tag">Démo UI IPTV</span>
285
  <span class="tag">HLS natif</span>
286
  <span class="tag">Responsive</span>
287
  </div>
288
+ <div>Note: Cette application est une démonstration côté client. Branchez vos API et flux sécurisés côté serveur pour une vraie intégration.</div>
289
  </footer>
290
 
291
  <script>
 
304
  setTheme(now);
305
  });
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  // Demo data
308
  const data = {
309
  live: [
 
337
  recent: document.getElementById('grid-recent')
338
  };
339
 
340
+ // Tabs
341
  tabs.forEach(tab => {
342
  tab.addEventListener('click', () => {
343
  tabs.forEach(t => t.classList.remove('active'));
344
  tab.classList.add('active');
345
  const chosen = tab.dataset.tab;
346
  Object.keys(grids).forEach(k => grids[k].classList.toggle('hidden', k !== chosen));
 
347
  localStorage.setItem('iptv_tab', chosen);
348
  applySearchAndFilters();
349
  });
 
380
  }
381
 
382
  // Render functions
383
+ function render() {
384
+ grids.live.innerHTML = '';
385
+ grids.vod.innerHTML = '';
386
+ grids.series.innerHTML = '';
387
+ data.live.forEach(it => grids.live.appendChild(createCard(it, 'live')));
388
+ data.vod.forEach(it => grids.vod.appendChild(createCard(it, 'vod')));
389
+ data.series.forEach(it => grids.series.appendChild(createCard(it, 'series')));
390
+ }
391
+ render();
392
+
393
+ // Favorites and recents
394
+ function getFavs() { return JSON.parse(localStorage.getItem('iptv_favs') || '[]'); }
395
+ function saveFavs(f) { localStorage.setItem('iptv_favs', JSON.stringify(f)); }
396
+ function favKey(item, type){ return `${type}:${item.id}`; }
397
+
398
+ function toggleFav(item, type){
399
+ const key = favKey(item,type);
400
+ let favs = getFavs();
401
+ if (favs.includes(key)) favs = favs.filter(k => k !== key);
402
+ else favs.push(key);
403
+ saveFavs(favs);
404
+ renderFavs();
405
+ }
406
+
407
+ function renderFavs(){
408
+ const favs = getFavs();
409
+ const container = grids.favs;
410
+ container.innerHTML = '';
411
+ if (favs.length === 0) {
412
+ const empty = document.createElement('div');
413
+ empty.className = 'empty';
414
+ empty.innerHTML = "<i class='bx bx-star' style='font-size:32px;'></i><div>Aucun favori pour le moment.</div>";
415
+ container.appendChild(empty);
416
+ return;
417
+ }
418
+ const addFrom = (arr, type) => arr.forEach(it => {
419
+ if (favs.includes(favKey(it,type))) container.appendChild(createCard(it,type));
420
+ });
421
+ addFrom(data.live,'live'); addFrom(data.vod,'vod'); addFrom(data.series,'series');
422
+ }
423
+ renderFavs();
424
+
425
+ function pushRecent(item, type){
426
+ const recents = JSON.parse(localStorage.getItem('iptv_recent') || '[]').filter(r => r.id !== item.id || r.type !== type);
427
+ recents.unshift({ id:item.id, type, title:item.title, poster:item.poster||item.logo, when:Date.now(), tag:item.tag||'' });
428
+ localStorage.setItem('iptv_recent', JSON.stringify(recents.slice(0,30)));
429
+ renderRecent();
430
+ }
431
+
432
+ function renderRecent(){
433
+ const recents = JSON.parse(localStorage.getItem('iptv_recent') || '[]');
434
+ const container = grids.recent;
435
+ container.innerHTML = '';
436
+ if (recents.length === 0) {
437
+ const empty = document.createElement('div');
438
+ empty.className = 'empty';
439
+ empty.innerHTML = "<i class='bx bx-time-five' style='font-size:32px;'></i><div>Votre historique de lecture apparaîtra ici.</div>";
440
+ container.appendChild(empty);
441
+ return;
442
+ }
443
+ recents.forEach(r => {
444
+ const item = { id:r.id, title:r.title, poster:r.poster, logo:r.poster, url:'#', tag:r.tag };
445
+ container.appendChild(createCard(item, r.type));
446
+ });
447
+ }
448
+ renderRecent();
449
+
450
+ // Search and filters
451
+ const searchInput = document.getElementById('searchInput');
452
+ const clearSearch = document.getElementById('clearSearch');
453
+ const filterBtns = document.querySelectorAll('.filter-chip');
454
+
455
+ function applySearchAndFilters(){
456
+ const q = (searchInput.value || '').toLowerCase().trim();
457
+ const activeTab = document.querySelector('.tab.active').dataset.tab;
458
+ const activeFilter = document.querySelector('.filter-chip.active').dataset.filter;
459
+ const container = grids[activeTab];
460
+ if (!container) return;
461
+ [...container.children].forEach(card => {
462
+ if (card.classList.contains('empty')) return;
463
+ const matchesQ = card.dataset.search?.includes(q) || q === '';
464
+ let matchesF = true;
465
+ if (activeFilter === 'hd') matchesF = card.dataset.quality === 'hd';
466
+ if (activeFilter === 'recent') matchesF = card.dataset.recent === '1';
467
+ card.style.display = matchesQ && matchesF ? '' : 'none';
468
+ });
469
+ }
470
+ searchInput.addEventListener('input', applySearchAndFilters);
471
+ clearSearch.addEventListener('click', () => { searchInput.value=''; applySearchAndFilters(); });
472
+ filterBtns.forEach(b => b.addEventListener('click', () => {
473
+ filterBtns.forEach(x => x.classList.remove('active'));
474
+ b.classList.add('active');
475
+ applySearchAndFilters();
476
+ }));
477
+
478
+ // Restore selected tab
479
+ (function restoreTab(){
480
+ const t = localStorage.getItem('iptv_tab');
481
+ if (!t) return;
482
+ const btn = document.querySelector(`.tab[data-tab="${t}"]`);
483
+ if (btn) btn.click();
484
+ })();
485
+
486
+ // Player
487
+ const playerDialog = document.getElementById('playerDialog');
488
+ const video = document.getElementById('video');
489
+ const chipTitle = document.getElementById('chipTitle');
490
+ const chipType = document.getElementById('chipType');
491
+ const chipRes = document.getElementById('chipRes');
492
+ const closePlayer = document.getElementById('closePlayer');
493
+ const pipBtn = document.getElementById('pipBtn');
494
+ const muteBtn = document.getElementById('muteBtn');
495
+ const castBtn = document.getElementById('castBtn');
496
+
497
+ function openPlayer(item, type){
498
+ chipTitle.textContent = item.title;
499
+ chipType.textContent = type.toUpperCase();
500
+ chipRes.textContent = 'Auto';
501
+ video.src = item.url;
502
+ video.play().catch(()=>{});
503
+ playerDialog.showModal();
504
+ pushRecent(item, type);
505
+ }
506
+ closePlayer.addEventListener('click', () => {
507
+ video.pause();
508
+ video.src = '';
509
+ playerDialog.close();
510
+ });
511
+ pipBtn.addEventListener('click', async () => {
512
+ if (document.pictureInPictureElement) document.exitPictureInPicture();
513
+ else if (video.requestPictureInPicture) await video.requestPictureInPicture();
514
+ });
515
+ muteBtn.addEventListener('click', () => {
516
+ video.muted = !video.muted;
517
+ muteBtn.innerHTML = video.muted ? "<i class='bx bx-volume-mute'></i> Unmute" : "<i class='bx bx-volume-full'></i> Mute";
518
+ });
519
+ castBtn.addEventListener('click', () => {
520
+ alert('Casting non implémenté dans cette démo.');
521
+ });
522
+
523
+ // Small helpers
524
+ document.getElementById('scanBtn').addEventListener('click', (e)=>{ e.preventDefault(); alert('Scanner QR non disponible dans cette démo.'); });
525
+ document.getElementById('helpBtn').addEventListener('click', (e)=>{ e.preventDefault(); alert('Aide: utilisez la recherche, les filtres et cliquez sur Lire pour démarrer.'); });
526
+ </script>
527
+ </body>
528
+ </html>