/
liveprogramma
/
liveprogrammav2
Обзор
Документация
Войти
/
liveprogramma
/
liveprogrammav2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
index.html
809 строк
33 KB
TwixoffStudio
Create: README.md, vercel.json, embed.html, index.html, package.json, package-lock.json, player.html
21 июн 2026, 22:38
Верифицирован
21 июн 2026, 22:38
3c3ba44
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> <title>📺 LiveПрограмма — ТВ расписание онлайн</title> <meta name="description" content="Расписание телепрограмм в прямом эфире. Смотри что идёт сейчас на ТВ."/> <link rel="preconnect" href="https://fonts.googleapis.com"/> <link href="https://fonts.googleapis.com/css2?family=Russo+One&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/> <style> *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} :root{ --bg:#000;--s1:#0d0d0d;--s2:#181818;--s3:#222;--border:#2a2a2a; --text:#fff;--muted:#888;--accent:#fe2c55;--accent2:#ff6b81; --green:#25f4ee;--gold:#ffd700; --r:12px; --safe-bottom: env(safe-area-inset-bottom, 0px); } html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:'Inter',sans-serif;-webkit-tap-highlight-color:transparent} /* ── LAYOUT ── */ .app{display:flex;flex-direction:column;height:100vh;height:100dvh} /* ── TOP BAR ── */ .topbar{ display:flex;align-items:center;padding:12px 16px 10px; background:linear-gradient(to bottom,rgba(0,0,0,.95),rgba(0,0,0,.6)); position:relative;z-index:20;flex-shrink:0;gap:10px } .logo{font-family:'Russo One',sans-serif;font-size:18px;letter-spacing:.3px;flex:1} .logo .live-dot{ display:inline-block;width:8px;height:8px;background:var(--accent); border-radius:50%;margin-right:6px;animation:pulse 1.5s ease infinite } @keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}} .search-btn{ width:38px;height:38px;border-radius:50%;background:var(--s3); border:1px solid var(--border);display:flex;align-items:center;justify-content:center; cursor:pointer;font-size:17px;transition:background .2s } .search-btn:hover{background:var(--s2)} /* ── CHANNEL TABS ── */ .tabs{ display:flex;overflow-x:auto;gap:8px;padding:8px 14px; background:rgba(0,0,0,.8);flex-shrink:0; scrollbar-width:none;-ms-overflow-style:none } .tabs::-webkit-scrollbar{display:none} .tab{ white-space:nowrap;font-size:13px;font-weight:600; padding:6px 14px;border-radius:20px; background:var(--s2);border:1.5px solid var(--border); color:var(--muted);cursor:pointer;transition:all .2s;flex-shrink:0 } .tab.active{background:var(--accent);border-color:var(--accent);color:#fff} .tab.on-air{position:relative} .tab.on-air::after{ content:'';position:absolute;top:3px;right:3px;width:6px;height:6px; background:var(--green);border-radius:50%;border:1.5px solid #000 } /* ── FEED — TikTok скролл ── */ .feed{ flex:1;overflow-y:scroll;overflow-x:hidden; scroll-snap-type:y mandatory; -webkit-overflow-scrolling:touch; scrollbar-width:none; } .feed::-webkit-scrollbar{display:none} /* ── CARD — одна передача ── */ .card{ scroll-snap-align:start; min-height:100%;height:100dvh; position:relative;display:flex;flex-direction:column; background:var(--bg);overflow:hidden } /* Фоновое размытое превью */ .card-bg{ position:absolute;inset:0; background:linear-gradient(160deg,#1a0a1e,#0a0a1a,#001a10); z-index:0 } .card-bg img{ width:100%;height:100%;object-fit:cover;opacity:.18; filter:blur(20px) saturate(1.5) } /* Плеер (верхняя часть) */ .card-player{ position:relative;z-index:1; width:100%;aspect-ratio:16/9;background:#000; display:flex;align-items:center;justify-content:center; flex-shrink:0;overflow:hidden } .card-player iframe{width:100%;height:100%;border:none} .player-placeholder{ width:100%;height:100%;display:flex;flex-direction:column; align-items:center;justify-content:center;gap:10px; background:linear-gradient(135deg,#111,#0a0a0a) } .play-ring{ width:72px;height:72px;border-radius:50%; border:2px solid rgba(255,255,255,.15); display:flex;align-items:center;justify-content:center; cursor:pointer;transition:all .2s; background:rgba(255,255,255,.06) } .play-ring:hover,.play-ring:active{background:rgba(254,44,85,.2);border-color:var(--accent)} .play-ring svg{width:28px;height:28px;fill:#fff;margin-left:4px} /* Инфо */ .card-body{ position:relative;z-index:1;flex:1; padding:16px 16px calc(16px + var(--safe-bottom)); display:flex;flex-direction:column;justify-content:space-between; overflow:hidden } /* Теперь в эфире */ .live-badge{ display:inline-flex;align-items:center;gap:5px; background:var(--accent);color:#fff; font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase; padding:4px 10px;border-radius:20px;margin-bottom:8px;width:fit-content } .live-badge .dot{width:6px;height:6px;background:#fff;border-radius:50%;animation:pulse 1s ease infinite} .next-badge{ display:inline-flex;align-items:center;gap:5px; background:rgba(255,255,255,.12);color:var(--muted); font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase; padding:4px 10px;border-radius:20px;margin-bottom:8px;width:fit-content } .channel-name{font-size:13px;color:var(--green);font-weight:700;margin-bottom:4px} .prog-title{font-family:'Russo One',sans-serif;font-size:clamp(20px,5vw,28px);line-height:1.2;margin-bottom:8px} .prog-time{font-size:13px;color:var(--muted);margin-bottom:10px;display:flex;align-items:center;gap:8px} .prog-time strong{color:var(--text)} .prog-desc{font-size:14px;color:rgba(255,255,255,.65);line-height:1.5; display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} /* Прогресс бар */ .progress-wrap{margin:14px 0 10px;position:relative} .progress-bar{height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden} .progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:2px;transition:width .5s} .progress-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:11px;color:var(--muted)} /* Следующие */ .next-list{margin-top:8px} .next-item{ display:flex;align-items:center;gap:10px; padding:10px 0;border-bottom:1px solid var(--border) } .next-item:last-child{border-bottom:none} .next-time{font-size:12px;font-weight:700;color:var(--muted);width:42px;flex-shrink:0} .next-title{font-size:13px;font-weight:500;flex:1; white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .next-dur{font-size:11px;color:var(--muted);flex-shrink:0} /* Сайд-экшены (TikTok style) */ .side-actions{ position:absolute;right:14px;bottom:calc(100px + var(--safe-bottom)); z-index:10;display:flex;flex-direction:column;align-items:center;gap:18px } .side-btn{display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer} .side-btn .icon{ width:46px;height:46px;border-radius:50%; background:rgba(255,255,255,.12);backdrop-filter:blur(10px); display:flex;align-items:center;justify-content:center;font-size:20px; transition:background .2s,transform .15s;border:1px solid rgba(255,255,255,.08) } .side-btn:active .icon{transform:scale(.88);background:rgba(254,44,85,.25)} .side-btn span{font-size:10px;color:rgba(255,255,255,.6);font-weight:600} /* Loader */ #loadScreen{ position:fixed;inset:0;background:#000;z-index:100; display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px } .loader-logo{font-family:'Russo One',sans-serif;font-size:28px} .loader-logo em{color:var(--accent);font-style:normal} .loader-bar{width:120px;height:3px;background:var(--s3);border-radius:2px;overflow:hidden} .loader-fill{height:100%;background:var(--accent);width:0;animation:load 2s ease forwards} @keyframes load{to{width:100%}} .loader-txt{font-size:13px;color:var(--muted)} /* Поиск */ #searchOverlay{ display:none;position:fixed;inset:0;background:rgba(0,0,0,.95);z-index:50; flex-direction:column;padding:20px 16px } #searchOverlay.show{display:flex} .search-input-wrap{ display:flex;align-items:center;gap:10px;margin-bottom:16px; background:var(--s2);border:1.5px solid var(--border);border-radius:12px;padding:12px 16px } .search-input{background:none;border:none;color:var(--text);font-size:16px;flex:1;outline:none} .search-close{font-size:22px;cursor:pointer;color:var(--muted)} .search-results{flex:1;overflow-y:auto} .search-item{ padding:12px 0;border-bottom:1px solid var(--border);cursor:pointer } .search-item:hover{opacity:.75} .search-item .si-ch{font-size:12px;color:var(--accent);font-weight:700;margin-bottom:3px} .search-item .si-title{font-size:15px;font-weight:600} .search-item .si-time{font-size:12px;color:var(--muted);margin-top:2px} /* Toast */ .toast{ position:fixed;bottom:90px;left:50%;transform:translateX(-50%) translateY(20px); background:rgba(255,255,255,.12);backdrop-filter:blur(16px); color:#fff;padding:10px 20px;border-radius:24px;font-size:13px;font-weight:600; opacity:0;transition:all .3s;pointer-events:none;white-space:nowrap;z-index:200 } .toast.show{opacity:1;transform:translateX(-50%) translateY(0)} /* Навбар снизу */ .nav{ display:flex;background:rgba(0,0,0,.92);backdrop-filter:blur(20px); border-top:1px solid var(--border);padding:8px 0 calc(8px + var(--safe-bottom)); flex-shrink:0;z-index:20 } .nav-item{ flex:1;display:flex;flex-direction:column;align-items:center;gap:3px; cursor:pointer;padding:4px 0;transition:opacity .2s } .nav-item.active .nav-icon{color:var(--accent)} .nav-item.active .nav-label{color:var(--accent)} .nav-icon{font-size:22px;transition:transform .2s} .nav-item:active .nav-icon{transform:scale(.85)} .nav-label{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--muted)} /* Страница «Все каналы» */ #allPage{display:none;position:fixed;inset:0;background:var(--bg);z-index:40;overflow-y:auto;padding-bottom:80px} #allPage.show{display:block} .all-header{padding:20px 16px 12px;font-family:'Russo One',sans-serif;font-size:20px; position:sticky;top:0;background:rgba(0,0,0,.95);z-index:10;display:flex;align-items:center;gap:10px} .all-back{font-size:22px;cursor:pointer} .all-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;padding:0 14px} .all-card{ background:var(--s2);border:1px solid var(--border);border-radius:12px;padding:14px; cursor:pointer;transition:all .2s } .all-card:hover,.all-card:active{border-color:var(--accent);background:var(--s3)} .all-card .ac-ch{font-size:13px;font-weight:700;color:var(--green);margin-bottom:6px} .all-card .ac-now{font-size:12px;color:var(--text);line-height:1.3; white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .all-card .ac-time{font-size:11px;color:var(--muted);margin-top:4px} .all-card .live-tag{ display:inline-block;background:var(--accent);color:#fff; font-size:9px;font-weight:800;padding:2px 6px;border-radius:8px;margin-bottom:4px } .group-title{padding:16px 16px 8px;font-size:11px;font-weight:700;text-transform:uppercase; letter-spacing:.8px;color:var(--muted)} @media(min-width:600px){ .card-player{aspect-ratio:16/9;max-height:50vh} .prog-title{font-size:32px} } </style> </head> <body> <!-- Loader --> <div id="loadScreen"> <div class="loader-logo">Live<em>Программа</em> 📺</div> <div class="loader-bar"><div class="loader-fill"></div></div> <div class="loader-txt">Загружаем расписание...</div> </div> <div class="app" id="app" style="display:none"> <!-- Топ-бар --> <div class="topbar"> <div class="logo"><span class="live-dot"></span>LiveПрограмма</div> <div class="search-btn" onclick="openSearch()">🔍</div> </div> <!-- Вкладки каналов --> <div class="tabs" id="tabs"></div> <!-- Основной фид --> <div class="feed" id="feed"></div> <!-- Нижний навбар --> <div class="nav"> <div class="nav-item active" onclick="showFeed()"> <div class="nav-icon">📺</div> <div class="nav-label">Эфир</div> </div> <div class="nav-item" onclick="showAll()"> <div class="nav-icon">📋</div> <div class="nav-label">Каналы</div> </div> <div class="nav-item" onclick="openSearch()"> <div class="nav-icon">🔍</div> <div class="nav-label">Поиск</div> </div> </div> </div> <!-- Все каналы --> <div id="allPage"> <div class="all-header"> <span class="all-back" onclick="showFeed()">←</span> Все каналы </div> <div id="allGrid" class="all-grid"></div> </div> <!-- Поиск --> <div id="searchOverlay"> <div class="search-input-wrap"> <input class="search-input" id="searchInput" placeholder="Поиск по программе..." oninput="doSearch()"/> <span class="search-close" onclick="closeSearch()">✕</span> </div> <div class="search-results" id="searchResults"></div> </div> <!-- Toast --> <div class="toast" id="toast"></div> <script> // ─── Плейлисты (твои репозитории) ──────────────────────────── const PLAYLISTS = { primary: "https://raw.githubusercontent.com/OinkTechLLC/livem3u/refs/heads/main/zabava-full.m3u", backup: "https://raw.githubusercontent.com/OinkTechLtd/rulive/refs/heads/main/russ.m3u", smotrim: "https://raw.githubusercontent.com/OinkTechLLC/livem3u/refs/heads/main/smotrim.m3u", }; // PlayerJS от твоих репозиториев const PLAYERJS_URLS = [ "https://raw.githubusercontent.com/OinkTechLtd/rulive/main/playerjs.js", "https://raw.githubusercontent.com/oinktechllc/cdnplayerjs/main/playerjs.js", "https://cdn.jsdelivr.net/npm/playerjs@0.0.12/dist/player.js", ]; // Прокси для стримов const PROXY = "https://secure-272717.tatnet.app/"; // CDN Zabava (без прокси) const ZABAVA_CDN = "zabava-htlive.cdn.ngenix.net"; // ─── Состояние ──────────────────────────────────────────────── let EPG = window.__EPG_DATA__ || null; let channels = []; let streams = {}; // channel_id → m3u8 url let current = 0; let activeChannel = null; const MSK_OFFSET = 3 * 3600000; function nowMSK(){ return new Date(Date.now() + MSK_OFFSET - new Date().getTimezoneOffset()*60000 - MSK_OFFSET + 3*3600000) } function nowTs(){ return Math.floor(Date.now()/1000) } // ─── Boot ───────────────────────────────────────────────────── async function boot(){ // 1. Загружаем EPG if(!EPG){ try{ const r = await fetch("data/schedule.json"); if(r.ok) EPG = await r.json(); } catch(e){} } // 2. Загружаем плейлисты await loadPlaylists(); // 3. Строим интерфейс buildUI(); document.getElementById("loadScreen").style.display = "none"; document.getElementById("app").style.display = "flex"; } // ─── Загрузка M3U плейлистов ───────────────────────────────── async function loadPlaylists(){ for(const [key, url] of Object.entries(PLAYLISTS)){ try{ const r = await fetch(url); if(!r.ok) continue; const txt = await r.text(); parseM3U(txt); } catch(e){} } } function parseM3U(txt){ const lines = txt.split("\n"); let meta = {}; for(let i=0;i<lines.length;i++){ const l = lines[i].trim(); if(l.startsWith("#EXTINF")){ const name = l.match(/,(.+)$/)?.[1]?.trim() || ""; const id = l.match(/tvg-id="([^"]+)"/)?.[1] || ""; const logo = l.match(/tvg-logo="([^"]+)"/)?.[1] || ""; meta = {name, id, logo}; } else if(l && !l.startsWith("#") && meta.name){ const streamUrl = needsProxy(l) ? PROXY + l.replace(/^https?:\/\//,"") : l; // Сопоставляем с каналами по имени/id const chId = matchChannel(meta.name, meta.id); if(chId && !streams[chId]){ streams[chId] = streamUrl; } meta = {}; } } } function needsProxy(url){ if(!url) return false; if(url.includes(ZABAVA_CDN)) return false; if(url.includes("tvrain.tv") || url.includes("akamaized.net")) return false; return false; // по умолчанию не проксируем — пусть браузер сам решает } function matchChannel(name, id){ if(!EPG) return null; const n = name.toLowerCase(); for(const ch of Object.values(EPG.channels)){ const cn = ch.name.toLowerCase(); if(cn === n || ch.id === id || ch.site_id === id) return ch.id; // Частичное совпадение if(n.includes(cn) || cn.includes(n.split(" ")[0])) return ch.id; } return null; } // ─── Построение UI ──────────────────────────────────────────── function buildUI(){ if(!EPG) { buildFallback(); return; } channels = Object.values(EPG.channels); buildTabs(); buildFeed(); buildAllGrid(); // Авто-скролл к текущей передаче setTimeout(()=>{ if(channels[0]) showChannel(channels[0].id); }, 300); } function buildFallback(){ // Нет EPG — показываем просто плеер const feed = document.getElementById("feed"); feed.innerHTML = `<div class="card" style="align-items:center;justify-content:center"> <div style="text-align:center;padding:40px 20px"> <div style="font-size:48px;margin-bottom:16px">📡</div> <div style="font-family:'Russo One',sans-serif;font-size:20px;margin-bottom:10px">Загрузка расписания...</div> <div style="color:#888;font-size:14px">Робот обновляет данные каждые 3 часа</div> </div> </div>`; document.getElementById("loadScreen").style.display = "none"; document.getElementById("app").style.display = "flex"; } // ─── Вкладки ───────────────────────────────────────────────── function buildTabs(){ const tabs = document.getElementById("tabs"); tabs.innerHTML = ""; // «Все» вкладка const all = mk("div","tab",{innerHTML:"Все",onclick:()=>showChannel(null)}); tabs.append(all); channels.forEach(ch=>{ const prog = currentProg(ch.id); const on = prog?.live; const t = mk("div",`tab${on?" on-air":""}`,{ innerHTML: ch.name, onclick: ()=>showChannel(ch.id), "data-id": ch.id, }); tabs.append(t); }); } // ─── Фид карточек ───────────────────────────────────────────── function buildFeed(filterId=null){ const feed = document.getElementById("feed"); feed.innerHTML = ""; const list = filterId ? channels.filter(c=>c.id===filterId) : channels; list.forEach(ch=> feed.append(buildCard(ch))); } function buildCard(ch){ const progs = EPG?.schedule?.[ch.id] || []; const ts = nowTs(); const prog = progs.find(p=> p.start<=ts && p.stop>ts) || progs[0]; const next = progs.filter(p=>p.start>ts).slice(0,3); const stream= streams[ch.id] || null; const card = mk("div","card"); // Фон const bg = mk("div","card-bg"); card.append(bg); // Плеер const player = mk("div","card-player"); if(stream){ player.innerHTML = `<div class="player-placeholder" id="ph-${ch.id}"> <div class="play-ring" onclick="openPlayer('${ch.id}','${stream}')"> <svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg> </div> <div style="font-size:13px;color:#888;margin-top:8px">${esc(ch.name)}</div> </div>`; } else { player.innerHTML = `<div class="player-placeholder"> <div style="font-size:36px;opacity:.3">📡</div> <div style="font-size:12px;color:#555;margin-top:8px">Поток не найден</div> </div>`; } card.append(player); // Тело const body = mk("div","card-body"); // Верхняя часть const top = mk("div"); if(prog?.live){ top.innerHTML = `<div class="live-badge"><span class="dot"></span>В ЭФИРЕ</div>`; } else if(next[0]){ const in_min = Math.round((next[0].start - ts)/60); top.innerHTML = `<div class="next-badge">⏱ Через ${in_min} мин</div>`; } top.innerHTML += `<div class="channel-name">${esc(ch.name)}</div>`; top.innerHTML += `<div class="prog-title">${esc(prog?.title || "Расписание обновляется")}</div>`; if(prog){ const st = fmtTime(prog.start); const sp = fmtTime(prog.stop); const dur= Math.round((prog.stop - prog.start)/60); top.innerHTML += `<div class="prog-time"> <strong>${st} — ${sp}</strong> · ${dur} мин </div>`; if(prog.desc){ top.innerHTML += `<div class="prog-desc">${esc(prog.desc)}</div>`; } // Прогресс if(prog.live){ const pct = Math.min(100, Math.round((ts - prog.start)/(prog.stop - prog.start)*100)); top.innerHTML += `<div class="progress-wrap"> <div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div> <div class="progress-labels"><span>${st}</span><span>${pct}%</span><span>${sp}</span></div> </div>`; } } body.append(top); // Следующие передачи if(next.length){ const nl = mk("div","next-list"); nl.innerHTML = `<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin-bottom:6px">Далее</div>`; next.forEach(n=>{ const dur = Math.round((n.stop-n.start)/60); nl.innerHTML += `<div class="next-item"> <span class="next-time">${fmtTime(n.start)}</span> <span class="next-title">${esc(n.title)}</span> <span class="next-dur">${dur} мин</span> </div>`; }); body.append(nl); } // Сайд-кнопки (TikTok стиль) const side = mk("div","side-actions"); side.innerHTML = ` <div class="side-btn" onclick="shareChannel('${ch.id}')"> <div class="icon">↗️</div><span>Поделиться</span> </div> <div class="side-btn" onclick="showEmbed('${ch.id}')"> <div class="icon">🔗</div><span>Встроить</span> </div> ${stream ? `<div class="side-btn" onclick="copyStream('${stream}')"> <div class="icon">📋</div><span>Стрим</span> </div>` : ""} <div class="side-btn" onclick="openAll()"> <div class="icon">☰</div><span>Все</span> </div> `; card.append(side); card.append(body); return card; } // ─── Открытие плеера ───────────────────────────────────────── function openPlayer(chId, url){ const ph = document.getElementById(`ph-${chId}`); if(!ph) return; // Используем PlayerJS через iframe const encoded = encodeURIComponent(JSON.stringify([{file:url,label:"HD",type:"hls"}])); ph.innerHTML = `<iframe src="player.html?stream=${encodeURIComponent(url)}&ch=${encodeURIComponent(chId)}" allowfullscreen allow="autoplay" style="width:100%;height:100%;border:none" ></iframe>`; } // ─── Навигация ──────────────────────────────────────────────── function showChannel(id){ activeChannel = id; // Обновить табы document.querySelectorAll(".tab").forEach(t=>{ t.classList.toggle("active", t.dataset.id===id || (!id && !t.dataset.id)); }); if(id){ buildFeed(id); } else { buildFeed(); } document.getElementById("feed").scrollTop = 0; } function showFeed(){ document.getElementById("allPage").classList.remove("show"); document.getElementById("app").style.display = "flex"; document.querySelectorAll(".nav-item").forEach((n,i)=>n.classList.toggle("active",i===0)); } function showAll(){ document.getElementById("allPage").classList.add("show"); document.querySelectorAll(".nav-item").forEach((n,i)=>n.classList.toggle("active",i===1)); } function openAll(){ showAll(); } // ─── Все каналы ─────────────────────────────────────────────── function buildAllGrid(){ const grid = document.getElementById("allGrid"); grid.innerHTML = ""; const groups = {}; channels.forEach(ch=>{ const g = ch.group || "Разное"; if(!groups[g]) groups[g] = []; groups[g].push(ch); }); for(const [g, chs] of Object.entries(groups)){ grid.innerHTML += `<div class="group-title" style="grid-column:1/-1">${g}</div>`; chs.forEach(ch=>{ const prog = currentProg(ch.id); const on = prog?.live; grid.innerHTML += `<div class="all-card" onclick="goChannel('${ch.id}')"> ${on?`<div class="live-tag">В ЭФИРЕ</div>`:""} <div class="ac-ch">${esc(ch.name)}</div> <div class="ac-now">${esc(prog?.title || "—")}</div> <div class="ac-time">${prog?fmtTime(prog.start)+" – "+fmtTime(prog.stop):""}</div> </div>`; }); } } function goChannel(id){ showFeed(); setTimeout(()=>showChannel(id), 50); } // ─── Поиск ─────────────────────────────────────────────────── function openSearch(){ document.getElementById("searchOverlay").classList.add("show"); document.getElementById("searchInput").focus(); } function closeSearch(){ document.getElementById("searchOverlay").classList.remove("show"); } function doSearch(){ const q = document.getElementById("searchInput").value.toLowerCase().trim(); const res = document.getElementById("searchResults"); if(!q||!EPG){ res.innerHTML=""; return; } const hits = []; const ts = nowTs(); for(const ch of channels){ const progs = EPG.schedule?.[ch.id] || []; for(const p of progs){ if(p.title.toLowerCase().includes(q)){ hits.push({ch, p}); if(hits.length>=50) break; } } } if(!hits.length){ res.innerHTML=`<div style="color:#666;padding:20px 0;text-align:center">Ничего не найдено</div>`; return; } res.innerHTML = hits.map(({ch,p})=>` <div class="search-item" onclick="goChannel('${ch.id}');closeSearch()"> <div class="si-ch">${esc(ch.name)}${p.live?` · <span style="color:var(--accent)">В ЭФИРЕ</span>`:""}</div> <div class="si-title">${esc(p.title)}</div> <div class="si-time">${fmtTime(p.start)} — ${fmtTime(p.stop)}</div> </div>`).join(""); } // ─── Утилиты ───────────────────────────────────────────────── function currentProg(chId){ const ts = nowTs(); const progs = EPG?.schedule?.[chId] || []; return progs.find(p=>p.start<=ts&&p.stop>ts) || progs[0] || null; } function fmtTime(ts){ if(!ts) return ""; const d = new Date(ts*1000); const h = (d.getUTCHours()+3)%24; const m = d.getUTCMinutes(); return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}`; } function esc(s){return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")} function mk(tag,cls,attrs){ const el=document.createElement(tag); if(cls) el.className=cls; if(attrs) for(const[k,v] of Object.entries(attrs)){ if(k==="innerHTML") el.innerHTML=v; else if(k==="onclick") el.onclick=v; else el.setAttribute(k,v); } return el; } function toast(msg){ const t=document.getElementById("toast"); t.textContent=msg; t.classList.add("show"); setTimeout(()=>t.classList.remove("show"),2200); } function copyStream(url){ navigator.clipboard.writeText(url).then(()=>toast("📋 Ссылка скопирована!")).catch(()=>toast("Не удалось скопировать")); } function shareChannel(id){ const url=location.origin+location.pathname+"?ch="+id; if(navigator.share){ navigator.share({title:"LiveПрограмма",url}).catch(()=>{}); } else { navigator.clipboard.writeText(url).then(()=>toast("🔗 Ссылка скопирована!")); } } // ─── Embed ─────────────────────────────────────────────────── let embedChId = ""; function showEmbed(chId){ embedChId = chId; const ch = EPG?.channels?.[chId]; document.getElementById("embedChName").textContent = ch?.name || chId; const appUrl = location.origin + location.pathname.replace(/[^/]*$/,""); const embedUrl= appUrl + "embed.html?ch=" + encodeURIComponent(chId) + "&app=" + encodeURIComponent(appUrl) + "&data=" + encodeURIComponent(appUrl + "data/schedule.json"); const code = `<iframe\n src="${embedUrl}"\n width="320" height="480"\n frameborder="0"\n allowfullscreen\n allow="autoplay; fullscreen"\n style="border-radius:12px;overflow:hidden"\n></iframe>`; document.getElementById("embedCode").textContent = code; document.getElementById("embedModal").style.display = "flex"; } function closeEmbed(){ document.getElementById("embedModal").style.display = "none"; } function copyEmbedCode(){ const code = document.getElementById("embedCode").textContent; navigator.clipboard.writeText(code).then(()=>toast("📋 Код скопирован!")).catch(()=>toast("Не удалось скопировать")); } function previewEmbed(){ const appUrl = location.origin + location.pathname.replace(/[^/]*$/,""); const url = appUrl + "embed.html?ch=" + encodeURIComponent(embedChId) + "&app=" + encodeURIComponent(appUrl) + "&data=" + encodeURIComponent(appUrl + "data/schedule.json"); window.open(url, "_blank", "width=360,height=540"); } // Закрытие по клику на фон document.addEventListener("click", e=>{ const modal = document.getElementById("embedModal"); if(modal && e.target === modal) closeEmbed(); }); // ─── Запуск ────────────────────────────────────────────────── boot(); // Обновляем прогресс-бары каждую минуту setInterval(()=>{ if(EPG) buildFeed(activeChannel||null); }, 60000); // Восстановление по параметру ?ch= const urlCh = new URLSearchParams(location.search).get("ch"); if(urlCh) setTimeout(()=>showChannel(urlCh),1000); </script> <!-- Embed Modal --> <div id="embedModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:60; align-items:center;justify-content:center;padding:20px"> <div style="background:#181818;border:1px solid #2a2a2a;border-radius:16px;width:100%;max-width:500px; max-height:90vh;overflow-y:auto;padding:22px"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px"> <div style="font-family:'Russo One',sans-serif;font-size:18px">🔗 Встроить канал</div> <span onclick="closeEmbed()" style="font-size:24px;cursor:pointer;color:#666;padding:4px 8px">✕</span> </div> <div style="font-size:11px;color:#666;margin-bottom:6px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Канал</div> <div id="embedChName" style="font-size:15px;font-weight:700;color:#25f4ee;margin-bottom:16px"></div> <div style="font-size:11px;color:#666;margin-bottom:8px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Код для вставки на сайт</div> <div style="background:#0d0d0d;border:1px solid #2a2a2a;border-radius:8px;padding:12px; font-size:11px;font-family:monospace;color:#aaa;word-break:break-all;line-height:1.6; margin-bottom:12px;max-height:130px;overflow-y:auto;white-space:pre-wrap" id="embedCode"></div> <div style="display:flex;gap:8px;margin-bottom:18px"> <button onclick="copyEmbedCode()" style="flex:1;background:linear-gradient(135deg,#fe2c55,#ff6b35); border:none;border-radius:10px;padding:12px;font-weight:700;font-size:13px; color:#fff;cursor:pointer;font-family:inherit">📋 Копировать код</button> <button onclick="previewEmbed()" style="background:#222;border:1px solid #333; border-radius:10px;padding:12px 16px;font-size:13px;color:#fff;cursor:pointer;font-family:inherit">👁 Превью</button> </div> <div style="background:rgba(254,44,85,.07);border:1px solid rgba(254,44,85,.2);border-radius:10px;padding:14px"> <div style="font-size:11px;font-weight:800;color:#fe2c55;margin-bottom:6px">📢 Виджет включает рекламу LiveПрограмма</div> <div style="font-size:11px;color:#777;line-height:1.7"> Внизу виджета есть кнопка <b style="color:#aaa">«Смотрите все каналы в LiveПрограмма»</b> которая ведёт на твоё приложение. Бесплатное продвижение! 🚀 </div> </div> </div> </div> </body> </html>