Guymy97 commited on
Commit
2312d69
·
verified ·
1 Parent(s): 31074a6

Update via AnyCoder

Browse files
Files changed (1) hide show
  1. index.html +117 -356
index.html CHANGED
@@ -8,7 +8,6 @@
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
11
- <!-- Icons -->
12
  <link href="https://unpkg.com/[email protected]/css/boxicons.min.css" rel="stylesheet">
13
 
14
  <style>
@@ -60,63 +59,24 @@
60
  overflow-x: hidden;
61
  }
62
 
63
- /* Header */
64
  header.appbar {
65
- position: sticky;
66
- top: 0;
67
- z-index: 50;
68
- backdrop-filter: blur(var(--blur));
69
- -webkit-backdrop-filter: blur(var(--blur));
70
  background: color-mix(in oklab, var(--bg-soft) 80%, transparent);
71
  border-bottom: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
72
  }
73
  .appbar-inner {
74
- max-width: 1200px;
75
- margin: 0 auto;
76
  padding: 14px clamp(14px, 3vw, 28px);
77
- display: grid;
78
- grid-template-columns: 1fr auto 1fr;
79
- align-items: center;
80
- gap: 12px;
81
- }
82
- .brand {
83
- display: flex;
84
- align-items: center;
85
- gap: 12px;
86
- }
87
- .logo {
88
- width: 40px; height: 40px;
89
- background: linear-gradient(135deg, var(--primary), var(--accent));
90
- border-radius: 14px;
91
- box-shadow: var(--shadow);
92
- position: relative;
93
- isolation: isolate;
94
- }
95
- .logo::after {
96
- content: "";
97
- position: absolute; inset: 2px;
98
- background: conic-gradient(from 180deg at 50% 50%, rgba(255,255,255,.18), transparent 30%, rgba(255,255,255,.18) 60%, transparent 85%);
99
- mix-blend-mode: overlay;
100
- border-radius: 12px;
101
- filter: blur(6px);
102
- }
103
- .brand h1 {
104
- margin: 0;
105
- font-size: clamp(16px, 2.3vw, 20px);
106
- letter-spacing: .3px;
107
- font-weight: 800;
108
- }
109
- .brand small {
110
- display: block;
111
- color: var(--muted);
112
- font-weight: 500;
113
- font-size: 12px;
114
- letter-spacing: .4px;
115
  }
 
 
 
 
 
116
 
117
- .top-actions {
118
- display: flex; align-items: center; justify-content: center; gap: 12px;
119
- }
120
  .top-actions a {
121
  color: var(--muted); text-decoration: none; font-weight: 600;
122
  font-size: 13px; padding: 8px 12px; border-radius: 999px;
@@ -125,35 +85,16 @@
125
  }
126
  .top-actions a:hover { color: var(--txt); border-color: color-mix(in oklab, var(--txt) 16%, transparent); }
127
 
128
- .header-right {
129
- display: flex; justify-content: flex-end; align-items: center; gap: 8px;
130
- }
131
- .anycoder {
132
- color: var(--primary); text-decoration: none; font-weight: 700;
133
- border: 1px dashed color-mix(in oklab, var(--primary) 60%, transparent);
134
- padding: 8px 12px; border-radius: 999px; background: color-mix(in oklab, var(--primary) 12%, transparent);
135
- }
136
- .theme-toggle {
137
- width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center;
138
- border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
139
- background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
140
- cursor: pointer; color: var(--muted);
141
- }
142
 
143
- /* Layout */
144
  main {
145
- max-width: 1200px;
146
- margin: 0 auto;
147
- padding: clamp(18px, 3vw, 28px);
148
- display: grid;
149
- grid-template-columns: 360px 1fr;
150
- gap: var(--gap);
151
- }
152
- @media (max-width: 980px) {
153
- main { grid-template-columns: 1fr; }
154
  }
 
155
 
156
- /* Card */
157
  .card {
158
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
159
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
@@ -161,202 +102,114 @@
161
  box-shadow: var(--shadow);
162
  }
163
 
164
- /* Login */
165
- .login {
166
- padding: clamp(18px, 3.2vw, 28px);
167
- position: sticky;
168
- top: calc(62px + 12px);
169
- align-self: start;
170
- }
171
- @media (max-width: 980px) {
172
- .login { position: relative; top: 0; }
173
- }
174
- .login-header {
175
- display: grid; gap: 8px;
176
- }
177
- .login-header h2 {
178
- margin: 0; font-size: clamp(18px, 2.6vw, 22px);
179
- }
180
  .subtitle { color: var(--muted); font-size: 13px; }
181
 
182
- .mac-grid {
183
- display: grid;
184
- grid-template-columns: repeat(6, 1fr);
185
- gap: 8px;
186
- margin: 16px 0 6px;
187
- }
188
- .mac-input {
189
- position: relative;
190
- }
191
  .mac-input input {
192
- width: 100%;
193
- padding: 14px 0;
194
- text-align: center;
195
- font-size: 18px;
196
- letter-spacing: 1px;
197
- border-radius: var(--radius-lg);
198
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
199
- background: color-mix(in oklab, var(--bg-soft) 70%, transparent);
200
- color: var(--txt);
201
- outline: none;
202
- transition: .2s border-color, .2s background;
203
  }
204
  .mac-input input:focus {
205
  border-color: color-mix(in oklab, var(--primary) 60%, transparent);
206
  background: color-mix(in oklab, var(--primary) 8%, transparent);
207
  box-shadow: 0 0 0 6px color-mix(in oklab, var(--primary) 10%, transparent);
208
  }
209
- .login .actions {
210
- display: flex; align-items: center; gap: 10px; margin-top: 12px; flex-wrap: wrap;
211
- }
212
  .btn {
213
- display: inline-flex; align-items: center; gap: 8px;
214
- padding: 12px 16px; border-radius: 12px;
215
  border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
216
  background: linear-gradient(180deg, color-mix(in oklab, var(--elev) 86%, transparent), transparent);
217
- color: var(--txt); font-weight: 700; cursor: pointer;
218
- transition: .2s transform, .2s border-color, .2s background, .2s color;
219
  }
220
  .btn:hover { transform: translateY(-1px); border-color: color-mix(in oklab, var(--txt) 16%, transparent); }
221
  .btn.primary {
222
  border-color: color-mix(in oklab, var(--primary) 60%, transparent);
223
  background: linear-gradient(180deg, color-mix(in oklab, var(--primary) 22%, transparent), transparent);
224
- color: white;
225
- box-shadow: 0 10px 18px color-mix(in oklab, var(--primary) 22%, transparent);
226
  }
227
  .btn.ghost { background: transparent; }
228
- .hint {
229
- font-size: 12px; color: var(--muted); margin-top: 6px;
230
- display: flex; align-items: center; gap: 6px;
231
- }
232
 
233
- .provider {
234
- margin-top: 18px;
235
- display: grid; gap: 10px;
236
- }
237
- .provider .row {
238
- display: grid;
239
- grid-template-columns: 1fr auto;
240
- gap: 10px;
241
- align-items: center;
242
- }
243
- .select, .input {
244
- position: relative;
245
- }
246
  select, .input input {
247
- width: 100%;
248
- appearance: none;
249
- padding: 14px 16px;
250
- border-radius: var(--radius-lg);
251
  border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
252
- background: color-mix(in oklab, var(--bg-soft) 70%, transparent);
253
- color: var(--txt);
254
- outline: none;
255
- transition: .2s border-color, .2s background;
256
- font-weight: 600;
257
  }
258
  select:focus, .input input:focus {
259
  border-color: color-mix(in oklab, var(--accent) 60%, transparent);
260
  background: color-mix(in oklab, var(--accent) 8%, transparent);
261
  box-shadow: 0 0 0 6px color-mix(in oklab, var(--accent) 10%, transparent);
262
  }
263
- .select i {
264
- position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
265
- color: var(--muted);
266
- pointer-events: none;
267
- }
268
 
269
- .divider {
270
- height: 1px; background: color-mix(in oklab, var(--txt) 10%, transparent);
271
- margin: 16px 0;
272
- }
273
 
274
- .status {
275
- display: grid; gap: 8px;
276
- padding: 10px 12px;
277
- background: color-mix(in oklab, var(--bg-soft) 60%, transparent);
278
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
279
- border-radius: var(--radius-md);
280
- }
281
- .status .line {
282
- display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted);
283
- }
284
 
285
- /* Content Area */
286
- .content {
287
- display: grid; gap: var(--gap);
288
- }
289
  .hero {
290
  position: relative; overflow: clip; border-radius: var(--radius-xl);
291
  background: linear-gradient(135deg, color-mix(in oklab, var(--primary) 16%, transparent), color-mix(in oklab, var(--accent) 10%, transparent)),
292
  url('https://images.unsplash.com/photo-1478720568477-152d9b164e26?q=80&w=1600&auto=format&fit=crop') center/cover no-repeat;
293
  min-height: 200px;
294
  }
295
- .hero::after {
296
- content: ""; position: absolute; inset: 0;
297
- background: linear-gradient(180deg, rgba(0,0,0,.35), transparent 40%, rgba(0,0,0,.55));
298
- pointer-events: none;
299
- }
300
- .hero-inner {
301
- position: relative; z-index: 1; padding: clamp(18px, 3vw, 28px);
302
- display: grid; gap: 8px;
303
- }
304
  .hero h2 { margin: 0; font-size: clamp(20px, 3vw, 28px); }
305
  .hero p { margin: 0; color: #dfe7ff; max-width: 70ch; }
306
 
307
- /* Tabs */
308
  .tabs {
309
  display: flex; gap: 10px; flex-wrap: wrap;
310
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
311
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
312
- border-radius: var(--radius-xl);
313
- padding: 8px;
314
  }
315
- .tab {
316
- padding: 10px 14px; border-radius: 999px; cursor: pointer; font-weight: 700;
317
- color: var(--muted);
318
- border: 1px solid transparent;
319
- background: transparent;
320
- transition: .2s;
321
  }
322
- .tab.active {
323
- color: var(--txt);
324
- background: color-mix(in oklab, var(--primary) 12%, transparent);
325
- border-color: color-mix(in oklab, var(--primary) 40%, transparent);
326
- box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--primary) 18%, transparent);
327
  }
 
 
 
 
328
 
329
- /* Grid of channels */
330
- .grid {
331
- display: grid;
332
- grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
333
- gap: var(--gap);
334
- }
335
  .channel {
336
  position: relative; overflow: clip; border-radius: 16px; isolation: isolate;
337
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
338
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
339
  transition: .2s transform, .2s box-shadow, .2s border-color;
340
  }
341
- .channel:hover {
342
- transform: translateY(-3px);
343
- border-color: color-mix(in oklab, var(--primary) 24%, transparent);
344
- box-shadow: 0 16px 30px rgba(0,0,0,.18);
345
- }
346
- .poster {
347
- aspect-ratio: 16/9; background: #0a0d14; position: relative;
348
- }
349
  .poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
350
  .badge {
351
  position: absolute; top: 10px; left: 10px;
352
- background: color-mix(in oklab, var(--accent) 26%, transparent);
353
- color: white; padding: 6px 10px; font-size: 12px; border-radius: 999px;
354
- display: inline-flex; align-items: center; gap: 6px;
355
- box-shadow: 0 6px 16px color-mix(in oklab, var(--accent) 18%, transparent);
356
- }
357
- .meta {
358
- padding: 12px; display: grid; gap: 6px;
359
  }
 
360
  .meta .title { font-weight: 800; }
361
  .meta .sub { color: var(--muted); font-size: 13px; }
362
  .meta .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -367,72 +220,28 @@
367
  color: white; display: inline-flex; align-items: center; gap: 8px;
368
  }
369
 
370
- /* Player modal */
371
- dialog.player {
372
- width: min(980px, 96vw);
373
- border: none;
374
- padding: 0;
375
- background: transparent;
376
- }
377
  dialog::backdrop {
378
  background: radial-gradient(800px 400px at 80% 10%, rgba(91,140,255,.3), transparent 60%),
379
  radial-gradient(600px 300px at 20% 90%, rgba(33,212,163,.25), transparent 60%),
380
  rgba(0,0,0,.65);
381
  backdrop-filter: blur(10px);
382
  }
383
- .player-card {
384
- border-radius: 18px; overflow: clip; background: var(--surface);
385
- border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
386
- box-shadow: var(--shadow);
387
- }
388
- .player-top {
389
- position: relative; background: #000; aspect-ratio: 16/9;
390
- display: grid; place-items: center;
391
- }
392
- video {
393
- width: 100%; height: 100%; object-fit: contain; background: #000;
394
- }
395
- .player-bottom {
396
- padding: 12px; display: grid; gap: 10px;
397
- }
398
- .player-actions {
399
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
400
- }
401
- .chip {
402
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
403
- background: color-mix(in oklab, var(--bg-soft) 60%, transparent);
404
- padding: 8px 12px; border-radius: 999px; color: var(--muted); font-weight: 700;
405
- }
406
- .close {
407
- margin-left: auto;
408
- background: transparent; border: 1px solid color-mix(in oklab, var(--txt) 12%, transparent);
409
- color: var(--muted); padding: 8px 12px; border-radius: 10px; cursor: pointer; font-weight: 800;
410
- }
411
  .close:hover { color: var(--txt); }
412
 
413
- /* Empty state */
414
- .empty {
415
- padding: 30px; text-align: center; color: var(--muted);
416
- border: 1px dashed color-mix(in oklab, var(--txt) 14%, transparent);
417
- border-radius: var(--radius-xl);
418
- background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
419
- }
420
 
421
- /* Footer */
422
- footer {
423
- max-width: 1200px; margin: 20px auto 60px; padding: 0 clamp(18px, 3vw, 28px);
424
- color: var(--muted); font-size: 12px;
425
- display: grid; gap: 10px;
426
- }
427
- .credits {
428
- display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
429
- }
430
- .tag {
431
- border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
432
- border-radius: 999px; padding: 6px 10px; background: color-mix(in oklab, var(--bg-soft) 60%, transparent);
433
- }
434
 
435
- /* Utility */
436
  .hidden { display: none !important; }
437
  .shake { animation: shake .22s 2; }
438
  @keyframes shake {
@@ -468,7 +277,6 @@
468
  </header>
469
 
470
  <main>
471
- <!-- Login / Settings -->
472
  <section class="card login" id="loginCard">
473
  <div class="login-header">
474
  <h2>Se connecter</h2>
@@ -513,11 +321,9 @@
513
  </form>
514
 
515
  <div class="divider"></div>
516
-
517
  <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>
518
  </section>
519
 
520
- <!-- Content -->
521
  <section class="content">
522
  <div class="hero">
523
  <div class="hero-inner">
@@ -534,10 +340,20 @@
534
  <button class="tab" data-tab="recent"><i class='bx bx-time-five'></i> Récents</button>
535
  </div>
536
 
537
- <!-- Grids -->
538
- <div id="grid-live" class="grid" role="region" aria-label="Chaînes en direct">
539
- <!-- Filled by JS -->
 
 
 
 
 
 
 
 
540
  </div>
 
 
541
  <div id="grid-vod" class="grid hidden" role="region" aria-label="Films VOD"></div>
542
  <div id="grid-series" class="grid hidden" role="region" aria-label="Séries TV"></div>
543
  <div id="grid-favs" class="grid hidden" role="region" aria-label="Favoris">
@@ -555,7 +371,6 @@
555
  </section>
556
  </main>
557
 
558
- <!-- Player Modal -->
559
  <dialog class="player" id="playerDialog">
560
  <div class="player-card">
561
  <div class="player-top">
@@ -610,14 +425,10 @@
610
  const pasteMacBtn = document.getElementById('pasteMac');
611
  const saveConfigBtn = document.getElementById('saveConfig');
612
 
613
- function formatHex(val) {
614
- return val.replace(/[^0-9a-f]/gi, '').toUpperCase().slice(0, 2);
615
- }
616
-
617
  macBlocks.forEach((inp, idx) => {
618
- inp.addEventListener('input', (e) => {
619
- const v = formatHex(inp.value);
620
- inp.value = v;
621
  if (v.length === 2 && idx < macBlocks.length - 1) macBlocks[idx + 1].focus();
622
  });
623
  inp.addEventListener('keydown', (e) => {
@@ -627,40 +438,28 @@
627
  });
628
  inp.addEventListener('paste', (e) => {
629
  const text = (e.clipboardData || window.clipboardData).getData('text');
630
- if (!text) return;
631
- e.preventDefault();
632
- fillMacFromString(text);
633
  });
634
  });
635
 
636
- function macToString() {
637
- return macBlocks.map(b => (b.value || '00')).join(':');
638
- }
639
  function setStatus(text, extra) {
640
  statusBox.innerHTML = `
641
  <div class="line"><i class='bx bx-info-circle'></i> ${text}</div>
642
  <div class="line"><i class='bx bx-time'></i> Valide jusqu’à: ${extra || '—'}</div>
643
  `;
644
  }
645
- function validMacString(s) {
646
- return /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/.test(s);
647
- }
648
  function fillMacFromString(text) {
649
  const cleaned = text.trim().replace(/-/g, ':').replace(/\./g, '').toUpperCase();
650
  let mac = cleaned;
651
- if (/^[0-9A-F]{12}$/.test(cleaned)) {
652
- mac = cleaned.match(/.{1,2}/g).join(':');
653
- }
654
  if (/^([0-9A-F]{2}[:]){5}[0-9A-F]{2}$/.test(mac)) {
655
  mac.split(':').forEach((pair, i) => macBlocks[i].value = pair);
656
- } else {
657
- flashInvalid();
658
- }
659
  }
660
  function flashInvalid() {
661
- macForm.classList.remove('shake');
662
- void macForm.offsetWidth;
663
- macForm.classList.add('shake');
664
  }
665
 
666
  randomMacBtn.addEventListener('click', () => {
@@ -681,15 +480,11 @@
681
  });
682
 
683
  saveConfigBtn.addEventListener('click', () => {
684
- const cfg = {
685
- provider: providerSelect.value,
686
- portal: portalUrl.value
687
- };
688
  localStorage.setItem('iptv_cfg', JSON.stringify(cfg));
689
  setStatus('Configuration sauvegardée', new Date(Date.now() + 86400000).toLocaleString());
690
  });
691
 
692
- // Load saved config to inputs
693
  (function loadCfg(){
694
  const cfg = JSON.parse(localStorage.getItem('iptv_cfg') || '{"provider":"custom","portal":""}');
695
  providerSelect.value = cfg.provider || 'custom';
@@ -698,30 +493,23 @@
698
  if (macSaved && validMacString(macSaved)) fillMacFromString(macSaved);
699
  })();
700
 
701
- // Simulated MAC auth
702
  macForm.addEventListener('submit', async (e) => {
703
  e.preventDefault();
704
  const mac = macToString();
705
- if (!validMacString(mac)) {
706
- flashInvalid();
707
- setStatus('Code MAC invalide', '—');
708
- return;
709
- }
710
  setStatus('Connexion en cours...', '—');
711
- await new Promise(r => setTimeout(r, 800));
712
- const cfg = JSON.parse(localStorage.getItem('iptv_cfg') || '{"provider":"custom"}');
713
  const last = parseInt(mac.split(':')[5], 16);
714
  if (Number.isFinite(last) && last % 2 === 0) {
715
  setStatus('Connecté', new Date(Date.now() + 7 * 86400000).toLocaleString());
716
  localStorage.setItem('iptv_mac', mac);
717
- document.body.dispatchEvent(new CustomEvent('iptv:connected', { detail: { mac, cfg } }));
718
  } else {
719
- setStatus('Accès refusé par le portail', '—');
720
- flashInvalid();
721
  }
722
  });
723
 
724
- // Content data (demo)
725
  const data = {
726
  live: [
727
  { id: 'l1', title: 'Infos 24', group: 'News', logo: 'https://picsum.photos/seed/news1/800/450', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', tag: 'HD' },
@@ -754,15 +542,20 @@
754
  recent: document.getElementById('grid-recent')
755
  };
756
 
 
757
  tabs.forEach(tab => {
758
  tab.addEventListener('click', () => {
759
  tabs.forEach(t => t.classList.remove('active'));
760
  tab.classList.add('active');
761
  const chosen = tab.dataset.tab;
762
  Object.keys(grids).forEach(k => grids[k].classList.toggle('hidden', k !== chosen));
 
 
 
763
  });
764
  });
765
 
 
766
  function createCard(item, type) {
767
  const el = document.createElement('div');
768
  el.className = 'channel';
@@ -784,45 +577,13 @@
784
  </div>
785
  </div>
786
  `;
 
 
 
787
  el.querySelector('.play').addEventListener('click', () => openPlayer(item, type));
788
- el.querySelector('.add-fav').addEventListener('click', (ev) => {
789
- ev.stopPropagation();
790
- toggleFav(item);
791
- });
792
  return el;
793
  }
794
 
795
- function renderAll() {
796
- grids.live.innerHTML = '';
797
- data.live.forEach(ch => grids.live.appendChild(createCard(ch, 'live')));
798
- grids.vod.innerHTML = '';
799
- data.vod.forEach(v => grids.vod.appendChild(createCard(v, 'vod')));
800
- grids.series.innerHTML = '';
801
- data.series.forEach(s => grids.series.appendChild(createCard(s, 'series')));
802
- renderFavs();
803
- renderRecent();
804
- }
805
-
806
- // Favorites
807
- function getFavs() { return JSON.parse(localStorage.getItem('iptv_favs') || '[]'); }
808
- function setFavs(f) { localStorage.setItem('iptv_favs', JSON.stringify(f)); renderFavs(); }
809
- function toggleFav(item) {
810
- const favs = getFavs();
811
- const idx = favs.findIndex(x => x.id === item.id);
812
- if (idx >= 0) favs.splice(idx, 1); else favs.push(item);
813
- setFavs(favs);
814
- }
815
- function renderFavs() {
816
- const favs = getFavs();
817
- const wrap = grids.favs;
818
- wrap.innerHTML = '';
819
- if (!favs.length) {
820
- const empty = document.createElement('div');
821
- empty.className = 'empty';
822
- empty.innerHTML = "<i class='bx bx-star' style='font-size:32px;'></i><div>Aucun favori pour le moment.</div>";
823
- wrap.appendChild(empty);
824
- return;
825
- }
826
- wrap.classList.add('grid');
827
- favs.forEach(it => {
828
- const type = it.year ? 'vod' : (it.group ? 'live
 
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
 
11
  <link href="https://unpkg.com/[email protected]/css/boxicons.min.css" rel="stylesheet">
12
 
13
  <style>
 
59
  overflow-x: hidden;
60
  }
61
 
 
62
  header.appbar {
63
+ position: sticky; top: 0; z-index: 50;
64
+ backdrop-filter: blur(var(--blur)); -webkit-backdrop-filter: blur(var(--blur));
 
 
 
65
  background: color-mix(in oklab, var(--bg-soft) 80%, transparent);
66
  border-bottom: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
67
  }
68
  .appbar-inner {
69
+ max-width: 1200px; margin: 0 auto;
 
70
  padding: 14px clamp(14px, 3vw, 28px);
71
+ display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
+ .brand { display: flex; align-items: center; gap: 12px; }
74
+ .logo { width: 40px; height: 40px; background: linear-gradient(135deg, var(--primary), var(--accent)); border-radius: 14px; box-shadow: var(--shadow); position: relative; isolation: isolate; }
75
+ .logo::after { content: ""; position: absolute; inset: 2px; background: conic-gradient(from 180deg at 50% 50%, rgba(255,255,255,.18), transparent 30%, rgba(255,255,255,.18) 60%, transparent 85%); mix-blend-mode: overlay; border-radius: 12px; filter: blur(6px); }
76
+ .brand h1 { margin: 0; font-size: clamp(16px, 2.3vw, 20px); letter-spacing: .3px; font-weight: 800; }
77
+ .brand small { display: block; color: var(--muted); font-weight: 500; font-size: 12px; letter-spacing: .4px; }
78
 
79
+ .top-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
 
 
80
  .top-actions a {
81
  color: var(--muted); text-decoration: none; font-weight: 600;
82
  font-size: 13px; padding: 8px 12px; border-radius: 999px;
 
85
  }
86
  .top-actions a:hover { color: var(--txt); border-color: color-mix(in oklab, var(--txt) 16%, transparent); }
87
 
88
+ .header-right { display: flex; justify-content: flex-end; align-items: center; gap: 8px; }
89
+ .anycoder { color: var(--primary); text-decoration: none; font-weight: 700; border: 1px dashed color-mix(in oklab, var(--primary) 60%, transparent); padding: 8px 12px; border-radius: 999px; background: color-mix(in oklab, var(--primary) 12%, transparent); }
90
+ .theme-toggle { width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center; border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent); background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent); cursor: pointer; color: var(--muted); }
 
 
 
 
 
 
 
 
 
 
 
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);
100
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
 
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);
164
  background: linear-gradient(135deg, color-mix(in oklab, var(--primary) 16%, transparent), color-mix(in oklab, var(--accent) 10%, transparent)),
165
  url('https://images.unsplash.com/photo-1478720568477-152d9b164e26?q=80&w=1600&auto=format&fit=crop') center/cover no-repeat;
166
  min-height: 200px;
167
  }
168
+ .hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,.35), transparent 40%, rgba(0,0,0,.55)); pointer-events: none; }
169
+ .hero-inner { position: relative; z-index: 1; padding: clamp(18px, 3vw, 28px); display: grid; gap: 8px; }
 
 
 
 
 
 
 
170
  .hero h2 { margin: 0; font-size: clamp(20px, 3vw, 28px); }
171
  .hero p { margin: 0; color: #dfe7ff; max-width: 70ch; }
172
 
 
173
  .tabs {
174
  display: flex; gap: 10px; flex-wrap: wrap;
175
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
176
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
177
+ border-radius: var(--radius-xl); padding: 8px;
178
+ position: sticky; top: calc(62px + 12px); z-index: 10;
179
  }
180
+ .tab { padding: 10px 14px; border-radius: 999px; cursor: pointer; font-weight: 700; color: var(--muted); border: 1px solid transparent; background: transparent; transition: .2s; }
181
+ .tab.active { color: var(--txt); background: color-mix(in oklab, var(--primary) 12%, transparent); border-color: color-mix(in oklab, var(--primary) 40%, transparent); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--primary) 18%, transparent); }
182
+
183
+ .toolbar {
184
+ display: grid; grid-template-columns: 1fr auto; gap: 10px; align-items: center;
 
185
  }
186
+ .searchbar {
187
+ display: grid; grid-template-columns: 24px 1fr 24px; align-items: center;
188
+ padding: 10px 12px; border-radius: 12px;
189
+ background: color-mix(in oklab, var(--bg-soft) 70%, transparent);
190
+ border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent);
191
  }
192
+ .searchbar input { border: none; outline: none; background: transparent; color: var(--txt); font-weight: 600; }
193
+ .filters { display: flex; gap: 8px; flex-wrap: wrap; }
194
+ .filter-chip { padding: 8px 12px; border-radius: 999px; border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent); background: color-mix(in oklab, var(--bg-soft) 60%, transparent); color: var(--muted); font-weight: 700; cursor: pointer; }
195
+ .filter-chip.active { color: var(--txt); border-color: color-mix(in oklab, var(--accent) 50%, transparent); background: color-mix(in oklab, var(--accent) 12%, transparent); }
196
 
197
+ .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr)); gap: var(--gap); }
 
 
 
 
 
198
  .channel {
199
  position: relative; overflow: clip; border-radius: 16px; isolation: isolate;
200
  background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent);
201
  border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent);
202
  transition: .2s transform, .2s box-shadow, .2s border-color;
203
  }
204
+ .channel:hover { transform: translateY(-3px); border-color: color-mix(in oklab, var(--primary) 24%, transparent); box-shadow: 0 16px 30px rgba(0,0,0,.18); }
205
+ .poster { aspect-ratio: 16/9; background: #0a0d14; position: relative; }
 
 
 
 
 
 
206
  .poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
207
  .badge {
208
  position: absolute; top: 10px; left: 10px;
209
+ background: color-mix(in oklab, var(--accent) 26%, transparent); color: white; padding: 6px 10px; font-size: 12px; border-radius: 999px;
210
+ display: inline-flex; align-items: center; gap: 6px; box-shadow: 0 6px 16px color-mix(in oklab, var(--accent) 18%, transparent);
 
 
 
 
 
211
  }
212
+ .meta { padding: 12px; display: grid; gap: 6px; }
213
  .meta .title { font-weight: 800; }
214
  .meta .sub { color: var(--muted); font-size: 13px; }
215
  .meta .row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
 
220
  color: white; display: inline-flex; align-items: center; gap: 8px;
221
  }
222
 
223
+ dialog.player { width: min(980px, 96vw); border: none; padding: 0; background: transparent; }
 
 
 
 
 
 
224
  dialog::backdrop {
225
  background: radial-gradient(800px 400px at 80% 10%, rgba(91,140,255,.3), transparent 60%),
226
  radial-gradient(600px 300px at 20% 90%, rgba(33,212,163,.25), transparent 60%),
227
  rgba(0,0,0,.65);
228
  backdrop-filter: blur(10px);
229
  }
230
+ .player-card { border-radius: 18px; overflow: clip; background: var(--surface); border: 1px solid color-mix(in oklab, var(--txt) 8%, transparent); box-shadow: var(--shadow); }
231
+ .player-top { position: relative; background: #000; aspect-ratio: 16/9; display: grid; place-items: center; }
232
+ video { width: 100%; height: 100%; object-fit: contain; background: #000; }
233
+ .player-bottom { padding: 12px; display: grid; gap: 10px; }
234
+ .player-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
235
+ .chip { border: 1px solid color-mix(in oklab, var(--txt) 10%, transparent); background: color-mix(in oklab, var(--bg-soft) 60%, transparent); padding: 8px 12px; border-radius: 999px; color: var(--muted); font-weight: 700; }
236
+ .close { margin-left: auto; background: transparent; border: 1px solid color-mix(in oklab, var(--txt) 12%, transparent); color: var(--muted); padding: 8px 12px; border-radius: 10px; cursor: pointer; font-weight: 800; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  .close:hover { color: var(--txt); }
238
 
239
+ .empty { padding: 30px; text-align: center; color: var(--muted); border: 1px dashed color-mix(in oklab, var(--txt) 14%, transparent); border-radius: var(--radius-xl); background: linear-gradient(180deg, color-mix(in oklab, var(--surface) 86%, transparent), transparent); }
 
 
 
 
 
 
240
 
241
+ footer { max-width: 1200px; margin: 20px auto 60px; padding: 0 clamp(18px, 3vw, 28px); color: var(--muted); font-size: 12px; display: grid; gap: 10px; }
242
+ .credits { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
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 {
 
277
  </header>
278
 
279
  <main>
 
280
  <section class="card login" id="loginCard">
281
  <div class="login-header">
282
  <h2>Se connecter</h2>
 
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">
 
340
  <button class="tab" data-tab="recent"><i class='bx bx-time-five'></i> Récents</button>
341
  </div>
342
 
343
+ <div class="toolbar">
344
+ <div class="searchbar">
345
+ <i class='bx bx-search' aria-hidden="true"></i>
346
+ <input id="searchInput" type="search" placeholder="Rechercher dans tous les onglets…" />
347
+ <button id="clearSearch" title="Effacer" class="btn ghost" style="border:none;background:transparent;color:var(--muted);padding:0;display:grid;place-items:center"><i class='bx bx-x'></i></button>
348
+ </div>
349
+ <div class="filters">
350
+ <button class="filter-chip active" data-filter="all"><i class='bx bx-layer'></i> Tous</button>
351
+ <button class="filter-chip" data-filter="hd"><i class='bx bx-video'></i> HD/4K</button>
352
+ <button class="filter-chip" data-filter="recent"><i class='bx bx-bolt-circle'></i> Nouveautés</button>
353
+ </div>
354
  </div>
355
+
356
+ <div id="grid-live" class="grid" role="region" aria-label="Chaînes en direct"></div>
357
  <div id="grid-vod" class="grid hidden" role="region" aria-label="Films VOD"></div>
358
  <div id="grid-series" class="grid hidden" role="region" aria-label="Séries TV"></div>
359
  <div id="grid-favs" class="grid hidden" role="region" aria-label="Favoris">
 
371
  </section>
372
  </main>
373
 
 
374
  <dialog class="player" id="playerDialog">
375
  <div class="player-card">
376
  <div class="player-top">
 
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) => {
 
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', () => {
 
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';
 
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: [
515
  { id: 'l1', title: 'Infos 24', group: 'News', logo: 'https://picsum.photos/seed/news1/800/450', url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', tag: 'HD' },
 
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
  });
556
  });
557
 
558
+ // Build cards
559
  function createCard(item, type) {
560
  const el = document.createElement('div');
561
  el.className = 'channel';
 
577
  </div>
578
  </div>
579
  `;
580
+ el.dataset.search = [item.title, item.group || '', item.year || '', item.tag || ''].join(' ').toLowerCase();
581
+ el.dataset.quality = ((item.tag || '').toLowerCase().includes('hd') || (item.tag || '').toLowerCase().includes('4k')) ? 'hd' : 'other';
582
+ el.dataset.recent = (['l3','v3','s3'].includes(item.id)) ? '1' : '0';
583
  el.querySelector('.play').addEventListener('click', () => openPlayer(item, type));
584
+ el.querySelector('.add-fav').addEventListener('click', (ev) => { ev.stopPropagation(); toggleFav(item, type); });
 
 
 
585
  return el;
586
  }
587
 
588
+ // Render functions
589
+ function