/* Общие функции: форматирование, карточки, расписание, меню. */ (function () { const SK = window.SK; SK.ch = id => SK.channels.find(c => c.id === id); SK.vid = id => SK.videos.find(v => v.id === id); SK.ratioClass = r => (r === "1:1" ? "r11" : r === "9:16" ? "r916" : ""); SK.esc = s => String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); SK.dur = s => { s = Math.round(s); const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), x = s % 60; return (h ? h + ":" + String(m).padStart(2, "0") : m) + ":" + String(x).padStart(2, "0"); }; SK.plural = (n, one, few, many) => { const a = Math.abs(n) % 100, b = a % 10; if (a > 10 && a < 20) return many; if (b > 1 && b < 5) return few; if (b === 1) return one; return many; }; SK.views = n => { const word = SK.plural(n, "просмотр", "просмотра", "просмотров"); if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(".", ",") + " тыс. просмотров"; return n + " " + word; }; SK.subs = n => n.toLocaleString("ru-RU") + " " + SK.plural(n, "подписчик", "подписчика", "подписчиков"); const MONTHS = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"]; SK.date = iso => { const d = new Date(iso + "T12:00:00"); const days = Math.round((Date.now() - d) / 864e5); if (days <= 0) return "сегодня"; if (days === 1) return "вчера"; if (days < 7) return days + " " + SK.plural(days, "день", "дня", "дней") + " назад"; return d.getDate() + " " + MONTHS[d.getMonth()]; }; SK.card = v => { const c = SK.ch(v.ch); return `
${SK.dur(v.dur)}${v.ratio !== "16:9" ? `${v.ratio}` : ""}

${SK.esc(v.title)}

${SK.esc(c.name)}${SK.views(v.views)}
`; }; /* ---- эфирная сетка: детерминированная, одинаковая у всех зрителей ---- */ function rng(seed) { let s = 0; for (const ch of seed) s = (s * 31 + ch.charCodeAt(0)) >>> 0; return () => { s = (s + 0x6d2b79f5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } // окна вещания по каналам, в минутах от полуночи function windows(chId, dow) { switch (chId) { case "tihaya-smena": return dow === 1 ? [[13 * 60, 1440]] : [[0, 1440]]; case "fosfor": return [[0, 5 * 60], [10 * 60, 1440]]; case "erohin": return dow === 0 ? [[10 * 60, 22 * 60]] : [[8 * 60, 12 * 60 + 30], [16 * 60, 23 * 60]]; case "nochnoy-zond": return [[0, 7 * 60], [21 * 60, 1440]]; case "kvadrat": return dow === 4 ? [[10 * 60, 1440]] : [[11 * 60, 15 * 60], [18 * 60, 23 * 60]]; } return []; } SK.dayKey = d => d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate(); SK.schedule = (chId, day) => { const r = rng(chId + SK.dayKey(day)); const titles = SK.programs[chId]; const lens = [30, 45, 60, 60, 90, 120]; const out = []; let prev = -1; for (const [a, b] of windows(chId, day.getDay())) { let t = a; while (t < b) { let len = lens[Math.floor(r() * lens.length)]; if (t + len > b) len = b - t; let k = Math.floor(r() * titles.length); if (k === prev) k = (k + 1) % titles.length; prev = k; out.push({ start: t, end: t + len, title: titles[k] }); t += len; } } return out; }; SK.hm = m => String(Math.floor(m / 60) % 24).padStart(2, "0") + ":" + String(m % 60).padStart(2, "0"); SK.nowMin = () => { const d = new Date(); return d.getHours() * 60 + d.getMinutes() + d.getSeconds() / 60; }; SK.onAir = chId => { const m = SK.nowMin(); return SK.schedule(chId, new Date()).find(p => p.start <= m && p.end > m) || null; }; /* Шкала времени. opts: { day, from, to, compact } */ SK.timeline = (el, opts) => { const day = opts.day || new Date(); const from = opts.from ?? 0, to = opts.to ?? 1440; const isToday = SK.dayKey(day) === SK.dayKey(new Date()); const hourPx = () => parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--hour")) || 168; const px = m => ((m - from) / 60) * hourPx(); function render() { const now = SK.nowMin(); let ruler = ""; for (let m = from; m <= to; m += 30) { ruler += m % 60 === 0 ? `${SK.hm(m)}` : ``; } const tracks = SK.channels.map(c => { const progs = SK.schedule(c.id, day).filter(p => p.end > from && p.start < to); let html = "", cursor = from; for (const p of progs) { if (p.start > cursor) html += clip({ start: cursor, end: p.start, title: "Нет эфира", off: true }, c); html += clip(p, c); cursor = p.end; } if (cursor < to) html += clip({ start: cursor, end: to, title: "Нет эфира", off: true }, c); return `
${html}
`; }).join(""); function clip(p, c) { const s = Math.max(p.start, from), e = Math.min(p.end, to); const state = p.off ? "off" : !isToday ? "" : p.end <= now ? "past" : p.start <= now ? "now" : ""; const href = p.off ? `channel.html?c=${c.id}` : state === "now" ? `live.html?c=${c.id}` : `channel.html?c=${c.id}`; const label = p.off ? "" : `${SK.esc(p.title)}${SK.hm(p.start)}–${SK.hm(p.end)}${state === "now" ? ", идёт сейчас" : ""}`; return `${label}`; } const labels = SK.channels.map(c => `${SK.esc(c.name)}`).join(""); const ph = isToday && now >= from && now <= to ? `
${SK.hm(Math.floor(now))}
` : ""; el.innerHTML = `
${opts.corner || "Каналы"}
${labels}
${ruler}
${tracks}${ph}
`; } render(); const sc = () => el.querySelector(".tl-scroll"); if (isToday) { const now = SK.nowMin(); sc().scrollLeft = Math.max(0, px(now) - sc().clientWidth * 0.35); setInterval(() => { const l = sc().scrollLeft; render(); sc().scrollLeft = l; }, 30000); } else if (opts.scrollTo != null) { sc().scrollLeft = px(opts.scrollTo); } }; SK.toast = msg => { let t = document.querySelector(".toast"); if (!t) { t = document.createElement("div"); t.className = "toast"; t.setAttribute("role", "status"); document.body.appendChild(t); } t.textContent = msg; t.classList.add("show"); clearTimeout(t._h); t._h = setTimeout(() => t.classList.remove("show"), 2600); }; SK.param = k => new URLSearchParams(location.search).get(k); /* мобильное меню */ document.addEventListener("click", e => { const b = e.target.closest(".menu-toggle"); if (!b) return; const bar = b.closest(".bar"); const open = bar.classList.toggle("open"); b.setAttribute("aria-expanded", open); }); })();