From 05d6b93faabdbea27119938bd2cf924064313897 Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:42:22 -0500 Subject: [PATCH 1/6] Update print statement from 'Hello' to 'Goodbye' --- templates/guide.html | 657 +++++++++++++++++++++++++++++++------------ 1 file changed, 477 insertions(+), 180 deletions(-) diff --git a/templates/guide.html b/templates/guide.html index d90e8b6..59e6e77 100644 --- a/templates/guide.html +++ b/templates/guide.html @@ -3,6 +3,9 @@ Live TV Guide @@ -295,9 +422,10 @@
  • Light
  • Dark
  • AOL / CompuServe
  • -
  • TV Guide Magazine
  • +
  • TV Guide (Refresh)
  • DirecTV
  • Comcast
  • +
  • TV Guide (Classic)
  • @@ -311,18 +439,23 @@ -
    +

    Program Info

    Click a channel on the left to start playback.

    - - + +
    + + + -
    -
    +
    + +
    @@ -332,7 +465,7 @@

    Program Info

    {{ t.isoformat() }}
    {% endfor %}
    -
    +
    @@ -344,7 +477,8 @@

    Program Info

    + data-name="{{ ch.name|e }}" + data-logo="{{ ch.logo }}"> {% if ch.logo %}{% endif %} {{ ch.name }}
    @@ -441,6 +575,118 @@

    Program Info

    } } +/* --- NEW: Fixed time header logic with left spacer --- + The goal: the fixed bar visually extends across the left channel area (blank), + while the actual time cells remain aligned with the program grid. + Implementation: fixedBar covers full viewport width (left:0). Inside it we create + a left spacer whose width == channel column width and paint that spacer with the + channel column background (theme-aware via CSS variables). The cloned time-header + is appended after the spacer so times start where they already do. +*/ + +function createOrUpdateFixedTimeBar(){ + const fixedBar = document.getElementById('fixedTimeBar'); + const gridTimeRow = document.getElementById('gridTimeRow'); + const guideOuter = document.getElementById('guideOuter'); + const playerRow = document.getElementById('playerRow'); + + if (!gridTimeRow || !fixedBar || !guideOuter) return; + + // Clone the server-side time header cells + const serverTimeHeader = gridTimeRow.querySelector('.time-header-wrap .grid-content .time-header'); + if (!serverTimeHeader) return; + + // clear existing + fixedBar.innerHTML = ''; + + // cloned container + const clonedGridContent = document.createElement('div'); + clonedGridContent.className = 'grid-content'; + + // left spacer so the times start where the grid content starts + const spacer = document.createElement('div'); + spacer.className = 'left-spacer'; + + // compute current channel column width + const firstChanCol = document.querySelector('.guide-row .chan-col'); + const chanColWidth = firstChanCol ? Math.round(firstChanCol.getBoundingClientRect().width) : 200; + spacer.style.width = chanColWidth + 'px'; + spacer.style.minWidth = chanColWidth + 'px'; + spacer.style.maxWidth = chanColWidth + 'px'; + + // clone the time header and append after the spacer + const clonedTimeHeader = serverTimeHeader.cloneNode(true); + clonedTimeHeader.classList.add('time-header'); + clonedTimeHeader.style.display = 'flex'; + clonedTimeHeader.style.height = '100%'; + + // append in order: spacer then header + clonedGridContent.appendChild(spacer); + clonedGridContent.appendChild(clonedTimeHeader); + + // create a now-line for the fixed header (positioned absolutely within clonedGridContent) + const nowLine = document.createElement('div'); + nowLine.id = 'nowLineFixed'; + nowLine.className = 'now-line'; + nowLine.style.position = 'absolute'; + nowLine.style.top = '0'; + nowLine.style.bottom = '0'; + nowLine.style.width = '2px'; + nowLine.style.left = '0px'; + nowLine.style.pointerEvents = 'none'; + clonedGridContent.appendChild(nowLine); + + fixedBar.appendChild(clonedGridContent); + + // fixedBar spans whole viewport left->right (so the left spacer covers the channel area) + fixedBar.style.left = '0px'; + fixedBar.style.width = window.innerWidth + 'px'; + + // Position top: place it directly below the player row + if (playerRow) { + const rect = playerRow.getBoundingClientRect(); + fixedBar.style.top = rect.bottom + 'px'; + } else { + const header = document.querySelector('.header'); + const headerRect = header ? header.getBoundingClientRect() : { bottom: 40 }; + fixedBar.style.top = headerRect.bottom + 'px'; + } + + // ensure the guide content is pushed down by the fixed bar height + requestAnimationFrame(() => { + const fbHeight = fixedBar.getBoundingClientRect().height || 34; + const currentPaddingTop = parseFloat(window.getComputedStyle(guideOuter).paddingTop) || 0; + if (currentPaddingTop < fbHeight) { + guideOuter.style.paddingTop = fbHeight + 'px'; + } + }); + + // hide the original in-grid now-line (we use the fixed one) + const origNow = document.getElementById('nowLineOriginal'); + if (origNow) origNow.style.display = 'none'; +} + +// update fixed now-line and also original now-line left (if original present) +function updateNowLine(){ + const gridStart = new Date("{{ grid_start.isoformat() }}"); + const scale = {{ SCALE }}; + const now = new Date(); + const minutesFromStart = (now - gridStart) / 60000; + const leftPx = (minutesFromStart * scale); + + // compute channel column width again (ensure sync if responsive) + const firstChanCol = document.querySelector('.guide-row .chan-col'); + const chanColWidth = firstChanCol ? firstChanCol.getBoundingClientRect().width : 0; + + // Fixed now-line: because we include the left spacer inside the fixed bar, add its width + const nlFixed = document.getElementById('nowLineFixed'); + if (nlFixed) nlFixed.style.left = (leftPx + chanColWidth) + 'px'; + + // In case original now-line is being used, update it too + const nlOrig = document.getElementById('nowLineOriginal'); + if (nlOrig) nlOrig.style.left = leftPx + 'px'; +} + document.addEventListener("DOMContentLoaded", () => { const savedTheme = localStorage.getItem("theme") || "dark"; document.body.classList.add(savedTheme); @@ -462,26 +708,67 @@

    Program Info

    prog.title = `${prog.dataset.title}\n${start.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})} - ${stop.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}`; }); + // Create and position the fixed time bar immediately after DOM ready + createOrUpdateFixedTimeBar(); updateClock(); - setInterval(updateClock, 1000); updateNowLine(); + + // Keep clock & now-line updated + setInterval(updateClock, 1000); setInterval(updateNowLine, 60000); }); -function setTheme(theme) { - const body = document.body; - // Remove all possible theme classes - body.classList.remove("light", "dark", "retro-tvguide", "retro-aol", "retro-webtv", "retro-tvguide2000", "retro-magazine", "directv", "comcast"); - // Add the new one - body.classList.add(theme); - // Save it for next load - localStorage.setItem("theme", theme); +// Recompute positions on resize (so fixed bar stays aligned) +window.addEventListener('resize', () => { + createOrUpdateFixedTimeBar(); + updateNowLine(); +}); + +/* --- Theme & UI helpers --- (unchanged) */ +function setTheme(theme){ + const b=document.body; + /* start fade out */ + b.classList.add("fade-switch"); + setTimeout(()=>{ + const wasTVG=b.classList.contains("tvguide1990"); + b.classList.remove("light","dark","retro-tvguide","retro-aol","retro-webtv", + "retro-tvguide2000","retro-magazine","directv","comcast","tvguide1990"); + b.classList.add(theme); + localStorage.setItem("theme",theme); + + /* ๐Ÿงฉ Restore logos + names when leaving TV Guide 1990 */ + if(wasTVG && theme!=="tvguide1990"){ + document.querySelectorAll(".chan-col .chan-name").forEach(el=>{ + const name=el.dataset.name||"Channel"; + const logo=el.dataset.logo; + el.innerHTML=logo?`${name}`:`${name}`; + }); + document.querySelectorAll(".grid-row,.chan-col").forEach(r=>r.style.height=""); + } + + /* ๐ŸŽจ Inject number capsules for TV Guide 1990 */ + if(theme==="tvguide1990") requestAnimationFrame(applyTvGuide1990Capsules); + + /* end fade back in */ + setTimeout(()=>b.classList.remove("fade-switch"),100); + },150); } -// On load, apply saved theme or default -document.addEventListener("DOMContentLoaded", () => { - const savedTheme = localStorage.getItem("theme") || "dark"; - setTheme(savedTheme); +function applyTvGuide1990Capsules(){ + document.querySelectorAll(".chan-col .chan-name").forEach((box,i)=>{ + if(box.querySelector(".channel-number")) return; + const cap=document.createElement("span"); + cap.className="channel-number"; + cap.textContent=String(i+1); + box.innerHTML=""; + box.appendChild(cap); + }); +} + +document.addEventListener("DOMContentLoaded",()=>{ + const t=localStorage.getItem("theme")||"dark"; + setTheme(t); + if(t==="tvguide1990") requestAnimationFrame(applyTvGuide1990Capsules); }); function updateClock() { @@ -495,16 +782,28 @@

    Program Info

    } } -function updateNowLine() { - const gridStart = new Date("{{ grid_start.isoformat() }}"); - const scale = {{ SCALE }}; - const now = new Date(); - const minutesFromStart = (now - gridStart) / 60000; - const nl = document.getElementById('nowLine'); - if (nl) nl.style.left = (minutesFromStart * scale) + "px"; -} - + + @@ -522,8 +821,6 @@

    Program Info

    document.getElementById("settingsMenu")?.remove(); document.getElementById("manageUsersMenu")?.remove(); - // Remove the HOME link - const homeLink = document.querySelector('.header .links a[href*="guide"]'); if (homeLink) homeLink.remove(); // Insert simplified Themes dropdown From 61f8ea856f422b1ebd5540631d2d055a3c6e533f Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:56:21 -0500 Subject: [PATCH 2/6] Updated AOL / CompuServe --- templates/guide.html | 453 +++++++++++++++++++++++++------------------ 1 file changed, 263 insertions(+), 190 deletions(-) diff --git a/templates/guide.html b/templates/guide.html index 59e6e77..a620457 100644 --- a/templates/guide.html +++ b/templates/guide.html @@ -26,109 +26,144 @@ .dropdown:hover .dropdown-content { display:block; } /* Submenu (fly-out for Themes) */ - .submenu { - position: relative; - } - .submenu > a { - display: flex; - justify-content: space-between; - align-items: center; - } - .submenu-content { - display: none; - position: absolute; - top: 0; - left: 100%; /* fly out to the right */ - background-color: #333; - min-width: 160px; - border-radius: 3px; - box-shadow: 0 4px 6px rgba(0,0,0,0.3); - z-index: 10; - } - .submenu-content li a { - padding: 10px 14px; - display: block; - color: #eee; - text-decoration: none; - } - .submenu-content li a:hover { - background-color: #0af; - color: #fff; - } - .submenu:hover .submenu-content { - display: block; - } + .submenu { position: relative; } + .submenu > a { display: flex; justify-content: space-between; align-items: center; } + .submenu-content { display: none; position: absolute; top: 0; left: 100%; background-color: #333; min-width: 160px; border-radius: 3px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 10; } + .submenu-content li a { padding: 10px 14px; display: block; color: #eee; text-decoration: none; } + .submenu-content li a:hover { background-color: #0af; color: #fff; } + .submenu:hover .submenu-content { display: block; } /* Remove default list bullets and padding for all dropdowns & submenus */ .dropdown-content, .submenu ul, - .submenu-content { - list-style: none; - margin: 0; - padding: 0; - } + .submenu-content { list-style: none; margin: 0; padding: 0; } /* Player row: Program Info (left) | Video (right) */ .player { display:flex; gap:16px; padding:12px; } .summary { flex:1; } #video { width:620px; height:350px; } - /* Guide grid */ - .guide-outer { - height: calc(100vh - 420px); - overflow-y: auto; - padding-bottom: 80px; - position: relative; - } - .guide-row { display:flex; } - .chan-col { width: 200px; flex-shrink:0; border-right:1px solid; position: relative; z-index: 2; background: var(--chan-col-bg, #1a1a1a); } - .grid-col { position:relative; flex:1; overflow-x:auto; overflow-y:visible; z-index:1; } - .chan-header { height: 34px; border-bottom:1px solid; position:sticky; top:0; z-index:5; } - .time-header-wrap { position:sticky; top:0; z-index:6; border-bottom:1px solid; } - - .time-cell { flex:0 0 {{ 30*SCALE }}px; width:{{ 30*SCALE }}px; text-align:center; border-right:1px solid; font-weight:600; } - .chan-name { - padding: 10px; height: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; - gap: 6px; border-bottom: 1px solid; cursor: pointer; position: relative; z-index: 10; pointer-events: auto; user-select: none; - } - .chan-name img { width:36px; height:36px; object-fit:contain; margin-bottom: 4px; } - .grid-row { position:relative; height:60px; border-bottom:1px solid; } - .grid-content { position:relative; width: {{ total_width }}px; min-height:100%; } - .program { position:absolute; top:6px; height:48px; border:1px solid; padding:4px 6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; - font-size:12px; border-radius:6px; z-index:1; } /* ensure fixed header sits above programs */ - .program.now { font-weight:bold; } - .now-line { position:absolute; top:0; bottom:0; width:2px; z-index:8; pointer-events:none; left: {{ now_offset }}px; } +/* === REPLACE the existing "Guide grid" block with the code below === */ + +/* Guide grid */ +.guide-outer { + height: calc(100vh - 420px); + overflow-y: auto; + padding-bottom: 80px; + position: relative; +} + +.guide-row { display:flex; } + +.chan-col { + width: 200px; + flex-shrink:0; + border-right:1px solid; + position: relative; + z-index: 2; + background: var(--chan-col-bg, #1a1a1a); +} + +/* NOTE: grid-col now allows vertical scrolling but we hide visual scrollbars by default */ +.grid-col { + position:relative; + flex:1; + overflow-x:auto; + overflow-y:auto; /* allow vertical scrolling but hide visuals by default */ + z-index:1; + + /* Firefox: hide scrollbar */ + scrollbar-width: none; + /* IE10+ */ + -ms-overflow-style: none; +} + +/* hide webkit scrollbars by default */ +.grid-col::-webkit-scrollbar { + width: 0; + height: 0; +} + +/* keep headers same */ +.chan-header { height: 34px; border-bottom:1px solid; position:sticky; top:0; z-index:5; } +.time-header-wrap { position:sticky; top:0; z-index:6; border-bottom:1px solid; } + +.time-cell { flex:0 0 {{ 30*SCALE }}px; width:{{ 30*SCALE }}px; text-align:center; border-right:1px solid; font-weight:600; } + +.chan-name { + padding: 10px; + height: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 6px; + border-bottom: 1px solid; + cursor: pointer; + position: relative; + z-index: 10; + pointer-events: auto; + user-select: none; +} +.chan-name img { width:36px; height:36px; object-fit:contain; margin-bottom: 4px; } + +.grid-row { position:relative; height:60px; border-bottom:1px solid; } +.grid-content { position:relative; width: {{ total_width }}px; min-height:100%; } +.program { + position:absolute; + top:6px; + height:48px; + border:1px solid; + padding:4px 6px; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; + font-size:12px; + border-radius:6px; + z-index:1; +} /* ensure fixed header sits above programs */ +.program.now { font-weight:bold; } +.now-line { position:absolute; top:0; bottom:0; width:2px; z-index:8; pointer-events:none; left: {{ now_offset }}px; } + +/* === Scrollbar reveal styles === + We hide the native scrollbars by default (cross-browser) and show a thin, themed scrollbar + when .show-scroll is present (or the element has keyboard focus via :focus-within). +*/ +.grid-col.show-scroll, +.grid-col:focus-within { + /* Firefox: show thin scrollbar when revealed */ + scrollbar-width: thin; +} + +/* Webkit: styled visible scrollbar when .show-scroll is added */ +.grid-col.show-scroll::-webkit-scrollbar { + width: 10px; /* vertical scrollbar width */ + height: 8px; /* horizontal scrollbar height (if any) */ +} +.grid-col.show-scroll::-webkit-scrollbar-track { + background: rgba(0,0,0,0.08); + border-radius: 6px; +} +.grid-col.show-scroll::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.28); + border-radius: 6px; + border: 2px solid transparent; + background-clip: padding-box; +} /* Fixed time header (new) */ - .time-header-fixed { - position: fixed; - z-index: 1200; /* high so it always sits on top */ - left: 0; /* now covers entire viewport width; we add a left spacer to keep times aligned */ - display: flex; - align-items: stretch; - overflow: hidden; - background: transparent; /* spacer will paint the left channel background */ - box-shadow: 0 2px 6px rgba(0,0,0,0.12); - height: 34px; - top: 0; /* set by JS */ - } + .time-header-fixed { position: fixed; z-index: 1200; left: 0; display: flex; align-items: stretch; overflow: hidden; background: transparent; box-shadow: 0 2px 6px rgba(0,0,0,0.12); height: 34px; top: 0; /* set by JS */ } .time-header-fixed .grid-content { display:flex; align-items:stretch; position:relative; height:100%; width:100%; } .time-header-fixed .time-header { display:flex; align-items:center; height:100%; flex: 0 0 auto; } .time-header-fixed .time-cell { display:inline-flex; align-items:center; justify-content:center; height:100%; border-right:1px solid var(--timebar-border, #ccc); padding:0 6px; font-weight:600; background: var(--timebar-bg, rgba(255,255,255,0.95)); color:var(--timebar-color,#000); } .time-header-fixed .now-line { position:absolute; top:0; bottom:0; width:2px; z-index:1300; pointer-events:none; } - .time-header-fixed .left-spacer { - flex: 0 0 auto; - height:100%; - background: var(--chan-col-bg, #1a1a1a); - border-right: 1px solid var(--timebar-border, rgba(0,0,0,0.12)); - } + .time-header-fixed .left-spacer { flex: 0 0 auto; height:100%; background: var(--chan-col-bg, #1a1a1a); border-right: 1px solid var(--timebar-border, rgba(0,0,0,0.12)); } /* Keep original in-grid time header hidden when using fixed header */ .hide-in-grid .time-header-wrap { visibility: hidden; height: 0; margin:0; padding:0; } - /* Theme-specific timebar and channel background variables (so fixed bar and the left spacer match theme) */ - /* Dark theme */ body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; } body.dark .time-header-fixed .now-line { background:#0f0; } @@ -163,125 +198,137 @@ body.light .program.now { background:#cfe8cf; border-color:#090; } body.light .now-line { background:#090; } - /* Retro AOL / CompuServe Theme */ - body.retro-aol { --timebar-bg:#2f4f4f; --timebar-border:#33cccc; --timebar-color:#000; --chan-col-bg:#2f4f4f; } - body.retro-aol .time-header-fixed .now-line { background:#090; } - body.retro-aol { --timebar-bg: #2f4f4f; --timebar-border: #33cccc; --timebar-color: #000; --now-line-color: #090; --chan-col-bg: #2f4f4f; } - body.retro-aol { background:#2f4f4f; color:#f0f0f0; font-family:"Tahoma","Arial",sans-serif; } - body.retro-aol .header { background:#004466; color:#ffcc00; border-bottom:2px solid #33cccc; } - body.retro-aol #video { background:#000; } - body.retro-aol .summary { color:#e0e0e0; } - body.retro-aol .chan-col { background:#355f5f; border-color:#33cccc; } - body.retro-aol .grid-col { background:#3b6b6b; } - body.retro-aol .chan-header, - body.retro-aol .time-header-wrap { background:#004466; color:#ffcc00; border-color:#33cccc; } - body.retro-aol .time-cell { color:#ffcc00; border-color:#1a3d3d; } - body.retro-aol .chan-name { color:#ffffff; border-color:#33cccc; font-weight:bold; } - body.retro-aol .program { background:#406d6d; border-color:#33cccc; color:#ffffff; } - body.retro-aol .program.now { background:#33cccc; border-color:#004466; color:#000; font-weight:bold; } - body.retro-aol .now-line { background:#ffcc00; } + /* compacted retro-aol theme + scrollbar reveal (drop-in replacement) */ +body.retro-aol{--timebar-bg:#004466;--timebar-border:#33cccc;--timebar-color:#33cccc;--now-line-color:#090;--chan-col-bg:#004466;--panel-bg-top:#e6f7f5;--panel-bg-bottom:#cfeff0;--now-line-panel-color:var(--panel-bg-top);--now-line-glow:rgba(198,242,240,0.6);background:#2f4f4f;color:#f0f0f0;font-family:"Tahoma","Arial",sans-serif} +body.retro-aol .header{background:#004466;color:#ffcc00;border-bottom:2px solid #33cccc} +body.retro-aol .dropdown-content,body.retro-aol .submenu-content{background:#004466;border:1px solid #33cccc;color:#ffcc01;box-shadow:0 4px 6px rgba(0,0,0,.3)} +body.retro-aol .dropdown-content a,body.retro-aol .submenu-content li a,body.retro-aol .dropdown-content .submenu> a,body.retro-aol .submenu-content .submenu> a{color:#ffcc01!important} +body.retro-aol .dropdown-content a:hover,body.retro-aol .submenu-content li a:hover{background:#33cccc!important;color:#004466!important} +body.retro-aol .summary{background:linear-gradient(to bottom,var(--panel-bg-top) 0%,var(--panel-bg-bottom) 100%);color:#00313a;border:2px solid #004466;border-top-color:#66e0e0;border-left-color:#66e0e0;border-right-color:#004466;border-bottom-color:#004466;box-shadow:4px 4px 0 rgba(0,0,0,.28);padding:12px;border-radius:6px;box-sizing:border-box} +body.retro-aol .summary h3{margin:0 0 6px 0;color:#004466;font-size:1.05em;font-weight:700} +body.retro-aol .summary p{margin:0;color:#05383a;line-height:1.3} +body.retro-aol .time-header-fixed .now-line,body.retro-aol .now-line{background:var(--now-line-panel-color);box-shadow:0 0 6px var(--now-line-glow);width:3px} +body.retro-aol .chan-col{background:#355f5f;border-color:#33cccc} +body.retro-aol .grid-col{background:#3b6b6b} +body.retro-aol .chan-header,body.retro-aol .time-header-wrap{background:#3b6b6b;color:#ffcc00;border-color:#33cccc} +body.retro-aol .time-cell{color:#ffcc00;border-color:var(--timebar-border,#33cccc);border-top:1px solid var(--timebar-border,#33cccc)} +body.retro-aol .time-header-fixed .time-cell{border-top:1px solid var(--timebar-border,#33cccc)} +body.retro-aol .chan-name{color:#fff;border-color:#33cccc;font-weight:700} +body.retro-aol .program{background:#406d6d;border-color:#33cccc;color:#fff} +body.retro-aol .program.now{background:#33cccc;border-color:#004466;color:#000;font-weight:700} +body.retro-aol .now-line{background:#ffcc00} +body.retro-aol .header .links>a,body.retro-aol .header .links>.dropdown>.dropbtn,body.retro-aol .header .links>span,body.retro-aol .header .links>div#clock{color:#ffcc01} + +/* guide grid scrollbar rules (compact) */ +/*.grid-col{position:relative;flex:1;overflow-x:auto;overflow-y:auto;z-index:1;scrollbar-width:none;-ms-overflow-style:none} +.grid-col::-webkit-scrollbar{width:0;height:0} +.grid-col.show-scroll,.grid-col:focus-within{scrollbar-width:thin} +.grid-col.show-scroll::-webkit-scrollbar{width:10px;height:8px} +.grid-col.show-scroll::-webkit-scrollbar-track{background:rgba(0,0,0,.08);border-radius:6px} +.grid-col.show-scroll::-webkit-scrollbar-thumb{background:rgba(0,0,0,.28);border-radius:6px;border:2px solid transparent;background-clip:padding-box} */ +body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-thumb{background:rgba(0,68,102,.55)} +body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-track{background:rgba(51,204,204,.06)} /* === RetroIPTVGuide Theme: TV Guide 1990 Edition (Final Compact) === */ body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --chan-col-bg: #d8d7d3; } body.tvguide1990 .time-header-fixed { box-shadow:none; } body.tvguide1990 .time-header-fixed .now-line { background:#000; height:3px; } body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --now-line-color: #000; --chan-col-bg: #d8d7d3; } - body.tvguide1990{background:#d8d7d3;color:#000;font-family:'Times New Roman',serif;font-size:.9em;} - body.tvguide1990 .header{background:#fff;color:#000;border-bottom:2px solid #000;box-shadow:none;} - body.tvguide1990 .header .links>a, - body.tvguide1990 .header .links>.dropdown>.dropbtn, - body.tvguide1990 .header .links>span, - body.tvguide1990 .header .links>#clock{color:#000!important;background:#fff!important;} - body.tvguide1990 .header .links>a:hover, - body.tvguide1990 .header .links>.dropdown:hover>.dropbtn{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .dropdown-content{background:#fff!important;color:#000!important;border:1px solid #000!important;box-shadow:none!important;} - body.tvguide1990 .dropdown-content a{color:#000!important;} - body.tvguide1990 .dropdown-content a:hover{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .submenu-content{background:#fff!important;border:1px solid #000!important;box-shadow:none!important;z-index:1000;} - body.tvguide1990 .submenu-content li a{color:#000!important;} - body.tvguide1990 .submenu-content li a:hover{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .dropdown-content .submenu>a{color:#000!important;padding-right:22px!important;position:relative;} - body.tvguide1990 .dropdown-content .submenu>a::after{content:"โ–ธ";position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#000;} - body.tvguide1990 .dropdown-content .submenu:hover>a{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .footer{background:transparent;color:#000;border-top:2px solid #000;} - body.tvguide1990 .chan-name img{display:none!important;} - body.tvguide1990 .chan-name span:not(.channel-number){display:none!important;} - body.tvguide1990 .chan-col{height:38px!important;padding:2px 0!important;display:flex;align-items:center;justify-content:center;background:#d8d7d3;border-bottom:1px solid #000;} - body.tvguide1990 .guide-row>.chan-col{border-right:1px solid #000!important;} /* vertical divider */ - body.tvguide1990 .channel-number{display:inline-flex;align-items:center;justify-content:center;font-family:'Arial Narrow','Helvetica Neue',sans-serif;font-weight:700;font-size:.9em;min-width:32px;padding:3px 12px;border-radius:999px;background:#000;color:#fff;letter-spacing:-.2px;border:1px solid #000;line-height:1;transform:scale(.9);} - body.tvguide1990 .chan-header{border-bottom:none!important;} - body.tvguide1990 .guide-row{align-items:stretch!important;} - body.tvguide1990 .time-header-wrap{border-top:1px solid #000!important;border-bottom:1px solid #000!important;margin-bottom:0!important;padding-bottom:0!important;height:38px!important;box-sizing:border-box;} - body.tvguide1990 .chan-col{height:38px!important;box-sizing:border-box;} - body.tvguide1990 .grid-content,body.tvguide1990 .time-header{transform:scaleX(.9);transform-origin:left center;} - body.tvguide1990 .time-cell{border-right:1px solid #000;color:#000;font-weight:700;font-size:.8em;padding:0 2px;min-width:0;} - body.tvguide1990 .grid-row{height:38px!important;position:relative;padding:1px 0;box-sizing:border-box;border-bottom:1px solid #000;} - body.tvguide1990 .program, - body.tvguide1990 .program.now{position:absolute;top:1px!important;height:calc(100% - 2px)!important;margin:0!important;padding:2px 4px!important;border:1px solid #000;box-sizing:border-box;border-radius:0!important;line-height:1.2;font-size:.8em!important;display:flex;align-items:center;background:#fff;font-weight:400!important;} - body.tvguide1990 .program.now{background:#e7e5dd!important;font-weight:700!important;} - body.tvguide1990 .grid-col{overflow-x:auto!important;overflow-y:hidden!important;height:38px!important;box-sizing:border-box;} - body.tvguide1990 .grid-content{height:100%!important;} - body.tvguide1990 #clock::before{content:"IPTV GUIDE";display:inline-block;margin-right:10px;padding:4px 10px;background:#fff;color:#000;font-weight:900;font-size:12px;line-height:1;border-radius:50px;border:2px solid #000;box-shadow:none;letter-spacing:.3px;vertical-align:middle;} - body.tvguide1990 #summary{border:2px solid #000;background:#fff;padding:12px;margin:10px;box-shadow:2px 2px 5px rgba(0,0,0,.25);} - body.tvguide1990 #video{border:2px solid #000;background:#000;box-shadow:3px 3px 6px rgba(0,0,0,.35);} - body.tvguide1990 .chan-col{position:relative;} - body.tvguide1990 .chan-col::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:20px;height:60%;background:#000;border-right:1px solid #000;} - + body.tvguide1990{background:#d8d7d3;color:#000;font-family:'Times New Roman',serif;font-size:.9em;} + body.tvguide1990 .header{background:#fff;color:#000;border-bottom:2px solid #000;box-shadow:none;} + body.tvguide1990 .header .links>a, + body.tvguide1990 .header .links>.dropdown>.dropbtn, + body.tvguide1990 .header .links>span, + body.tvguide1990 .header .links>#clock{color:#000!important;background:#fff!important;} + body.tvguide1990 .header .links>a:hover, + body.tvguide1990 .header .links>.dropdown:hover>.dropbtn{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .dropdown-content{background:#fff!important;color:#000!important;border:1px solid #000!important;box-shadow:none!important;} + body.tvguide1990 .dropdown-content a{color:#000!important;} + body.tvguide1990 .dropdown-content a:hover{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .submenu-content{background:#fff!important;border:1px solid #000!important;box-shadow:none!important;z-index:1000;} + body.tvguide1990 .submenu-content li a{color:#000!important;} + body.tvguide1990 .submenu-content li a:hover{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .dropdown-content .submenu>a{color:#000!important;padding-right:22px!important;position:relative;} + body.tvguide1990 .dropdown-content .submenu>a::after{content:"โ–ธ";position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#000;} + body.tvguide1990 .dropdown-content .submenu:hover>a{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .footer{background:transparent;color:#000;border-top:2px solid #000;} + body.tvguide1990 .chan-name img{display:none!important;} + body.tvguide1990 .chan-name span:not(.channel-number){display:none!important;} + body.tvguide1990 .chan-col{height:38px!important;padding:2px 0!important;display:flex;align-items:center;justify-content:center;background:#d8d7d3;border-bottom:1px solid #000;} + body.tvguide1990 .guide-row>.chan-col{border-right:1px solid #000!important;} /* vertical divider */ + body.tvguide1990 .channel-number{display:inline-flex;align-items:center;justify-content:center;font-family:'Arial Narrow','Helvetica Neue',sans-serif;font-weight:700;font-size:.9em;min-width:32px;padding:3px 12px;border-radius:999px;background:#000;color:#fff;letter-spacing:-.2px;border:1px solid #000;line-height:1;transform:scale(.9);} + body.tvguide1990 .chan-header{border-bottom:none!important;} + body.tvguide1990 .guide-row{align-items:stretch!important;} + body.tvguide1990 .time-header-wrap{border-top:1px solid #000!important;border-bottom:1px solid #000!important;margin-bottom:0!important;padding-bottom:0!important;height:38px!important;box-sizing:border-box;} + body.tvguide1990 .chan-col{height:38px!important;box-sizing:border-box;} + body.tvguide1990 .grid-content,body.tvguide1990 .time-header{transform:scaleX(.9);transform-origin:left center;} + body.tvguide1990 .time-cell{border-right:1px solid #000;color:#000;font-weight:700;font-size:.8em;padding:0 2px;min-width:0;} + body.tvguide1990 .grid-row{height:38px!important;position:relative;padding:1px 0;box-sizing:border-box;border-bottom:1px solid #000;} + body.tvguide1990 .program, + body.tvguide1990 .program.now{position:absolute;top:1px!important;height:calc(100% - 2px)!important;margin:0!important;padding:2px 4px!important;border:1px solid #000;box-sizing:border-box;border-radius:0!important;line-height:1.2;font-size:.8em!important;display:flex;align-items:center;background:#fff;font-weight:400!important;} + body.tvguide1990 .program.now{background:#e7e5dd!important;font-weight:700!important;} + body.tvguide1990 .grid-col{overflow-x:auto!important;overflow-y:hidden!important;height:38px!important;box-sizing:border-box;} + body.tvguide1990 .grid-content{height:100%!important;} + body.tvguide1990 #clock::before{content:"IPTV GUIDE";display:inline-block;margin-right:10px;padding:4px 10px;background:#fff;color:#000;font-weight:900;font-size:12px;line-height:1;border-radius:50px;border:2px solid #000;box-shadow:none;letter-spacing:.3px;vertical-align:middle;} + body.tvguide1990 #summary{border:2px solid #000;background:#fff;padding:12px;margin:10px;box-shadow:2px 2px 5px rgba(0,0,0,.25);} + body.tvguide1990 #video{border:2px solid #000;background:#000;box-shadow:3px 3px 6px rgba(0,0,0,.35);} + body.tvguide1990 .chan-col{position:relative;} + body.tvguide1990 .chan-col::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:20px;height:60%;background:#000;border-right:1px solid #000;} /* DirecTV */ body.directv { --timebar-bg:#002b80; --timebar-border:#001f66; --timebar-color:#d8ebff; --chan-col-bg:#001b50; } body.directv .time-header-fixed .now-line { background:#ffcc00; } body.directv { --timebar-bg: #002b80; --timebar-border: #001f66; --timebar-color: #d8ebff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } - body.directv { background:#001a66; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } - body.directv .header { background:linear-gradient(to bottom,#1d72ff,#003a8c); border-bottom:2px solid #001f66; } - body.directv .header .links > a, body.directv .header .links > .dropdown > .dropbtn, body.directv .header .links > span, body.directv .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } - body.directv .header .links > a:hover, body.directv .header .links > .dropdown:hover > .dropbtn { background:#003f9e !important; color:#ffd802 !important; } - body.directv .dropdown-content, body.directv .submenu-content { background:#003a8c !important; border:1px solid #0070ff !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } - body.directv .dropdown-content a, body.directv .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } - body.directv .dropdown-content a:hover, body.directv .submenu-content li a:hover { background:#0070ff !important; color:#fff !important; } - body.directv .dropdown-content .submenu > a { color:#fff !important; padding-right:28px !important; position:relative; } - body.directv .dropdown-content .submenu > a::after { content:"โ–ธ"; position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#fff; } - body.directv .dropdown-content .submenu:hover > a { background:#0070ff !important; color:#fff !important; } - body.directv .summary, body.directv #program-info { background:linear-gradient(to bottom,#66b2ff,#003f9e); color:#fff; border:1px solid #004bdb; } - body.directv .time-header-wrap { background:#002b80; color:#d8ebff; border-bottom:2px solid #001f66; } - body.directv .time-cell { background:#002b80; color:#b9dcff; border-color:#001f66; font-weight:bold; } - body.directv .chan-col { background:#001f66; border-right:1px solid #004bdb; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } - body.directv .chan-header { background:linear-gradient(to bottom,#0b3b8c,#3c8dff); color:#fff; border-color:#003b91; } - body.directv .grid-col { background:#001f66; } - body.directv .program { background:#003a8c; border:1px solid #004bdb; color:#fff; border-radius:2px; } - body.directv .program.now { background:#ffd802; border:1px solid #caa600; color:#000; font-weight:bold; box-shadow:none; } - body.directv .program:hover { background:linear-gradient(to right,#1d67d9,#7fbfff); color:#fff; } - body.directv .now-line { background:#ffcc00; } - body.directv .footer { background:linear-gradient(to top,#001f66,#004bdb); color:#fff; border-top:2px solid #003580; padding:5px 10px; font-size:0.9em; } - body.directv .footer .dot-red { color:#ff3c3c; } body.directv .footer .dot-green { color:#00cc00; } body.directv .footer .dot-yellow { color:#ffd700; } body.directv .footer .dot-blue { color:#4ca9ff; } - body.directv #video { background:#000; border:2px solid #004bdb; box-shadow:0 0 6px rgba(0,0,0,0.6); } - body.directv #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } + body.directv { background:#001a66; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } + body.directv .header { background:linear-gradient(to bottom,#1d72ff,#003a8c); border-bottom:2px solid #001f66; } + body.directv .header .links > a, body.directv .header .links > .dropdown > .dropbtn, body.directv .header .links > span, body.directv .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } + body.directv .header .links > a:hover, body.directv .header .links > .dropdown:hover > .dropbtn { background:#003f9e !important; color:#ffd802 !important; } + body.directv .dropdown-content, body.directv .submenu-content { background:#003a8c !important; border:1px solid #0070ff !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } + body.directv .dropdown-content a, body.directv .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } + body.directv .dropdown-content a:hover, body.directv .submenu-content li a:hover { background:#0070ff !important; color:#fff !important; } + body.directv .dropdown-content .submenu > a { color:#fff !important; padding-right:28px !important; position:relative; } + body.directv .dropdown-content .submenu > a::after { content:"โ–ธ"; position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#fff; } + body.directv .dropdown-content .submenu:hover > a { background:#0070ff !important; color:#fff !important; } + body.directv .summary, body.directv #program-info { background:linear-gradient(to bottom,#66b2ff,#003f9e); color:#fff; border:1px solid #004bdb; } + body.directv .time-header-wrap { background:#002b80; color:#d8ebff; border-bottom:2px solid #001f66; } + body.directv .time-cell { background:#002b80; color:#b9dcff; border-color:#001f66; font-weight:bold; } + body.directv .chan-col { background:#001f66; border-right:1px solid #004bdb; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } + body.directv .chan-header { background:linear-gradient(to bottom,#0b3b8c,#3c8dff); color:#fff; border-color:#003b91; } + body.directv .grid-col { background:#001f66; } + body.directv .program { background:#003a8c; border:1px solid #004bdb; color:#fff; border-radius:2px; } + body.directv .program.now { background:#ffd802; border:1px solid #caa600; color:#000; font-weight:bold; box-shadow:none; } + body.directv .program:hover { background:linear-gradient(to right,#1d67d9,#7fbfff); color:#fff; } + body.directv .now-line { background:#ffcc00; } + body.directv .footer { background:linear-gradient(to top,#001f66,#004bdb); color:#fff; border-top:2px solid #003580; padding:5px 10px; font-size:0.9em; } + body.directv .footer .dot-red { color:#ff3c3c; } body.directv .footer .dot-green { color:#00cc00; } body.directv .footer .dot-yellow { color:#ffd700; } body.directv .footer .dot-blue { color:#4ca9ff; } + body.directv #video { background:#000; border:2px solid #004bdb; box-shadow:0 0 6px rgba(0,0,0,0.6); } + body.directv #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } /* Comcast */ body.comcast { --timebar-bg:linear-gradient(to bottom,#0055cc,#001b50); --timebar-border:#003090; --timebar-color:#fff; --chan-col-bg:#001b50; } body.comcast .time-header-fixed .now-line { background:#ffcc00; } body.comcast { --timebar-bg: linear-gradient(to bottom,#0055cc,#001b50); --timebar-border: #003090; --timebar-color: #fff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } body.comcast { background:#001b50; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } - body.comcast .header { background:linear-gradient(to bottom,#0055cc,#001b50); border-bottom:2px solid #003090; } - body.comcast .header .links > a, body.comcast .header .links > .dropdown > .dropbtn, body.comcast .header .links > span, body.comcast .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } - body.comcast .header .links > a:hover, body.comcast .header .links > .dropdown:hover > .dropbtn { background:#003890 !important; color:#ffcc00 !important; } - body.comcast .dropdown-content, body.comcast .submenu-content { background:#002a70 !important; border:1px solid #0044cc !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } - body.comcast .dropdown-content a, body.comcast .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } - body.comcast .dropdown-content a:hover, body.comcast .submenu-content li a:hover { background:#0044cc !important; color:#fff !important; } - body.comcast .summary, body.comcast #program-info { background:linear-gradient(to bottom,#001b50,#003890); color:#fff; border:1px solid #003090; } - body.comcast .time-header-wrap { background:#003890; color:#bcd8ff; border-bottom:2px solid #002b80; } - body.comcast .time-cell { background:#003890; color:#ffffff; border-color:#002b80; font-weight:bold; } - body.comcast .chan-col { background:#001b50; border-right:1px solid #0044cc; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } - body.comcast .chan-header { background:#002a70; color:#fff; border-color:#0044cc; } - body.comcast .grid-col { background:#002a70; } - body.comcast .program { background:#003890; border:1px solid #0044cc; color:#fff; border-radius:2px; } - body.comcast .program.now { background:#ffffff; border:1px solid #cccccc; color:#000; font-weight:bold; } - body.comcast .program:hover { background:#0044cc; color:#fff; } - body.comcast .now-line { background:#ffcc00; } - body.comcast #video { background:#000; border:2px solid #0044cc; box-shadow:0 0 8px rgba(0,0,0,.6); } - body.comcast .footer { background:linear-gradient(to top,#001b50,#0044cc); color:#fff; border-top:2px solid #002b80; padding:5px 10px; font-size:0.9em; } - body.comcast .footer .dot-red { color:#ff3c3c; } body.comcast .footer .dot-green { color:#00cc00; } body.comcast .footer .dot-yellow { color:#ffd700; } body.comcast .footer .dot-blue { color:#4ca9ff; } - body.comcast #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } + body.comcast .header { background:linear-gradient(to bottom,#0055cc,#001b50); border-bottom:2px solid #003090; } + body.comcast .header .links > a, body.comcast .header .links > .dropdown > .dropbtn, body.comcast .header .links > span, body.comcast .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } + body.comcast .header .links > a:hover, body.comcast .header .links > .dropdown:hover > .dropbtn { background:#003890 !important; color:#ffcc00 !important; } + body.comcast .dropdown-content, body.comcast .submenu-content { background:#002a70 !important; border:1px solid #0044cc !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } + body.comcast .dropdown-content a, body.comcast .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } + body.comcast .dropdown-content a:hover, body.comcast .submenu-content li a:hover { background:#0044cc !important; color:#fff !important; } + body.comcast .summary, body.comcast #program-info { background:linear-gradient(to bottom,#001b50,#003890); color:#fff; border:1px solid #003090; } + body.comcast .time-header-wrap { background:#003890; color:#bcd8ff; border-bottom:2px solid #002b80; } + body.comcast .time-cell { background:#003890; color:#ffffff; border-color:#002b80; font-weight:bold; } + body.comcast .chan-col { background:#001b50; border-right:1px solid #0044cc; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } + body.comcast .chan-header { background:#002a70; color:#fff; border-color:#0044cc; } + body.comcast .grid-col { background:#002a70; } + body.comcast .program { background:#003890; border:1px solid #0044cc; color:#fff; border-radius:2px; } + body.comcast .program.now { background:#ffffff; border:1px solid #cccccc; color:#000; font-weight:bold; } + body.comcast .program:hover { background:#0044cc; color:#fff; } + body.comcast .now-line { background:#ffcc00; } + body.comcast #video { background:#000; border:2px solid #0044cc; box-shadow:0 0 8px rgba(0,0,0,.6); } + body.comcast .footer { background:linear-gradient(to top,#001b50,#0044cc); color:#fff; border-top:2px solid #002b80; padding:5px 10px; font-size:0.9em; } + body.comcast .footer .dot-red { color:#ff3c3c; } body.comcast .footer .dot-green { color:#00cc00; } body.comcast .footer .dot-yellow { color:#ffd700; } body.comcast .footer .dot-blue { color:#4ca9ff; } + body.comcast #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } /* Retro TV Guide Magazine Theme */ body.retro-magazine { background:#ffffff; color:#000000; font-family:"Times New Roman","Georgia",serif; } @@ -309,7 +356,6 @@ body.retro-magazine #current-tuner { color:#000000 !important; font-weight:bold; } /* Add other theme variables as needed */ - body{transition:background-color .3s ease,color .3s ease;} body.fade-switch{opacity:0;transition:opacity .25s ease;} @@ -328,9 +374,7 @@ /* If you want submenu children to stack above parent dropdowns too */ .dropdown-content .submenu, -.submenu-content .submenu { - z-index: 2200; -} +.submenu-content .submenu { z-index: 2200; } /* If you need to keep the time bar behind menus explicitly, ensure it's lower than header */ .time-header-fixed { @@ -430,10 +474,8 @@
    - LOGOUT
    -
    --:-- --
    @@ -804,6 +846,37 @@

    Program Info

    }); } }); + +// Show scrollbars on hover after a short delay, hide on leave. +// Also reveal for keyboard focus (accessibility) via focus-within. +(function () { + const HOVER_DELAY = 400; // milliseconds + + document.querySelectorAll('.grid-col').forEach(el => { + let hoverTimer = null; + + el.addEventListener('mouseenter', () => { + hoverTimer = setTimeout(() => el.classList.add('show-scroll'), HOVER_DELAY); + }); + + el.addEventListener('mouseleave', () => { + clearTimeout(hoverTimer); + hoverTimer = null; + el.classList.remove('show-scroll'); + }); + + // For touch devices: briefly show the scrollbar when touched + el.addEventListener('touchstart', () => { + el.classList.add('show-scroll'); + setTimeout(() => el.classList.remove('show-scroll'), 1400); + }); + + // If content receives focus (keyboard), keep scrollbar visible for navigation + el.addEventListener('focusin', () => el.classList.add('show-scroll')); + el.addEventListener('focusout', () => el.classList.remove('show-scroll')); + }); +})(); + From d2330f846ae4eb06f8a1a3a9a57175198a1f0e29 Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:23:22 -0500 Subject: [PATCH 3/6] Auto Scroll Feature and CSS consolidation --- static/css/base.css | 343 +++++++++++++++++++ static/js/auto-scroll.js | 185 ++++++++++ templates/guide.html | 719 ++++++++------------------------------- 3 files changed, 679 insertions(+), 568 deletions(-) create mode 100644 static/css/base.css create mode 100644 static/js/auto-scroll.js diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..afedbcc --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,343 @@ +/* Make the page itself non-scrollable so only the guide area scrolls */ +html, body { height: 100%; overflow: hidden; } + +/* Header + menus */ +body { font-family: Arial, sans-serif; margin:0; } +.header { background:#222; height:40px; display:flex; justify-content:space-between; align-items:center; padding:0 10px; } +.header .links { display:flex; align-items:center; height:100%; gap:10px; } +.header .links > a, .header .links > .dropdown > .dropbtn, .header .links > span, .header .links > div#clock { display:flex; align-items:center; justify-content:center; padding:0 12px; color:#eee; font-weight:bold; text-decoration:none; height:40px; line-height:40px; } +.header .links > a:hover, .header .links > .dropdown:hover > .dropbtn { background:#111; color:#0af; } +.dropdown { position:relative; } +.dropbtn { background:none; border:none; font:inherit; color:inherit; cursor:pointer; } +.dropdown-content { display:none; position:absolute; top:100%; left:0; background:#333; min-width:180px; border-radius:3px; box-shadow:0 4px 6px rgba(0,0,0,0.3); } +.dropdown-content a { padding:10px 14px; display:block; color:#eee; text-decoration:none; } +.dropdown-content a:hover { background:#0af; color:#fff; } +.dropdown:hover .dropdown-content { display:block; } + +/* Submenu (fly-out for Themes) */ +.submenu { position: relative; } +.submenu > a { display: flex; justify-content: space-between; align-items: center; } +.submenu-content { display: none; position: absolute; top: 0; left: 100%; background-color: #333; min-width: 160px; border-radius: 3px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 10; } +.submenu-content li a { padding: 10px 14px; display: block; color: #eee; text-decoration: none; } +.submenu-content li a:hover { background-color: #0af; color: #fff; } +.submenu:hover .submenu-content { display: block; } + +/* Remove default list bullets and padding for all dropdowns & submenus */ +.dropdown-content, +.submenu ul, +.submenu-content { list-style: none; margin: 0; padding: 0; } + +/* Player row: Program Info (left) | Video (right) */ +.player { display:flex; gap:16px; padding:12px; } +.summary { flex:1; } +#video { width:620px; height:350px; } + +/* === REPLACE the existing "Guide grid" block with the code below === */ + +/* Guide grid */ +.guide-outer { height: calc(100vh - 420px); overflow-y: auto; padding-bottom: 80px; position: relative; } +.guide-row { display:flex; } +.chan-col { width: 200px; flex-shrink:0; border-right:1px solid; position: relative; z-index: 2; background: var(--chan-col-bg, #1a1a1a); } + +/* NOTE: grid-col now allows vertical scrolling but we hide visual scrollbars by default */ +.grid-col { position:relative; flex:1; overflow-x:auto; overflow-y:auto; z-index:1; +/* Firefox: hide scrollbar */ scrollbar-width: none; +/* IE10+ */ -ms-overflow-style: none; } + +/* hide webkit scrollbars by default */ +.grid-col::-webkit-scrollbar { width: 0; height: 0; } + +/* keep headers same */ +.chan-header { height: 34px; border-bottom:1px solid; position:sticky; top:0; z-index:5; } +.time-header-wrap { position:sticky; top:0; z-index:6; border-bottom:1px solid; } + +/* Use CSS variable for per-cell width; keep both flex-basis and width in sync */ +.time-cell { flex: 0 0 var(--timecell-width); width: var(--timecell-width); text-align:center; border-right:1px solid; font-weight:600; } + +.chan-name { padding: 10px; height: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 6px; border-bottom: 1px solid; cursor: pointer; position: relative; z-index: 10; pointer-events: auto; user-select: none; } +.chan-name img { width:36px; height:36px; object-fit:contain; margin-bottom: 4px; } +.grid-row { position:relative; height:60px; border-bottom:1px solid; } +.grid-content { position:relative; width: var(--total-width); min-height:100%; } +.program { position:absolute; top:6px; height:48px; border:1px solid; padding:4px 6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; border-radius:6px; z-index:1; } /* ensure fixed header sits above programs */ +.program.now { font-weight:bold; } +.now-line { position:absolute; top:0; bottom:0; width:2px; z-index:8; pointer-events:none; left: var(--now-offset); } + +/* === Scrollbar reveal styles === + We hide the native scrollbars by default (cross-browser) and show a thin, themed scrollbar + when .show-scroll is present (or the element has keyboard focus via :focus-within). +*/ +.grid-col.show-scroll, +.grid-col:focus-within { + /* Firefox: show thin scrollbar when revealed */ + scrollbar-width: thin; +} + +/* Webkit: styled visible scrollbar when .show-scroll is added */ +.grid-col.show-scroll::-webkit-scrollbar { width: 10px; /* vertical scrollbar width */ height: 8px; /* horizontal scrollbar height (if any) */ } +.grid-col.show-scroll::-webkit-scrollbar-track { background: rgba(0,0,0,0.08); border-radius: 6px; } +.grid-col.show-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.28); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; } + +/* Apply the variable to the fixed-left spacer (safe to re-declare; will override earlier rules) */ +.time-header-fixed .left-spacer { width: var(--chan-col-width, 200px); min-width: var(--chan-col-width, 200px); max-width: var(--chan-col-width, 200px); height: var(--chan-col-height, auto); /* optional: aligns heights if you changed the fixed header height */ } + +/* Fixed time header (new) */ +.time-header-fixed { position: fixed; z-index: 1200; left: 0; display: flex; align-items: stretch; overflow: hidden; background: transparent; box-shadow: 0 2px 6px rgba(0,0,0,0.12); height: 34px; top: 0; /* set by JS */ } +.time-header-fixed .grid-content { display:flex; align-items:stretch; position:relative; height:100%; width:100%; } +.time-header-fixed .time-header { display:flex; align-items:center; height:100%; flex: 0 0 auto; } +.time-header-fixed .time-cell { display:inline-flex; align-items:center; justify-content:center; height:100%; border-right:1px solid var(--timebar-border, #ccc); padding:0 6px; font-weight:600; background: var(--timebar-bg, rgba(255,255,255,0.95)); color:var(--timebar-color,#000); } +.time-header-fixed .now-line { position:absolute; top:0; bottom:0; width:2px; z-index:1300; pointer-events:none; } +.time-header-fixed .left-spacer { flex: 0 0 auto; height:100%; background: var(--chan-col-bg, #1a1a1a); border-right: 1px solid var(--timebar-border, rgba(0,0,0,0.12)); } +/* Keep original in-grid time header hidden when using fixed header */ +.hide-in-grid .time-header-wrap { visibility: hidden; height: 0; margin:0; padding:0; } + + /* Dark theme */ + body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; } + body.dark .time-header-fixed .now-line { background:#0f0; } + body.dark {--timebar-bg: #222; --timebar-border: #444; --timebar-color: #ddd; --now-line-color: #0f0; --chan-col-bg: #1a1a1a; } + body.dark { background:#111; color:#ddd; } + body.dark .header { background:#222; } + body.dark #video { background:#000; } + body.dark .summary { color:#ccc; } + body.dark .chan-col { background:#1a1a1a; border-color:#333; } + body.dark .grid-col { background:#181818; } + body.dark .chan-header, body.dark .time-header-wrap { background:#222; border-color:#444; } + body.dark .time-cell { color:#bbb; border-color:#333; } + body.dark .chan-name { color:#fff; border-color:#333; } + body.dark .program { background:#3a3a3a; border-color:#555; color:#eee; } + body.dark .program.now { background:#2d5030; border-color:#47a447; } + body.dark .now-line { background:#0f0; } + + /* Light theme */ + body.light { --timebar-bg:#fff; --timebar-border:#ddd; --timebar-color:#000; --chan-col-bg:#f9f9f9; background:#fff; color:#000; } + body.light .time-header-fixed .now-line { background:#090; } + body.light { --timebar-bg: #fff; --timebar-border: #ddd; --timebar-color: #000; --now-line-color: #090; --chan-col-bg: #f9f9f9; } + body.light { background:#fff; color:#000; } + body.light .header { background:#222; } + body.light #video { background:#fff; } + body.light .summary { color:#000; } + body.light .chan-col { background:#f9f9f9; border-color:#ccc; } + body.light .grid-col { background:#fafafa; } + body.light .chan-header, body.light .time-header-wrap { background:#ddd; border-color:#bbb; } + body.light .time-cell { color:#000; border-color:#ccc; } + body.light .chan-name { color:#000; border-color:#ccc; } + body.light .program { background:#ddd; border-color:#aaa; color:#000; } + body.light .program.now { background:#cfe8cf; border-color:#090; } + body.light .now-line { background:#090; } + +/* Retro-AOL */ +body.retro-aol{--timebar-bg:#004466;--timebar-border:#33cccc;--timebar-color:#33cccc;--now-line-color:#090;--chan-col-bg:#004466;--panel-bg-top:#e6f7f5;--panel-bg-bottom:#cfeff0;--now-line-panel-color:var(--panel-bg-top);--now-line-glow:rgba(198,242,240,0.6);background:#2f4f4f;color:#f0f0f0;font-family:"Tahoma","Arial",sans-serif} +body.retro-aol .header{background:#004466;color:#ffcc00;border-bottom:2px solid #33cccc} +body.retro-aol .dropdown-content,body.retro-aol .submenu-content{background:#004466;border:1px solid #33cccc;color:#ffcc01;box-shadow:0 4px 6px rgba(0,0,0,.3)} +body.retro-aol .dropdown-content a,body.retro-aol .submenu-content li a,body.retro-aol .dropdown-content .submenu> a,body.retro-aol .submenu-content .submenu> a{color:#ffcc01!important} +body.retro-aol .dropdown-content a:hover,body.retro-aol .submenu-content li a:hover{background:#33cccc!important;color:#004466!important} +body.retro-aol .summary{background:linear-gradient(to bottom,var(--panel-bg-top) 0%,var(--panel-bg-bottom) 100%);color:#00313a;border:2px solid #004466;border-top-color:#66e0e0;border-left-color:#66e0e0;border-right-color:#004466;border-bottom-color:#004466;box-shadow:4px 4px 0 rgba(0,0,0,.28);padding:12px;border-radius:6px;box-sizing:border-box} +body.retro-aol .summary h3{margin:0 0 6px 0;color:#004466;font-size:1.05em;font-weight:700} +body.retro-aol .summary p{margin:0;color:#05383a;line-height:1.3} +body.retro-aol .time-header-fixed .now-line,body.retro-aol .now-line{background:var(--now-line-panel-color);box-shadow:0 0 6px var(--now-line-glow);width:3px} +body.retro-aol .chan-col{background:#355f5f;border-color:#33cccc} +body.retro-aol .grid-col{background:#3b6b6b} +body.retro-aol .chan-header,body.retro-aol .time-header-wrap{background:#3b6b6b;color:#ffcc00;border-color:#33cccc} +body.retro-aol .time-cell{color:#ffcc00;border-color:var(--timebar-border,#33cccc);border-top:1px solid var(--timebar-border,#33cccc)} +body.retro-aol .time-header-fixed .time-cell{border-top:1px solid var(--timebar-border,#33cccc)} +body.retro-aol .chan-name{color:#fff;border-color:#33cccc;font-weight:700} +body.retro-aol .program{background:#406d6d;border-color:#33cccc;color:#fff} +body.retro-aol .program.now{background:#33cccc;border-color:#004466;color:#000;font-weight:700} +body.retro-aol .now-line{background:#ffcc00} +body.retro-aol .header .links>a,body.retro-aol .header .links>.dropdown>.dropbtn,body.retro-aol .header .links>span,body.retro-aol .header .links>div#clock{color:#ffcc01} +body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-thumb{background:rgba(0,68,102,.55)} +body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-track{background:rgba(51,204,204,.06)} + + /* === RetroIPTVGuide Theme: TV Guide 1990 Edition (Final Compact) === */ + body.tvguide1990{--chan-col-width:200px;--chan-col-height:38px} + body.tvguide1990 .time-header-fixed .left-spacer{width:var(--chan-col-width,200px)!important;min-width:var(--chan-col-width,200px)!important;max-width:var(--chan-col-width,200px)!important;height:var(--chan-col-height,auto)!important;flex:0 0 var(--chan-col-width,200px)!important} + body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --chan-col-bg: #d8d7d3; } + body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --now-line-color: #000; --chan-col-bg: #d8d7d3; } + body.tvguide1990{background:#d8d7d3;color:#000;font-family:'Times New Roman',serif;font-size:.9em;} + body.tvguide1990 .header{background:#fff;color:#000;border-bottom:2px solid #000;box-shadow:none;} + body.tvguide1990 .header .links>a, + body.tvguide1990 .header .links>.dropdown>.dropbtn, + body.tvguide1990 .header .links>span, + body.tvguide1990 .header .links>#clock{color:#000!important;background:#fff!important;} + body.tvguide1990 .header .links>a:hover, + body.tvguide1990 .header .links>.dropdown:hover>.dropbtn{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .dropdown-content{background:#fff!important;color:#000!important;border:1px solid #000!important;box-shadow:none!important;} + body.tvguide1990 .dropdown-content a{color:#000!important;} + body.tvguide1990 .dropdown-content a:hover{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .submenu-content{background:#fff!important;border:1px solid #000!important;box-shadow:none!important;z-index:1000;} + body.tvguide1990 .submenu-content li a{color:#000!important;} + body.tvguide1990 .submenu-content li a:hover{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .dropdown-content .submenu>a{color:#000!important;padding-right:22px!important;position:relative;} + body.tvguide1990 .dropdown-content .submenu>a::after{content:"โ–ธ";position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#000;} + body.tvguide1990 .dropdown-content .submenu:hover>a{background:#e0e0e0!important;color:#000!important;} + body.tvguide1990 .footer{background:transparent;color:#000;border-top:2px solid #000;} + body.tvguide1990 .chan-name img{display:none!important;} + body.tvguide1990 .chan-name span:not(.channel-number){display:none!important;} + body.tvguide1990 .chan-col{height:38px!important;padding:2px 0!important;display:flex;align-items:center;justify-content:center;background:#d8d7d3;border-bottom:1px solid #000;} + body.tvguide1990 .guide-row>.chan-col{border-right:1px solid #000!important;} /* vertical divider */ + body.tvguide1990 .channel-number{display:inline-flex;align-items:center;justify-content:center;font-family:'Arial Narrow','Helvetica Neue',sans-serif;font-weight:700;font-size:.9em;min-width:32px;padding:3px 12px;border-radius:999px;background:#000;color:#fff;letter-spacing:-.2px;border:1px solid #000;line-height:1;transform:scale(.9);} + body.tvguide1990 .chan-header{border-bottom:none!important;} + body.tvguide1990 .guide-row{align-items:stretch!important;} + body.tvguide1990 .time-header-wrap{border-top:1px solid #000!important;border-bottom:1px solid #000!important;margin-bottom:0!important;padding-bottom:0!important;height:38px!important;box-sizing:border-box;} + body.tvguide1990 .chan-col{height:38px!important;box-sizing:border-box;} + body.tvguide1990 .grid-content,body.tvguide1990 .time-header{transform:scaleX(.9);transform-origin:left center;} + body.tvguide1990 .time-cell{border-right:1px solid #000;color:#000;font-weight:700;font-size:.8em;padding:0 2px;min-width:0;} + body.tvguide1990 .grid-row{height:38px!important;position:relative;padding:1px 0;box-sizing:border-box;border-bottom:1px solid #000;} + body.tvguide1990 .program, + body.tvguide1990 .program.now{position:absolute;top:1px!important;height:calc(100% - 2px)!important;margin:0!important;padding:2px 4px!important;border:1px solid #000;box-sizing:border-box;border-radius:0!important;line-height:1.2;font-size:.8em!important;display:flex;align-items:center;background:#fff;font-weight:400!important;} + body.tvguide1990 .program.now{background:#e7e5dd!important;font-weight:700!important;} + body.tvguide1990 .grid-col{overflow-x:auto!important;overflow-y:hidden!important;height:38px!important;box-sizing:border-box;} + body.tvguide1990 .grid-content{height:100%!important;} + body.tvguide1990 #clock::before{content:"IPTV GUIDE";display:inline-block;margin-right:10px;padding:4px 10px;background:#fff;color:#000;font-weight:900;font-size:12px;line-height:1;border-radius:50px;border:2px solid #000;box-shadow:none;letter-spacing:.3px;vertical-align:middle;} + body.tvguide1990 #summary{border:2px solid #000;background:#fff;padding:12px;margin:10px;box-shadow:2px 2px 5px rgba(0,0,0,.25);} + body.tvguide1990 #video{border:2px solid #000;background:#000;box-shadow:3px 3px 6px rgba(0,0,0,.35);} + body.tvguide1990 .chan-col{position:relative;} + body.tvguide1990 .chan-col::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:20px;height:60%;background:#000;border-right:1px solid #000;} + body.tvguide1990 { --chan-col-width: 200px; --chan-col-height: 38px; } + + + /* DirecTV */ + body.directv { --timebar-bg:#002b80; --timebar-border:#001f66; --timebar-color:#d8ebff; --chan-col-bg:#001b50; } + body.directv .time-header-fixed .now-line { background:#ffcc00; } + body.directv { --timebar-bg: #002b80; --timebar-border: #001f66; --timebar-color: #d8ebff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } + body.directv { background:#001a66; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } + body.directv .header { background:linear-gradient(to bottom,#1d72ff,#003a8c); border-bottom:2px solid #001f66; } + body.directv .header .links > a, body.directv .header .links > .dropdown > .dropbtn, body.directv .header .links > span, body.directv .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } + body.directv .header .links > a:hover, body.directv .header .links > .dropdown:hover > .dropbtn { background:#003f9e !important; color:#ffd802 !important; } + body.directv .dropdown-content, body.directv .submenu-content { background:#003a8c !important; border:1px solid #0070ff !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } + body.directv .dropdown-content a, body.directv .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } + body.directv .dropdown-content a:hover, body.directv .submenu-content li a:hover { background:#0070ff !important; color:#fff !important; } + body.directv .dropdown-content .submenu > a { color:#fff !important; padding-right:28px !important; position:relative; } + body.directv .dropdown-content .submenu > a::after { content:"โ–ธ"; position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#fff; } + body.directv .dropdown-content .submenu:hover > a { background:#0070ff !important; color:#fff !important; } + body.directv .summary, body.directv #program-info { background:linear-gradient(to bottom,#66b2ff,#003f9e); color:#fff; border:1px solid #004bdb; } + body.directv .time-header-wrap { background:#002b80; color:#d8ebff; border-bottom:2px solid #001f66; } + body.directv .time-cell { background:#002b80; color:#b9dcff; border-color:#001f66; font-weight:bold; } + body.directv .chan-col { background:#001f66; border-right:1px solid #004bdb; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } + body.directv .chan-header { background:linear-gradient(to bottom,#0b3b8c,#3c8dff); color:#fff; border-color:#003b91; } + body.directv .grid-col { background:#001f66; } + body.directv .program { background:#003a8c; border:1px solid #004bdb; color:#fff; border-radius:2px; } + body.directv .program.now { background:#ffd802; border:1px solid #caa600; color:#000; font-weight:bold; box-shadow:none; } + body.directv .program:hover { background:linear-gradient(to right,#1d67d9,#7fbfff); color:#fff; } + body.directv .now-line { background:#ffcc00; } + body.directv .footer { background:linear-gradient(to top,#001f66,#004bdb); color:#fff; border-top:2px solid #003580; padding:5px 10px; font-size:0.9em; } + body.directv .footer .dot-red { color:#ff3c3c; } body.directv .footer .dot-green { color:#00cc00; } body.directv .footer .dot-yellow { color:#ffd700; } + body.directv .footer .dot-blue { color:#4ca9ff; } + body.directv #video { background:#000; border:2px solid #004bdb; box-shadow:0 0 6px rgba(0,0,0,0.6); } + body.directv #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } + + /* Comcast */ + body.comcast { --timebar-bg:linear-gradient(to bottom,#0055cc,#001b50); --timebar-border:#003090; --timebar-color:#fff; --chan-col-bg:#001b50; } + body.comcast .time-header-fixed .now-line { background:#ffcc00; } + body.comcast { --timebar-bg: linear-gradient(to bottom,#0055cc,#001b50); --timebar-border: #003090; --timebar-color: #fff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } + body.comcast { background:#001b50; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } + body.comcast .header { background:linear-gradient(to bottom,#0055cc,#001b50); border-bottom:2px solid #003090; } + body.comcast .header .links > a, body.comcast .header .links > .dropdown > .dropbtn, body.comcast .header .links > span, body.comcast .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } + body.comcast .header .links > a:hover, body.comcast .header .links > .dropdown:hover > .dropbtn { background:#003890 !important; color:#ffcc00 !important; } + body.comcast .dropdown-content, body.comcast .submenu-content { background:#002a70 !important; border:1px solid #0044cc !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } + body.comcast .dropdown-content a, body.comcast .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } + body.comcast .dropdown-content a:hover, body.comcast .submenu-content li a:hover { background:#0044cc !important; color:#fff !important; } + body.comcast .summary, body.comcast #program-info { background:linear-gradient(to bottom,#001b50,#003890); color:#fff; border:1px solid #003090; } + body.comcast .time-header-wrap { background:#003890; color:#bcd8ff; border-bottom:2px solid #002b80; } + body.comcast .time-cell { background:#003890; color:#ffffff; border-color:#002b80; font-weight:bold; } + body.comcast .chan-col { background:#001b50; border-right:1px solid #0044cc; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } + body.comcast .chan-header { background:#002a70; color:#fff; border-color:#0044cc; } + body.comcast .grid-col { background:#002a70; } + body.comcast .program { background:#003890; border:1px solid #0044cc; color:#fff; border-radius:2px; } + body.comcast .program.now { background:#ffffff; border:1px solid #cccccc; color:#000; font-weight:bold; } + body.comcast .program:hover { background:#0044cc; color:#fff; } + body.comcast .now-line { background:#ffcc00; } + body.comcast #video { background:#000; border:2px solid #0044cc; box-shadow:0 0 8px rgba(0,0,0,.6); } + body.comcast .footer { background:linear-gradient(to top,#001b50,#0044cc); color:#fff; border-top:2px solid #002b80; padding:5px 10px; font-size:0.9em; } + body.comcast .footer .dot-red { color:#ff3c3c; } body.comcast .footer .dot-green { color:#00cc00; } body.comcast .footer .dot-yellow { color:#ffd700; } + body.comcast .footer .dot-blue { color:#4ca9ff; } + body.comcast #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } + + /* Retro TV Guide Magazine Theme */ + body.retro-magazine{--panel-bg-top:#ffffff;--panel-bg-bottom:#f2f2f2;--now-line-panel-color:#fff;--now-line-glow:rgba(0,0,0,0.22);background:#fff;color:#000;font-family:"Times New Roman","Georgia",serif} + body.retro-magazine .summary{background:linear-gradient(to bottom,var(--panel-bg-top) 0%,var(--panel-bg-bottom) 100%);color:#000;border:2px solid #000;border-top-color:#000;border-left-color:#000;border-right-color:#000;border-bottom-color:#000;box-shadow:4px 4px 0 rgba(0,0,0,0.22);padding:12px;border-radius:6px;box-sizing:border-box} + body.retro-magazine .summary h3{margin:0 0 6px 0;color:#000;font-size:1.05em;font-weight:700} + body.retro-magazine .summary p{margin:0;color:#111;line-height:1.35} + body.retro-magazine .time-header-fixed .now-line,body.retro-magazine .now-line{background:var(--now-line-panel-color);box-shadow:0 0 6px var(--now-line-glow);width:3px} + body.retro-magazine { background:#ffffff; color:#000000; font-family:"Times New Roman","Georgia",serif; } + body.retro-magazine .header { background:#ffffff; color:#000; border-bottom:2px solid #000; font-weight:bold; } + body.retro-magazine .header .links > a, + body.retro-magazine .header .links > .dropdown > .dropbtn, + body.retro-magazine .header .links > span, + body.retro-magazine .header .links > #clock { background:#ffffff !important; color:#000000 !important; } + body.retro-magazine .header .links > a:hover, + body.retro-magazine .header .links > .dropdown:hover > .dropbtn { background:#e0e0e0 !important; color:#000000 !important; } + body.retro-magazine .dropdown-content { background:#ffffff !important; color:#000000 !important; border:1px solid #000 !important; box-shadow:none !important; } + body.retro-magazine .dropdown-content a { background:#ffffff !important; color:#000000 !important; } + body.retro-magazine .dropdown-content a:hover { background:#e0e0e0 !important; color:#000000 !important; } + body.retro-magazine #video { background:#000; } + body.retro-magazine .chan-col { background:#fff; border:1px solid #000; color:#000; font-weight:bold; } + body.retro-magazine .grid-col { background:#fff; } + body.retro-magazine .chan-header, + body.retro-magazine .time-header-wrap { background:#fff; color:#fff; border:1px solid #000; font-weight:bold; } + body.retro-magazine .time-cell { color:#000; border:1px solid #000; font-weight:bold; } + body.retro-magazine .chan-name { color:#000; border:1px solid #000; font-weight:bold; } + body.retro-magazine .program { background:#fff; border:1px solid #000; color:#000; font-size:14px; } + body.retro-magazine .program.now { background:#e0e0e0; border:2px solid #000; color:#000; font-weight:bold; } + body.retro-magazine .now-line { background:#000; height:3px; } + body.retro-magazine #current-tuner { color:#000000 !important; font-weight:bold; } + body.retro-magazine .time-header-fixed .left-spacer { background: var(--timebar-bg, #fff) !important; } + + /* Add other theme variables as needed */ + body{transition:background-color .3s ease,color .3s ease;} + body.fade-switch{opacity:0;transition:opacity .25s ease;} + +/* Insert inside @@ -447,8 +25,7 @@ HOME {% if current_user is defined and current_user.username == 'admin' %} - - MANAGE USERS + MANAGE USERS {% endif %} LOGOUT @@ -479,7 +57,6 @@
    --:-- --
    -
    @@ -520,42 +97,42 @@

    Program Info

    data-url="{{ ch.url }}" data-cid="{{ ch.tvg_id }}" data-name="{{ ch.name|e }}" - data-logo="{{ ch.logo }}"> + data-logo="{{ ch.logo }}"> {% if ch.logo %}{% endif %} {{ ch.name }}
    - {% set cid = ch.tvg_id %} - {% set channel_epg = epg[cid] if cid in epg else [] %} - {% if channel_epg|length == 0 %} -
    - No Guide Data Available -
    - {% else %} - {% for prog in channel_epg %} - {% if prog.title == 'No Guide Data Available' %} -
    - {{ prog.title }} -
    - {% elif prog.start and prog.stop %} - {% set left = ((prog.start - grid_start).total_seconds()/60) * SCALE %} - {% set calc_width = (prog.stop - prog.start).total_seconds()/60 * SCALE %} - {% set width = 24 if calc_width < 24 else calc_width %} -
    - {{ prog.title }} -
    - {% endif %} - {% endfor %} - {% endif %} + {% set cid = ch.tvg_id %} + {% set channel_epg = epg[cid] if cid in epg else [] %} + {% if channel_epg|length == 0 %} +
    + No Guide Data Available +
    + {% else %} + {% for prog in channel_epg %} + {% if prog.title == 'No Guide Data Available' %} +
    + {{ prog.title }} +
    + {% elif prog.start and prog.stop %} + {% set left = ((prog.start - grid_start).total_seconds()/60) * SCALE %} + {% set calc_width = (prog.stop - prog.start).total_seconds()/60 * SCALE %} + {% set width = 24 if calc_width < 24 else calc_width %} +
    + {{ prog.title }} +
    + {% endif %} + {% endfor %} + {% endif %}
    @@ -572,7 +149,6 @@

    Program Info

    currentChannelId = cid; updateSummary(cid, name); - // ๐Ÿ”ฅ Log playback to backend fetch("/play_channel", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, @@ -617,15 +193,7 @@

    Program Info

    } } -/* --- NEW: Fixed time header logic with left spacer --- - The goal: the fixed bar visually extends across the left channel area (blank), - while the actual time cells remain aligned with the program grid. - Implementation: fixedBar covers full viewport width (left:0). Inside it we create - a left spacer whose width == channel column width and paint that spacer with the - channel column background (theme-aware via CSS variables). The cloned time-header - is appended after the spacer so times start where they already do. -*/ - +/* Fixed time header function (kept as your implementation) */ function createOrUpdateFixedTimeBar(){ const fixedBar = document.getElementById('fixedTimeBar'); const gridTimeRow = document.getElementById('gridTimeRow'); @@ -634,39 +202,45 @@

    Program Info

    if (!gridTimeRow || !fixedBar || !guideOuter) return; - // Clone the server-side time header cells - const serverTimeHeader = gridTimeRow.querySelector('.time-header-wrap .grid-content .time-header'); + const headerWrap = gridTimeRow.querySelector('.time-header-wrap .grid-content'); + const serverTimeHeader = headerWrap ? headerWrap.querySelector('.time-header') : null; if (!serverTimeHeader) return; - // clear existing fixedBar.innerHTML = ''; - // cloned container const clonedGridContent = document.createElement('div'); clonedGridContent.className = 'grid-content'; + clonedGridContent.style.display = 'flex'; + clonedGridContent.style.alignItems = 'stretch'; + clonedGridContent.style.position = 'relative'; + clonedGridContent.style.height = '100%'; - // left spacer so the times start where the grid content starts const spacer = document.createElement('div'); spacer.className = 'left-spacer'; - // compute current channel column width - const firstChanCol = document.querySelector('.guide-row .chan-col'); - const chanColWidth = firstChanCol ? Math.round(firstChanCol.getBoundingClientRect().width) : 200; - spacer.style.width = chanColWidth + 'px'; - spacer.style.minWidth = chanColWidth + 'px'; - spacer.style.maxWidth = chanColWidth + 'px'; + const guideRect = guideOuter.getBoundingClientRect(); + const headerGridRect = headerWrap.getBoundingClientRect(); + + let spacerWidth = Math.round(headerGridRect.left - guideRect.left); + + if (!spacerWidth || spacerWidth <= 0) { + const firstChanCol = document.querySelector('.guide-row .chan-col'); + spacerWidth = firstChanCol ? Math.round(firstChanCol.getBoundingClientRect().width) : 200; + } + + spacer.style.width = spacer.style.minWidth = spacer.style.maxWidth = spacerWidth + 'px'; + spacer.style.flex = '0 0 ' + spacerWidth + 'px'; + spacer.style.height = '100%'; + spacer.style.pointerEvents = 'none'; - // clone the time header and append after the spacer const clonedTimeHeader = serverTimeHeader.cloneNode(true); clonedTimeHeader.classList.add('time-header'); clonedTimeHeader.style.display = 'flex'; clonedTimeHeader.style.height = '100%'; - // append in order: spacer then header clonedGridContent.appendChild(spacer); clonedGridContent.appendChild(clonedTimeHeader); - // create a now-line for the fixed header (positioned absolutely within clonedGridContent) const nowLine = document.createElement('div'); nowLine.id = 'nowLineFixed'; nowLine.className = 'now-line'; @@ -680,11 +254,9 @@

    Program Info

    fixedBar.appendChild(clonedGridContent); - // fixedBar spans whole viewport left->right (so the left spacer covers the channel area) fixedBar.style.left = '0px'; fixedBar.style.width = window.innerWidth + 'px'; - // Position top: place it directly below the player row if (playerRow) { const rect = playerRow.getBoundingClientRect(); fixedBar.style.top = rect.bottom + 'px'; @@ -694,7 +266,6 @@

    Program Info

    fixedBar.style.top = headerRect.bottom + 'px'; } - // ensure the guide content is pushed down by the fixed bar height requestAnimationFrame(() => { const fbHeight = fixedBar.getBoundingClientRect().height || 34; const currentPaddingTop = parseFloat(window.getComputedStyle(guideOuter).paddingTop) || 0; @@ -703,12 +274,10 @@

    Program Info

    } }); - // hide the original in-grid now-line (we use the fixed one) const origNow = document.getElementById('nowLineOriginal'); if (origNow) origNow.style.display = 'none'; } -// update fixed now-line and also original now-line left (if original present) function updateNowLine(){ const gridStart = new Date("{{ grid_start.isoformat() }}"); const scale = {{ SCALE }}; @@ -716,15 +285,12 @@

    Program Info

    const minutesFromStart = (now - gridStart) / 60000; const leftPx = (minutesFromStart * scale); - // compute channel column width again (ensure sync if responsive) const firstChanCol = document.querySelector('.guide-row .chan-col'); const chanColWidth = firstChanCol ? firstChanCol.getBoundingClientRect().width : 0; - // Fixed now-line: because we include the left spacer inside the fixed bar, add its width const nlFixed = document.getElementById('nowLineFixed'); if (nlFixed) nlFixed.style.left = (leftPx + chanColWidth) + 'px'; - // In case original now-line is being used, update it too const nlOrig = document.getElementById('nowLineOriginal'); if (nlOrig) nlOrig.style.left = leftPx + 'px'; } @@ -750,26 +316,22 @@

    Program Info

    prog.title = `${prog.dataset.title}\n${start.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})} - ${stop.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}`; }); - // Create and position the fixed time bar immediately after DOM ready createOrUpdateFixedTimeBar(); updateClock(); updateNowLine(); - // Keep clock & now-line updated setInterval(updateClock, 1000); setInterval(updateNowLine, 60000); }); -// Recompute positions on resize (so fixed bar stays aligned) window.addEventListener('resize', () => { createOrUpdateFixedTimeBar(); updateNowLine(); }); -/* --- Theme & UI helpers --- (unchanged) */ +/* --- Theme & UI helpers --- */ function setTheme(theme){ const b=document.body; - /* start fade out */ b.classList.add("fade-switch"); setTimeout(()=>{ const wasTVG=b.classList.contains("tvguide1990"); @@ -778,7 +340,6 @@

    Program Info

    b.classList.add(theme); localStorage.setItem("theme",theme); - /* ๐Ÿงฉ Restore logos + names when leaving TV Guide 1990 */ if(wasTVG && theme!=="tvguide1990"){ document.querySelectorAll(".chan-col .chan-name").forEach(el=>{ const name=el.dataset.name||"Channel"; @@ -788,23 +349,30 @@

    Program Info

    document.querySelectorAll(".grid-row,.chan-col").forEach(r=>r.style.height=""); } - /* ๐ŸŽจ Inject number capsules for TV Guide 1990 */ if(theme==="tvguide1990") requestAnimationFrame(applyTvGuide1990Capsules); - /* end fade back in */ setTimeout(()=>b.classList.remove("fade-switch"),100); },150); } +// applyTvGuide1990Capsules: only operate on original .chan-col elements (exclude auto-scroll clones) function applyTvGuide1990Capsules(){ - document.querySelectorAll(".chan-col .chan-name").forEach((box,i)=>{ - if(box.querySelector(".channel-number")) return; - const cap=document.createElement("span"); - cap.className="channel-number"; - cap.textContent=String(i+1); - box.innerHTML=""; + const originalCols = Array.from(document.querySelectorAll('.chan-col')).filter(c => !c.closest('.__auto_scroll_clone')); + originalCols.forEach((col,i)=>{ + const box = col.querySelector('.chan-name'); + if(!box) return; + if(box.querySelector('.channel-number')) return; + const cap = document.createElement('span'); + cap.className='channel-number'; + cap.textContent = String(i+1); + box.innerHTML = ''; box.appendChild(cap); + col.classList.add('tvguide1990-applied'); }); + + if (window.__autoScroll && typeof window.__autoScroll.recomputeLoops === 'function') { + window.__autoScroll.recomputeLoops(); + } } document.addEventListener("DOMContentLoaded",()=>{ @@ -823,80 +391,97 @@

    Program Info

    }); } } - - From 47af865467c210565b8ae4dc25ad9abed538aade Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:33:51 -0500 Subject: [PATCH 4/6] Updates for v4.1.0 - Auto Scrolling Guide / Theme refinement --- static/css/about.css | 67 ++++ static/css/base.css | 634 ++++++++++++++++++++------------- static/css/change_password.css | 89 +++++ static/css/change_tuner.css | 134 +++++++ static/css/logs.css | 104 ++++++ static/css/manage_users.css | 101 ++++++ static/js/auto-scroll.js | 422 ++++++++++++++-------- static/js/tuner-settings.js | 370 +++++++++++++++++++ templates/_header.html | 37 ++ templates/about.html | 302 ++-------------- templates/base.html | 276 ++++++++++++++ templates/change_password.html | 283 ++------------- templates/change_tuner.html | 306 +++------------- templates/guide.html | 124 +++---- templates/logs.html | 168 +++------ templates/manage_users.html | 198 +++------- 16 files changed, 2084 insertions(+), 1531 deletions(-) create mode 100644 static/css/about.css create mode 100644 static/css/change_password.css create mode 100644 static/css/change_tuner.css create mode 100644 static/css/logs.css create mode 100644 static/css/manage_users.css create mode 100644 static/js/tuner-settings.js create mode 100644 templates/_header.html create mode 100644 templates/base.html diff --git a/static/css/about.css b/static/css/about.css new file mode 100644 index 0000000..9a99a65 --- /dev/null +++ b/static/css/about.css @@ -0,0 +1,67 @@ +/* Per-page styles for About page (scoped) */ + +.container-about { + max-width: 820px; + margin: 40px auto 80px; + padding: 0 16px; + box-sizing: border-box; +} + +.about-box { + padding: 20px; + border-radius: 8px; + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.03); + box-shadow: 0 6px 18px rgba(0,0,0,0.12); +} + +.about-box h2 { + margin: 0 0 12px; + font-size: 1.4rem; + font-weight: 700; + color: inherit; + text-align: center; +} + +.about-box ul { + list-style: none; + padding: 0; + margin: 8px 0 0 0; + font-size: 0.98rem; +} + +.about-box li { + padding: 10px 12px; + border-bottom: 1px solid rgba(255,255,255,0.04); + display: flex; + justify-content: space-between; + gap: 12px; +} + +.about-box li:last-child { + border-bottom: none; +} + +/* Ensure labels and values wrap nicely on small screens */ +.about-box li strong { min-width: 160px; display:inline-block; color:inherit; } +.about-box li span { color: inherit; word-break: break-word; } + +/* Light theme overrides */ +body.light .about-box { + background: #fff; + border-color: rgba(0,0,0,0.06); + color: #000; + box-shadow: 0 4px 10px rgba(0,0,0,0.06); +} +body.light .about-box li { border-bottom: 1px solid rgba(0,0,0,0.06); } + +/* Retro magazine tweaks */ +body.retro-magazine .about-box { background: #fff; border: 1px solid #000; } + +/* Responsive */ +@media (max-width: 640px) { + .container-about { padding: 0 12px; margin: 28px 12px 60px; } + .about-box { padding: 14px; } + .about-box li { padding: 8px 6px; display:block; } + .about-box li strong { display:block; margin-bottom:6px; } +} diff --git a/static/css/base.css b/static/css/base.css index afedbcc..a8b27e8 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,23 +1,113 @@ +/* Base CSS - authoritative header/menu + full existing guide & theme rules + NOTE: header/menu block is authoritative; the rest of your theme data is preserved exactly. + Place this at /static/css/base.css +*/ + /* Make the page itself non-scrollable so only the guide area scrolls */ html, body { height: 100%; overflow: hidden; } -/* Header + menus */ +/* Authoritative Header + menus (single source of truth for site) */ body { font-family: Arial, sans-serif; margin:0; } -.header { background:#222; height:40px; display:flex; justify-content:space-between; align-items:center; padding:0 10px; } -.header .links { display:flex; align-items:center; height:100%; gap:10px; } -.header .links > a, .header .links > .dropdown > .dropbtn, .header .links > span, .header .links > div#clock { display:flex; align-items:center; justify-content:center; padding:0 12px; color:#eee; font-weight:bold; text-decoration:none; height:40px; line-height:40px; } -.header .links > a:hover, .header .links > .dropdown:hover > .dropbtn { background:#111; color:#0af; } -.dropdown { position:relative; } -.dropbtn { background:none; border:none; font:inherit; color:inherit; cursor:pointer; } -.dropdown-content { display:none; position:absolute; top:100%; left:0; background:#333; min-width:180px; border-radius:3px; box-shadow:0 4px 6px rgba(0,0,0,0.3); } -.dropdown-content a { padding:10px 14px; display:block; color:#eee; text-decoration:none; } -.dropdown-content a:hover { background:#0af; color:#fff; } -.dropdown:hover .dropdown-content { display:block; } +.header { + height: 40px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px; + position: relative; + z-index: 2000; + box-shadow: none; + background: #222; +} + +/* header link container */ +.header .links { + display: flex; + align-items: center; + height: 100%; + gap: 10px; + margin: 0; + padding: 0; +} + +/* header items: links, dropbtns, clock, spans */ +.header .links > a, +.header .links > .dropdown > .dropbtn, +.header .links > span, +.header .links > div#clock { + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + color: #eee; + font-weight: bold; + text-decoration: none; + height: 40px; + line-height: 40px; +} + +/* hover state */ +.header .links > a:hover, +.header .links > .dropdown:hover > .dropbtn { + background: #111; + color: #0af; +} + +/* dropdown basics */ +.dropdown { + position: relative; + display: inline-flex; + align-items: center; + margin: 0; + padding: 0; +} +.dropbtn { + background: none; + border: none; + font: inherit; + color: inherit; + cursor: pointer; + padding: 0 12px; + height: 40px; + line-height: 40px; +} + +/* dropdown menu */ +.dropdown-content { + display: none; + position: absolute; + top: 100%; + left: 0; + background: #333; + min-width: 180px; + border-radius: 3px; + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + z-index: 2100; + margin-top: 0; +} +.dropdown-content a { + padding: 10px 14px; + display: block; + color: #eee; + text-decoration: none; +} +.dropdown-content a:hover { background: #0af; color:#fff; } +.dropdown:hover .dropdown-content { display: block; } /* Submenu (fly-out for Themes) */ .submenu { position: relative; } .submenu > a { display: flex; justify-content: space-between; align-items: center; } -.submenu-content { display: none; position: absolute; top: 0; left: 100%; background-color: #333; min-width: 160px; border-radius: 3px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 10; } +.submenu-content { + display: none; + position: absolute; + top: 0; + left: 100%; + background-color: #333; + min-width: 160px; + border-radius: 3px; + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + z-index: 2200; +} .submenu-content li a { padding: 10px 14px; display: block; color: #eee; text-decoration: none; } .submenu-content li a:hover { background-color: #0af; color: #fff; } .submenu:hover .submenu-content { display: block; } @@ -32,18 +122,23 @@ body { font-family: Arial, sans-serif; margin:0; } .summary { flex:1; } #video { width:620px; height:350px; } -/* === REPLACE the existing "Guide grid" block with the code below === */ - -/* Guide grid */ +/* === Guide grid === */ .guide-outer { height: calc(100vh - 420px); overflow-y: auto; padding-bottom: 80px; position: relative; } .guide-row { display:flex; } .chan-col { width: 200px; flex-shrink:0; border-right:1px solid; position: relative; z-index: 2; background: var(--chan-col-bg, #1a1a1a); } -/* NOTE: grid-col now allows vertical scrolling but we hide visual scrollbars by default */ -.grid-col { position:relative; flex:1; overflow-x:auto; overflow-y:auto; z-index:1; -/* Firefox: hide scrollbar */ scrollbar-width: none; -/* IE10+ */ -ms-overflow-style: none; } - +/* grid-col allows vertical scrolling but we hide visual scrollbars by default */ +.grid-col { + position:relative; + flex:1; + overflow-x:auto; + overflow-y:auto; + z-index:1; + /* Firefox: hide scrollbar */ + scrollbar-width: none; + /* IE10+ */ + -ms-overflow-style: none; +} /* hide webkit scrollbars by default */ .grid-col::-webkit-scrollbar { width: 0; height: 0; } @@ -54,83 +149,123 @@ body { font-family: Arial, sans-serif; margin:0; } /* Use CSS variable for per-cell width; keep both flex-basis and width in sync */ .time-cell { flex: 0 0 var(--timecell-width); width: var(--timecell-width); text-align:center; border-right:1px solid; font-weight:600; } -.chan-name { padding: 10px; height: auto; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 6px; border-bottom: 1px solid; cursor: pointer; position: relative; z-index: 10; pointer-events: auto; user-select: none; } +.chan-name { + padding: 10px; + height: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 6px; + border-bottom: 1px solid; + cursor: pointer; + position: relative; + z-index: 10; + pointer-events: auto; + user-select: none; +} .chan-name img { width:36px; height:36px; object-fit:contain; margin-bottom: 4px; } .grid-row { position:relative; height:60px; border-bottom:1px solid; } .grid-content { position:relative; width: var(--total-width); min-height:100%; } -.program { position:absolute; top:6px; height:48px; border:1px solid; padding:4px 6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; border-radius:6px; z-index:1; } /* ensure fixed header sits above programs */ +.program { + position:absolute; + top:6px; + height:48px; + border:1px solid; + padding:4px 6px; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; + font-size:12px; + border-radius:6px; + z-index:1; +} .program.now { font-weight:bold; } .now-line { position:absolute; top:0; bottom:0; width:2px; z-index:8; pointer-events:none; left: var(--now-offset); } -/* === Scrollbar reveal styles === - We hide the native scrollbars by default (cross-browser) and show a thin, themed scrollbar - when .show-scroll is present (or the element has keyboard focus via :focus-within). -*/ +/* Scrollbar reveal styles */ .grid-col.show-scroll, .grid-col:focus-within { - /* Firefox: show thin scrollbar when revealed */ scrollbar-width: thin; } - -/* Webkit: styled visible scrollbar when .show-scroll is added */ -.grid-col.show-scroll::-webkit-scrollbar { width: 10px; /* vertical scrollbar width */ height: 8px; /* horizontal scrollbar height (if any) */ } +.grid-col.show-scroll::-webkit-scrollbar { width: 10px; height: 8px; } .grid-col.show-scroll::-webkit-scrollbar-track { background: rgba(0,0,0,0.08); border-radius: 6px; } .grid-col.show-scroll::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.28); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; } -/* Apply the variable to the fixed-left spacer (safe to re-declare; will override earlier rules) */ -.time-header-fixed .left-spacer { width: var(--chan-col-width, 200px); min-width: var(--chan-col-width, 200px); max-width: var(--chan-col-width, 200px); height: var(--chan-col-height, auto); /* optional: aligns heights if you changed the fixed header height */ } +/* Apply the variable to the fixed-left spacer */ +.time-header-fixed .left-spacer { + width: var(--chan-col-width, 200px); + min-width: var(--chan-col-width, 200px); + max-width: var(--chan-col-width, 200px); + height: var(--chan-col-height, auto); +} -/* Fixed time header (new) */ -.time-header-fixed { position: fixed; z-index: 1200; left: 0; display: flex; align-items: stretch; overflow: hidden; background: transparent; box-shadow: 0 2px 6px rgba(0,0,0,0.12); height: 34px; top: 0; /* set by JS */ } +/* Fixed time header (core) */ +.time-header-fixed { + position: fixed; + z-index: 1200; + left: 0; + display: flex; + align-items: stretch; + overflow: hidden; + background: transparent; + box-shadow: 0 2px 6px rgba(0,0,0,0.12); + height: 34px; + top: 0; +} .time-header-fixed .grid-content { display:flex; align-items:stretch; position:relative; height:100%; width:100%; } .time-header-fixed .time-header { display:flex; align-items:center; height:100%; flex: 0 0 auto; } -.time-header-fixed .time-cell { display:inline-flex; align-items:center; justify-content:center; height:100%; border-right:1px solid var(--timebar-border, #ccc); padding:0 6px; font-weight:600; background: var(--timebar-bg, rgba(255,255,255,0.95)); color:var(--timebar-color,#000); } +.time-header-fixed .time-cell { + display:inline-flex; align-items:center; justify-content:center; + height:100%; border-right:1px solid var(--timebar-border, #ccc); + padding:0 6px; font-weight:600; background: var(--timebar-bg, rgba(255,255,255,0.95)); + color:var(--timebar-color,#000); +} .time-header-fixed .now-line { position:absolute; top:0; bottom:0; width:2px; z-index:1300; pointer-events:none; } -.time-header-fixed .left-spacer { flex: 0 0 auto; height:100%; background: var(--chan-col-bg, #1a1a1a); border-right: 1px solid var(--timebar-border, rgba(0,0,0,0.12)); } -/* Keep original in-grid time header hidden when using fixed header */ +.time-header-fixed .left-spacer { flex: 0 0 auto; height:100%; background: var(--chan-col-bg, #1a1a1a); border-right: 1px solid var(--timebar-border, rgba(0,0,0,0.12)); pointer-events: none; } .hide-in-grid .time-header-wrap { visibility: hidden; height: 0; margin:0; padding:0; } - /* Dark theme */ - body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; } - body.dark .time-header-fixed .now-line { background:#0f0; } - body.dark {--timebar-bg: #222; --timebar-border: #444; --timebar-color: #ddd; --now-line-color: #0f0; --chan-col-bg: #1a1a1a; } - body.dark { background:#111; color:#ddd; } - body.dark .header { background:#222; } - body.dark #video { background:#000; } - body.dark .summary { color:#ccc; } - body.dark .chan-col { background:#1a1a1a; border-color:#333; } - body.dark .grid-col { background:#181818; } - body.dark .chan-header, body.dark .time-header-wrap { background:#222; border-color:#444; } - body.dark .time-cell { color:#bbb; border-color:#333; } - body.dark .chan-name { color:#fff; border-color:#333; } - body.dark .program { background:#3a3a3a; border-color:#555; color:#eee; } - body.dark .program.now { background:#2d5030; border-color:#47a447; } - body.dark .now-line { background:#0f0; } - - /* Light theme */ - body.light { --timebar-bg:#fff; --timebar-border:#ddd; --timebar-color:#000; --chan-col-bg:#f9f9f9; background:#fff; color:#000; } - body.light .time-header-fixed .now-line { background:#090; } - body.light { --timebar-bg: #fff; --timebar-border: #ddd; --timebar-color: #000; --now-line-color: #090; --chan-col-bg: #f9f9f9; } - body.light { background:#fff; color:#000; } - body.light .header { background:#222; } - body.light #video { background:#fff; } - body.light .summary { color:#000; } - body.light .chan-col { background:#f9f9f9; border-color:#ccc; } - body.light .grid-col { background:#fafafa; } - body.light .chan-header, body.light .time-header-wrap { background:#ddd; border-color:#bbb; } - body.light .time-cell { color:#000; border-color:#ccc; } - body.light .chan-name { color:#000; border-color:#ccc; } - body.light .program { background:#ddd; border-color:#aaa; color:#000; } - body.light .program.now { background:#cfe8cf; border-color:#090; } - body.light .now-line { background:#090; } +/* Theme variables & theme-specific rules preserved below (copied/preserved from original base.css) */ + +/* Dark theme */ +body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; } +body.dark .time-header-fixed .now-line { background:#0f0; } +body.dark .header { background:#222; } +body.dark #video { background:#000; } +body.dark .summary { color:#ccc; } +body.dark .chan-col { background:#1a1a1a; border-color:#333; } +body.dark .grid-col { background:#181818; } +body.dark .chan-header, body.dark .time-header-wrap { background:#222; border-color:#444; } +body.dark .time-cell { color:#bbb; border-color:#333; } +body.dark .chan-name { color:#fff; border-color:#333; } +body.dark .program { background:#3a3a3a; border-color:#555; color:#eee; } +body.dark .program.now { background:#2d5030; border-color:#47a447; } +body.dark .now-line { background:#0f0; } + +/* Light theme */ +body.light { --timebar-bg:#fff; --timebar-border:#ddd; --timebar-color:#000; --chan-col-bg:#f9f9f9; background:#fff; color:#000; } +body.light .time-header-fixed .now-line { background:#090; } +body.light .header { background:#222; } +body.light #video { background:#fff; } +body.light .summary { color:#000; } +body.light .chan-col { background:#f9f9f9; border-color:#ccc; } +body.light .grid-col { background:#fafafa; } +body.light .chan-header, body.light .time-header-wrap { background:#ddd; border-color:#bbb; } +body.light .time-cell { color:#000; border-color:#ccc; } +body.light .chan-name { color:#000; border-color:#ccc; } +body.light .program { background:#ddd; border-color:#aaa; color:#000; } +body.light .program.now { background:#cfe8cf; border-color:#090; } +body.light .now-line { background:#090; } +body.light .header #clock { color: #ffffff !important; } /* Retro-AOL */ -body.retro-aol{--timebar-bg:#004466;--timebar-border:#33cccc;--timebar-color:#33cccc;--now-line-color:#090;--chan-col-bg:#004466;--panel-bg-top:#e6f7f5;--panel-bg-bottom:#cfeff0;--now-line-panel-color:var(--panel-bg-top);--now-line-glow:rgba(198,242,240,0.6);background:#2f4f4f;color:#f0f0f0;font-family:"Tahoma","Arial",sans-serif} -body.retro-aol .header{background:#004466;color:#ffcc00;border-bottom:2px solid #33cccc} +body.retro-aol { --timebar-bg:#004466; --timebar-border:#33cccc; --timebar-color:#33cccc; --chan-col-bg:#004466; background:#2f4f4f; color:#f0f0f0; font-family:"Tahoma","Arial",sans-serif; } +body.retro-aol .header { background:#004466; color:#ffcc00; border-bottom:2px solid #33cccc; } body.retro-aol .dropdown-content,body.retro-aol .submenu-content{background:#004466;border:1px solid #33cccc;color:#ffcc01;box-shadow:0 4px 6px rgba(0,0,0,.3)} body.retro-aol .dropdown-content a,body.retro-aol .submenu-content li a,body.retro-aol .dropdown-content .submenu> a,body.retro-aol .submenu-content .submenu> a{color:#ffcc01!important} body.retro-aol .dropdown-content a:hover,body.retro-aol .submenu-content li a:hover{background:#33cccc!important;color:#004466!important} -body.retro-aol .summary{background:linear-gradient(to bottom,var(--panel-bg-top) 0%,var(--panel-bg-bottom) 100%);color:#00313a;border:2px solid #004466;border-top-color:#66e0e0;border-left-color:#66e0e0;border-right-color:#004466;border-bottom-color:#004466;box-shadow:4px 4px 0 rgba(0,0,0,.28);padding:12px;border-radius:6px;box-sizing:border-box} +body.retro-aol .summary{background:linear-gradient(to bottom,var(--panel-bg-top,#e6f7f5) 0%,var(--panel-bg-bottom,#cfeff0) 100%);color:#00313a;border:2px solid #004466;padding:12px;border-radius:6px;box-sizing:border-box} body.retro-aol .summary h3{margin:0 0 6px 0;color:#004466;font-size:1.05em;font-weight:700} body.retro-aol .summary p{margin:0;color:#05383a;line-height:1.3} body.retro-aol .time-header-fixed .now-line,body.retro-aol .now-line{background:var(--now-line-panel-color);box-shadow:0 0 6px var(--now-line-glow);width:3px} @@ -147,197 +282,198 @@ body.retro-aol .header .links>a,body.retro-aol .header .links>.dropdown>.dropbtn body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-thumb{background:rgba(0,68,102,.55)} body.retro-aol .grid-col.show-scroll::-webkit-scrollbar-track{background:rgba(51,204,204,.06)} - /* === RetroIPTVGuide Theme: TV Guide 1990 Edition (Final Compact) === */ - body.tvguide1990{--chan-col-width:200px;--chan-col-height:38px} - body.tvguide1990 .time-header-fixed .left-spacer{width:var(--chan-col-width,200px)!important;min-width:var(--chan-col-width,200px)!important;max-width:var(--chan-col-width,200px)!important;height:var(--chan-col-height,auto)!important;flex:0 0 var(--chan-col-width,200px)!important} - body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --chan-col-bg: #d8d7d3; } - body.tvguide1990 { --timebar-bg: #fff; --timebar-border: #000; --timebar-color: #000; --now-line-color: #000; --chan-col-bg: #d8d7d3; } - body.tvguide1990{background:#d8d7d3;color:#000;font-family:'Times New Roman',serif;font-size:.9em;} - body.tvguide1990 .header{background:#fff;color:#000;border-bottom:2px solid #000;box-shadow:none;} - body.tvguide1990 .header .links>a, - body.tvguide1990 .header .links>.dropdown>.dropbtn, - body.tvguide1990 .header .links>span, - body.tvguide1990 .header .links>#clock{color:#000!important;background:#fff!important;} - body.tvguide1990 .header .links>a:hover, - body.tvguide1990 .header .links>.dropdown:hover>.dropbtn{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .dropdown-content{background:#fff!important;color:#000!important;border:1px solid #000!important;box-shadow:none!important;} - body.tvguide1990 .dropdown-content a{color:#000!important;} - body.tvguide1990 .dropdown-content a:hover{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .submenu-content{background:#fff!important;border:1px solid #000!important;box-shadow:none!important;z-index:1000;} - body.tvguide1990 .submenu-content li a{color:#000!important;} - body.tvguide1990 .submenu-content li a:hover{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .dropdown-content .submenu>a{color:#000!important;padding-right:22px!important;position:relative;} - body.tvguide1990 .dropdown-content .submenu>a::after{content:"โ–ธ";position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#000;} - body.tvguide1990 .dropdown-content .submenu:hover>a{background:#e0e0e0!important;color:#000!important;} - body.tvguide1990 .footer{background:transparent;color:#000;border-top:2px solid #000;} - body.tvguide1990 .chan-name img{display:none!important;} - body.tvguide1990 .chan-name span:not(.channel-number){display:none!important;} - body.tvguide1990 .chan-col{height:38px!important;padding:2px 0!important;display:flex;align-items:center;justify-content:center;background:#d8d7d3;border-bottom:1px solid #000;} - body.tvguide1990 .guide-row>.chan-col{border-right:1px solid #000!important;} /* vertical divider */ - body.tvguide1990 .channel-number{display:inline-flex;align-items:center;justify-content:center;font-family:'Arial Narrow','Helvetica Neue',sans-serif;font-weight:700;font-size:.9em;min-width:32px;padding:3px 12px;border-radius:999px;background:#000;color:#fff;letter-spacing:-.2px;border:1px solid #000;line-height:1;transform:scale(.9);} - body.tvguide1990 .chan-header{border-bottom:none!important;} - body.tvguide1990 .guide-row{align-items:stretch!important;} - body.tvguide1990 .time-header-wrap{border-top:1px solid #000!important;border-bottom:1px solid #000!important;margin-bottom:0!important;padding-bottom:0!important;height:38px!important;box-sizing:border-box;} - body.tvguide1990 .chan-col{height:38px!important;box-sizing:border-box;} - body.tvguide1990 .grid-content,body.tvguide1990 .time-header{transform:scaleX(.9);transform-origin:left center;} - body.tvguide1990 .time-cell{border-right:1px solid #000;color:#000;font-weight:700;font-size:.8em;padding:0 2px;min-width:0;} - body.tvguide1990 .grid-row{height:38px!important;position:relative;padding:1px 0;box-sizing:border-box;border-bottom:1px solid #000;} - body.tvguide1990 .program, - body.tvguide1990 .program.now{position:absolute;top:1px!important;height:calc(100% - 2px)!important;margin:0!important;padding:2px 4px!important;border:1px solid #000;box-sizing:border-box;border-radius:0!important;line-height:1.2;font-size:.8em!important;display:flex;align-items:center;background:#fff;font-weight:400!important;} - body.tvguide1990 .program.now{background:#e7e5dd!important;font-weight:700!important;} - body.tvguide1990 .grid-col{overflow-x:auto!important;overflow-y:hidden!important;height:38px!important;box-sizing:border-box;} - body.tvguide1990 .grid-content{height:100%!important;} - body.tvguide1990 #clock::before{content:"IPTV GUIDE";display:inline-block;margin-right:10px;padding:4px 10px;background:#fff;color:#000;font-weight:900;font-size:12px;line-height:1;border-radius:50px;border:2px solid #000;box-shadow:none;letter-spacing:.3px;vertical-align:middle;} - body.tvguide1990 #summary{border:2px solid #000;background:#fff;padding:12px;margin:10px;box-shadow:2px 2px 5px rgba(0,0,0,.25);} - body.tvguide1990 #video{border:2px solid #000;background:#000;box-shadow:3px 3px 6px rgba(0,0,0,.35);} - body.tvguide1990 .chan-col{position:relative;} - body.tvguide1990 .chan-col::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:20px;height:60%;background:#000;border-right:1px solid #000;} - body.tvguide1990 { --chan-col-width: 200px; --chan-col-height: 38px; } - - - /* DirecTV */ - body.directv { --timebar-bg:#002b80; --timebar-border:#001f66; --timebar-color:#d8ebff; --chan-col-bg:#001b50; } - body.directv .time-header-fixed .now-line { background:#ffcc00; } - body.directv { --timebar-bg: #002b80; --timebar-border: #001f66; --timebar-color: #d8ebff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } - body.directv { background:#001a66; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } - body.directv .header { background:linear-gradient(to bottom,#1d72ff,#003a8c); border-bottom:2px solid #001f66; } - body.directv .header .links > a, body.directv .header .links > .dropdown > .dropbtn, body.directv .header .links > span, body.directv .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } - body.directv .header .links > a:hover, body.directv .header .links > .dropdown:hover > .dropbtn { background:#003f9e !important; color:#ffd802 !important; } - body.directv .dropdown-content, body.directv .submenu-content { background:#003a8c !important; border:1px solid #0070ff !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } - body.directv .dropdown-content a, body.directv .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } - body.directv .dropdown-content a:hover, body.directv .submenu-content li a:hover { background:#0070ff !important; color:#fff !important; } - body.directv .dropdown-content .submenu > a { color:#fff !important; padding-right:28px !important; position:relative; } - body.directv .dropdown-content .submenu > a::after { content:"โ–ธ"; position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#fff; } - body.directv .dropdown-content .submenu:hover > a { background:#0070ff !important; color:#fff !important; } - body.directv .summary, body.directv #program-info { background:linear-gradient(to bottom,#66b2ff,#003f9e); color:#fff; border:1px solid #004bdb; } - body.directv .time-header-wrap { background:#002b80; color:#d8ebff; border-bottom:2px solid #001f66; } - body.directv .time-cell { background:#002b80; color:#b9dcff; border-color:#001f66; font-weight:bold; } - body.directv .chan-col { background:#001f66; border-right:1px solid #004bdb; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } - body.directv .chan-header { background:linear-gradient(to bottom,#0b3b8c,#3c8dff); color:#fff; border-color:#003b91; } - body.directv .grid-col { background:#001f66; } - body.directv .program { background:#003a8c; border:1px solid #004bdb; color:#fff; border-radius:2px; } - body.directv .program.now { background:#ffd802; border:1px solid #caa600; color:#000; font-weight:bold; box-shadow:none; } - body.directv .program:hover { background:linear-gradient(to right,#1d67d9,#7fbfff); color:#fff; } - body.directv .now-line { background:#ffcc00; } - body.directv .footer { background:linear-gradient(to top,#001f66,#004bdb); color:#fff; border-top:2px solid #003580; padding:5px 10px; font-size:0.9em; } - body.directv .footer .dot-red { color:#ff3c3c; } body.directv .footer .dot-green { color:#00cc00; } body.directv .footer .dot-yellow { color:#ffd700; } - body.directv .footer .dot-blue { color:#4ca9ff; } - body.directv #video { background:#000; border:2px solid #004bdb; box-shadow:0 0 6px rgba(0,0,0,0.6); } - body.directv #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } - - /* Comcast */ - body.comcast { --timebar-bg:linear-gradient(to bottom,#0055cc,#001b50); --timebar-border:#003090; --timebar-color:#fff; --chan-col-bg:#001b50; } - body.comcast .time-header-fixed .now-line { background:#ffcc00; } - body.comcast { --timebar-bg: linear-gradient(to bottom,#0055cc,#001b50); --timebar-border: #003090; --timebar-color: #fff; --now-line-color: #ffcc00; --chan-col-bg: #001b50; } - body.comcast { background:#001b50; color:#fff; font-family:"Segoe UI","Arial",sans-serif; } - body.comcast .header { background:linear-gradient(to bottom,#0055cc,#001b50); border-bottom:2px solid #003090; } - body.comcast .header .links > a, body.comcast .header .links > .dropdown > .dropbtn, body.comcast .header .links > span, body.comcast .header .links > div#clock { color:#ffffff !important; font-weight:bold !important; text-shadow:1px 1px 2px #000 !important; } - body.comcast .header .links > a:hover, body.comcast .header .links > .dropdown:hover > .dropbtn { background:#003890 !important; color:#ffcc00 !important; } - body.comcast .dropdown-content, body.comcast .submenu-content { background:#002a70 !important; border:1px solid #0044cc !important; box-shadow:0 4px 8px rgba(0,0,0,.4) !important; } - body.comcast .dropdown-content a, body.comcast .submenu-content li a { color:#ffffff !important; font-weight:bold !important; } - body.comcast .dropdown-content a:hover, body.comcast .submenu-content li a:hover { background:#0044cc !important; color:#fff !important; } - body.comcast .summary, body.comcast #program-info { background:linear-gradient(to bottom,#001b50,#003890); color:#fff; border:1px solid #003090; } - body.comcast .time-header-wrap { background:#003890; color:#bcd8ff; border-bottom:2px solid #002b80; } - body.comcast .time-cell { background:#003890; color:#ffffff; border-color:#002b80; font-weight:bold; } - body.comcast .chan-col { background:#001b50; border-right:1px solid #0044cc; color:#fff; font-weight:bold; text-shadow:1px 1px 2px #000; } - body.comcast .chan-header { background:#002a70; color:#fff; border-color:#0044cc; } - body.comcast .grid-col { background:#002a70; } - body.comcast .program { background:#003890; border:1px solid #0044cc; color:#fff; border-radius:2px; } - body.comcast .program.now { background:#ffffff; border:1px solid #cccccc; color:#000; font-weight:bold; } - body.comcast .program:hover { background:#0044cc; color:#fff; } - body.comcast .now-line { background:#ffcc00; } - body.comcast #video { background:#000; border:2px solid #0044cc; box-shadow:0 0 8px rgba(0,0,0,.6); } - body.comcast .footer { background:linear-gradient(to top,#001b50,#0044cc); color:#fff; border-top:2px solid #002b80; padding:5px 10px; font-size:0.9em; } - body.comcast .footer .dot-red { color:#ff3c3c; } body.comcast .footer .dot-green { color:#00cc00; } body.comcast .footer .dot-yellow { color:#ffd700; } - body.comcast .footer .dot-blue { color:#4ca9ff; } - body.comcast #clock::before { content:"IPTV GUIDE"; display:inline-block; margin-right:10px; padding:3px 8px; background:#e41e26; color:#fff; font-weight:900; font-size:12px; line-height:1; border-radius:3px; border:1px solid rgba(255,255,255,.85); box-shadow:0 1px 2px rgba(0,0,0,.35); letter-spacing:.4px; vertical-align:middle; } - - /* Retro TV Guide Magazine Theme */ - body.retro-magazine{--panel-bg-top:#ffffff;--panel-bg-bottom:#f2f2f2;--now-line-panel-color:#fff;--now-line-glow:rgba(0,0,0,0.22);background:#fff;color:#000;font-family:"Times New Roman","Georgia",serif} - body.retro-magazine .summary{background:linear-gradient(to bottom,var(--panel-bg-top) 0%,var(--panel-bg-bottom) 100%);color:#000;border:2px solid #000;border-top-color:#000;border-left-color:#000;border-right-color:#000;border-bottom-color:#000;box-shadow:4px 4px 0 rgba(0,0,0,0.22);padding:12px;border-radius:6px;box-sizing:border-box} - body.retro-magazine .summary h3{margin:0 0 6px 0;color:#000;font-size:1.05em;font-weight:700} - body.retro-magazine .summary p{margin:0;color:#111;line-height:1.35} - body.retro-magazine .time-header-fixed .now-line,body.retro-magazine .now-line{background:var(--now-line-panel-color);box-shadow:0 0 6px var(--now-line-glow);width:3px} - body.retro-magazine { background:#ffffff; color:#000000; font-family:"Times New Roman","Georgia",serif; } - body.retro-magazine .header { background:#ffffff; color:#000; border-bottom:2px solid #000; font-weight:bold; } - body.retro-magazine .header .links > a, - body.retro-magazine .header .links > .dropdown > .dropbtn, - body.retro-magazine .header .links > span, - body.retro-magazine .header .links > #clock { background:#ffffff !important; color:#000000 !important; } - body.retro-magazine .header .links > a:hover, - body.retro-magazine .header .links > .dropdown:hover > .dropbtn { background:#e0e0e0 !important; color:#000000 !important; } - body.retro-magazine .dropdown-content { background:#ffffff !important; color:#000000 !important; border:1px solid #000 !important; box-shadow:none !important; } - body.retro-magazine .dropdown-content a { background:#ffffff !important; color:#000000 !important; } - body.retro-magazine .dropdown-content a:hover { background:#e0e0e0 !important; color:#000000 !important; } - body.retro-magazine #video { background:#000; } - body.retro-magazine .chan-col { background:#fff; border:1px solid #000; color:#000; font-weight:bold; } - body.retro-magazine .grid-col { background:#fff; } - body.retro-magazine .chan-header, - body.retro-magazine .time-header-wrap { background:#fff; color:#fff; border:1px solid #000; font-weight:bold; } - body.retro-magazine .time-cell { color:#000; border:1px solid #000; font-weight:bold; } - body.retro-magazine .chan-name { color:#000; border:1px solid #000; font-weight:bold; } - body.retro-magazine .program { background:#fff; border:1px solid #000; color:#000; font-size:14px; } - body.retro-magazine .program.now { background:#e0e0e0; border:2px solid #000; color:#000; font-weight:bold; } - body.retro-magazine .now-line { background:#000; height:3px; } - body.retro-magazine #current-tuner { color:#000000 !important; font-weight:bold; } - body.retro-magazine .time-header-fixed .left-spacer { background: var(--timebar-bg, #fff) !important; } - - /* Add other theme variables as needed */ - body{transition:background-color .3s ease,color .3s ease;} - body.fade-switch{opacity:0;transition:opacity .25s ease;} - -/* Insert inside - - -
    - - - -
    --:-- --
    -
    - -

    About RetroIPTVGuide

    -
    +{% block content %} +
    +
    +

    About RetroIPTVGuide

      -
    • Version: {{ info.version }}
    • -
    • Release date: {{ info.release_date }}
    • -
    • Python version: {{ info.python_version }}
    • -
    • OS: {{ info.os_info }}
    • -
    • Install path: {{ info.install_path }}
    • -
    • Database: {{ info.db_path }}
    • -
    • Logs: {{ info.log_path }}
    • -
    • Server uptime: {{ info.uptime }}
    • +
    • Version:{{ info.version }}
    • +
    • Release date:{{ info.release_date }}
    • +
    • Python version:{{ info.python_version }}
    • +
    • OS:{{ info.os_info }}
    • +
    • Install path:{{ info.install_path }}
    • +
    • Database:{{ info.db_path }}
    • +
    • Logs:{{ info.log_path }}
    • +
    • Server uptime:{{ info.uptime }}
    +
    +{% endblock %} +{% block scripts_extra %} - - - +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..86fcd39 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,276 @@ + + + + {% block title %}IPTV Guide{% endblock %} + + + + + + {% block page_styles %} + {# per-page CSS variables or small style blocks can be provided here #} + {% endblock %} + + + {% include '_header.html' %} + + {% block content %} + + {% endblock %} + + + + + + + {% block scripts_extra %} + {# per-page scripts can be placed here #} + {% endblock %} + + diff --git a/templates/change_password.html b/templates/change_password.html index 213701c..d7cded7 100644 --- a/templates/change_password.html +++ b/templates/change_password.html @@ -1,241 +1,13 @@ - - - - Change Password - - - -
    - - -
    --:-- --
    -
    - -
    +{% block content %} +
    +

    Change Password

    @@ -243,39 +15,28 @@

    Change Password

    + {% with messages = get_flashed_messages() %} {% if messages %} - {% for msg in messages %} -

    {{ msg }}

    - {% endfor %} +
    + {% for msg in messages %} +

    {{ msg }}

    + {% endfor %} +
    {% endif %} {% endwith %} +
    +{% endblock %} +{% block scripts_extra %} - - - +{% endblock %} diff --git a/templates/change_tuner.html b/templates/change_tuner.html index b0a12a6..b7e3737 100644 --- a/templates/change_tuner.html +++ b/templates/change_tuner.html @@ -1,228 +1,14 @@ - - - - Change Tuner - - - -
    - -
    --:-- --
    -
    - -
    +{% block content %} +
    +

    Change Active Tuner

    @@ -234,9 +20,9 @@

    Change Active Tuner

    -
    +
    -
    +

    Update Tuner URLs

    @@ -246,28 +32,31 @@

    Update Tuner URLs

    {% endfor %} - - +
    + +
    +
    + +
    -
    +
    - -
    +

    Manage Tuners

    - - - +
    +
    +
    - + @@ -291,48 +80,43 @@

    Rename Tuner

    {% endfor %} - +
    + +
    +
    +{% endblock %} +{% block scripts_extra %} - - - +{% endblock %} diff --git a/templates/guide.html b/templates/guide.html index fa6def0..fad5c51 100644 --- a/templates/guide.html +++ b/templates/guide.html @@ -47,7 +47,7 @@
  • TV Guide (Refresh)
  • DirecTV
  • Comcast
  • -
  • TV Guide (Classic)
  • +
    @@ -394,79 +394,79 @@

    Program Info

    diff --git a/templates/logs.html b/templates/logs.html index f0479dd..3ae3355 100644 --- a/templates/logs.html +++ b/templates/logs.html @@ -1,144 +1,58 @@ - - - - Activity Log - - - -
    - -
    --:-- --
    -
    - -
    +{% block content %} +
    +

    Activity Log ({{ log_size }} bytes)

    - -
    - -
    + +
    + +
    - - - + + +
    - - - - - - {% for user, action, ts, log_type in entries %} - - - - - - {% endfor %} - -
    UserActionTimestamp
    {{ user }}{{ action }}{{ ts }}
    +
    + + + + + + {% for user, action, ts, log_type in entries %} + + + + + + {% endfor %} + +
    UserActionTimestamp
    {{ user }}{{ action }}{{ ts }}
    +
    +
    +{% endblock %} +{% block scripts_extra %} - - - +{% endblock %} diff --git a/templates/manage_users.html b/templates/manage_users.html index bedd200..4db5521 100644 --- a/templates/manage_users.html +++ b/templates/manage_users.html @@ -1,165 +1,71 @@ - - - - Manage Users - - - -
    - -
    --:-- --
    -
    - -
    +{% block content %} +
    +

    Manage Users

    -
    -

    Add New User

    - - - - + + +

    Add New User

    + +
    +
    +

    Existing Users

    - - - {% for user in users %} - - - + + {% endfor %} + +
    UsernameActions
    {{ user }} -
    - - - +
    + + + + + + {% for user in users %} + + + - - {% endfor %} -
    UsernameActions
    {{ user }} + + + + -
    - - - + + + +
    -
    +
    +
    {% with messages = get_flashed_messages() %} {% if messages %} - {% for msg in messages %} -

    {{ msg }}

    - {% endfor %} +
    + {% for msg in messages %} +

    {{ msg }}

    + {% endfor %} +
    {% endif %} {% endwith %} +
    +{% endblock %} +{% block scripts_extra %} - - +{% endblock %} From 1bd6bebb23c85e704b69f9d92dd04d899279019d Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:02:28 -0500 Subject: [PATCH 5/6] Update auto-scroll.js --- static/js/auto-scroll.js | 577 ++++++++++++++++++++++++--------------- 1 file changed, 355 insertions(+), 222 deletions(-) diff --git a/static/js/auto-scroll.js b/static/js/auto-scroll.js index 545cbe4..3d7869d 100644 --- a/static/js/auto-scroll.js +++ b/static/js/auto-scroll.js @@ -1,6 +1,9 @@ -// Robust auto-scroll with runtime theme-change handling (tvguide1990). -// Replaces previous auto-scroll behavior: stops when theme becomes tvguide1990, -// reflects the stored preference, and still allows the user to re-enable via settings. +// auto-scroll v36.3 โ€” deterministic wrap + RAF primary with interval fallback watchdog. +// - Clone full row elements so program cells are carried with clones. +// - Deterministic immediate wrap to prep offset to avoid stop/restart races. +// - Primary animation via requestAnimationFrame; fallback watcher uses setInterval to nudge scrollTop +// when RAF hasn't advanced (handles throttling/race across browsers). +// - Exposes status and cloneNow APIs. (function () { const PREF_KEY = 'autoScrollEnabled'; @@ -8,21 +11,37 @@ function setPref(v) { localStorage.setItem(PREF_KEY, v ? 'true' : 'false'); } const SELECTOR_PRIORITY = ['#guideOuter', '.guide-outer', '.grid-col']; - const scrollSpeed = 1.2; // px per frame - visible - const idleDelay = 5000; // ms + const scrollSpeed = 1.2; // px per frame (visual) + const idleDelay = 15000; // ms initial inactivity/start delay (15s) + const waitForContentMs = 5000; // wait up to 5s for rows to be populated before cloning + const contentSampleCount = 3; // sample when checking readiness + + const PROGRAM_CELL_SELECTORS = [ + '.programme', '.program', '.prog', '.prog-col', '.prog-cell', '.program-cell', + '.title', '.epg-item', '.time', '.programme-item', '.programs', '.epg' + ]; let scroller = null; let rafId = null; let isScrolling = false; let lastActivity = Date.now(); let idleInterval = null; - let bodyObserver = null; + let watchdogInterval = null; + + // timestamps for watchdog: last time frameLoop actually ran + let lastFrameTime = 0; + + let loopMode = true; + let endReached = false; + let endReachedAt = 0; + let autoRestart = false; + let autoRestartDelayMs = 30000; function log(...args) { - if (window && window.console && console.debug) console.debug.apply(console, ['[auto-scroll]'].concat(args)); + if (window && window.console && console.debug) console.debug.apply(console, ['[auto-scroll v36.3]'].concat(args)); } - function findBestScroller() { + function findScroller() { const nodes = SELECTOR_PRIORITY.map(s => Array.from(document.querySelectorAll(s))).flat(); if (!nodes.length) return null; let best = null; @@ -30,16 +49,12 @@ try { const delta = Math.max(0, n.scrollHeight - n.clientHeight); if (!best || delta > best.delta) best = { el: n, delta }; - } catch (e) { /* ignore */ } + } catch (e) {} }); - if (!best && document.getElementById('guideOuter')) { - const g = document.getElementById('guideOuter'); - return { el: g, delta: Math.max(0, g.scrollHeight - g.clientHeight) }; - } - return best; + return best ? best.el : null; } - function ensureScrollerStyles(el) { + function ensureStyles(el) { try { const cs = getComputedStyle(el); if (!/(auto|scroll)/.test(cs.overflowY)) el.style.overflowY = 'auto'; @@ -47,269 +62,387 @@ el.style.maxHeight = 'calc(100vh - 420px)'; } el.style.scrollBehavior = 'auto'; - } catch (e) { /* ignore */ } + } catch (e) {} } - function markClone(node) { + function supportsNativeSmoothScroll() { + try { return 'scrollBehavior' in document.documentElement.style; } catch (e) { return false; } + } + + function smoothScrollTo(el, targetTop, duration = 650) { + if (!el) return Promise.resolve(); + if (supportsNativeSmoothScroll()) { + try { + el.scrollTo({ top: targetTop, behavior: 'smooth' }); + return new Promise(resolve => setTimeout(resolve, duration)); + } catch (e) {} + } + return new Promise(resolve => { + const start = el.scrollTop; + const change = targetTop - start; + const startTime = performance.now(); + const dur = Math.max(1, duration); + const ease = t => (t < 0.5) ? (2 * t * t) : (-1 + (4 - 2 * t) * t); + function step(now) { + const elapsed = now - startTime; + const t = Math.min(1, elapsed / dur); + try { el.scrollTop = start + change * ease(t); } catch (e) {} + if (t < 1) requestAnimationFrame(step); + else resolve(); + } + requestAnimationFrame(step); + }); + } + + function markClone(node, srcId) { try { node.classList.add('__auto_scroll_clone'); - node.dataset.__autoScrollClone = '1'; - node.setAttribute('aria-hidden', 'true'); - node.style.pointerEvents = 'none'; + node.dataset.autoScrollClone = '1'; + if (srcId) node.dataset.autoScrollSrcid = srcId; Array.from(node.querySelectorAll('[id]')).forEach(el => el.removeAttribute('id')); - } catch (e) { /* ignore */ } + } catch (e) {} } - // Generic clone-for-seamless-loop: clones enough rows from the start/end to fill visible area. - function cloneRowsForSeamlessLoop(scrollerEl) { + function getOriginalRows(sc) { try { - if (!scrollerEl) return; - if (scrollerEl.dataset.__autoScrollCloned === '1') return; // already applied - - let rows = Array.from(scrollerEl.querySelectorAll('.chan-col')); - if (!rows.length) { - rows = Array.from(scrollerEl.children).filter(n => n.nodeType === 1); - } - if (!rows.length) { - log('cloneRowsForSeamlessLoop: no rows found; skipping clone'); - scrollerEl.dataset.__autoScrollCloned = '1'; - return; + const chanCols = Array.from(sc.querySelectorAll('.chan-col')); + const rowsSet = new Set(); + if (chanCols.length) { + chanCols.forEach(c => { + const row = c.closest('.guide-row') || c.parentElement; + if (row && row.nodeType === 1) rowsSet.add(row); + }); + } else { + Array.from(sc.children).forEach(ch => { if (ch && ch.nodeType === 1) rowsSet.add(ch); }); } + return Array.from(rowsSet).filter(r => !(r.dataset && (r.dataset.autoScrollClone === '1' || r.dataset.__autoScrollClone === '1'))); + } catch (e) { + return []; + } + } + + function rowHasContent(r) { + if (!r) return false; + const name = r.querySelector && r.querySelector('.chan-name'); + if (name && name.textContent && name.textContent.trim().length) return true; + for (const sel of PROGRAM_CELL_SELECTORS) { + const el = r.querySelector(sel); + if (el && el.textContent && el.textContent.trim().length) return true; + } + const txt = r.textContent || ''; + return txt.trim().length > 4; + } - let rowHeight = rows[0].getBoundingClientRect().height || rows[0].offsetHeight || 40; - if (!isFinite(rowHeight) || rowHeight <= 0) rowHeight = 40; - const visibleRows = Math.max(1, Math.ceil(scrollerEl.clientHeight / rowHeight)); - const clonesPerSide = visibleRows + 1; - - const totalRows = rows.length; - const leftClones = Math.min(clonesPerSide, totalRows); - const rightClones = Math.min(clonesPerSide, totalRows); - - const toPrepend = []; - for (let i = 0; i < leftClones; i++) { - const src = rows[totalRows - 1 - i]; - if (!src) break; - const clone = src.cloneNode(true); - markClone(clone); - toPrepend.push(clone); + function scrollerHasProgramInfo(sc) { + try { + for (const sel of PROGRAM_CELL_SELECTORS) { + const el = sc.querySelector(sel); + if (el && el.textContent && el.textContent.trim().length > 0) return true; } - for (let i = toPrepend.length - 1; i >= 0; i--) { - scrollerEl.insertBefore(toPrepend[i], scrollerEl.firstChild); + if ((sc.innerText || sc.textContent || '').trim().length > 60) return true; + } catch (e) {} + return false; + } + + function waitForProgramInfo(sc, timeoutMs = 4000) { + return new Promise(resolve => { + const start = Date.now(); + const check = () => { + try { + if (scrollerHasProgramInfo(sc)) return resolve(true); + if (Date.now() - start >= timeoutMs) return resolve(false); + } catch (e) { return resolve(false); } + setTimeout(check, 150); + }; + check(); + }); + } + + function waitForContent(sc, timeoutMs = waitForContentMs, sampleCount = contentSampleCount) { + return new Promise(resolve => { + const start = Date.now(); + const check = () => { + const rows = getOriginalRows(sc); + let ok = false; + for (let i = 0; i < Math.min(sampleCount, rows.length); i++) { + if (rowHasContent(rows[i])) { ok = true; break; } + } + if (ok) return resolve(true); + if (Date.now() - start >= timeoutMs) return resolve(false); + setTimeout(check, 150); + }; + check(); + }); + } + + // Clone rows (full row elements). Returns Promise resolved once clones are added and prep offset stored. + function cloneOnce(sc) { + return new Promise(resolve => { + try { + if (!sc) return resolve(0); + if (sc.dataset.__autoScrollCloned === '1') return resolve(0); + + const originals = getOriginalRows(sc); + if (!originals.length) { sc.dataset.__autoScrollCloned = '1'; return resolve(0); } + + originals.forEach((orig, idx) => { + if (!orig.dataset.autoScrollSrcid) orig.dataset.autoScrollSrcid = 'asrc-' + idx + '-' + Date.now(); + }); + + let rowHeight = originals[0].getBoundingClientRect().height || originals[0].offsetHeight || 40; + if (!isFinite(rowHeight) || rowHeight <= 0) rowHeight = 40; + const visibleRows = Math.max(1, Math.ceil(sc.clientHeight / rowHeight)); + const clonesPerSide = visibleRows + 1; + + const total = originals.length; + const left = Math.min(clonesPerSide, total); + const right = Math.min(clonesPerSide, total); + + const leftClones = []; + for (let i = 0; i < left; i++) { + const srcRow = originals[total - 1 - i]; + if (!srcRow) break; + const cloneRow = srcRow.cloneNode(true); + try { cloneRow.innerHTML = srcRow.innerHTML; } catch (e) {} + markClone(cloneRow, srcRow.dataset.autoScrollSrcid); + leftClones.push(cloneRow); + } + + let prependedHeight = 0; + for (let i = leftClones.length - 1; i >= 0; i--) sc.insertBefore(leftClones[i], sc.firstChild); + for (let i = 0; i < leftClones.length; i++) { + const h = leftClones[i].getBoundingClientRect().height || leftClones[i].offsetHeight || 0; + prependedHeight += h; + } + + for (let i = 0; i < right; i++) { + const srcRow = originals[i]; + if (!srcRow) break; + const cloneRow = srcRow.cloneNode(true); + try { cloneRow.innerHTML = srcRow.innerHTML; } catch (e) {} + markClone(cloneRow, srcRow.dataset.autoScrollSrcid); + sc.appendChild(cloneRow); + } + + if (prependedHeight > 0) { + try { sc.scrollTop = Number(prependedHeight) || 0; } catch (e) {} + } + + // store and mark after waiting for programs (best-effort) + waitForProgramInfo(sc, 4000).then(found => { + try { + sc.dataset.__autoScrollPrependedHeight = String(prependedHeight || 0); + sc.dataset.__autoScrollCloned = '1'; + } catch (e) {} + log('cloneOnce: prepended', leftClones.length, 'and appended', right, 'prependedHeight=' + prependedHeight, 'programsDetected=' + !!found); + resolve(leftClones.length + right); + }).catch(() => { + try { + sc.dataset.__autoScrollPrependedHeight = String(prependedHeight || 0); + sc.dataset.__autoScrollCloned = '1'; + } catch (e) {} + log('cloneOnce: program wait failed; proceeding. prependedHeight=' + prependedHeight); + resolve(leftClones.length + right); + }); + + } catch (e) { + log('cloneOnce error', e); + resolve(0); } + }); + } + + // Dispatch synthetic events to trigger program-updaters that listen for hover/focus. + function refreshProgramInfoForVisible(sc) { + try { + if (!sc) return; + const rows = getOriginalRows(sc); + if (!rows.length) return; + const scRect = sc.getBoundingClientRect(); + let target = rows.find(r => { + const rect = r.getBoundingClientRect(); + return (rect.top >= scRect.top - 2 && rect.top <= scRect.bottom + 2); + }); + if (!target) target = rows[0]; + if (!target) return; + ['mouseenter', 'mouseover', 'focus', 'mousemove'].forEach(type => { + try { target.dispatchEvent(new Event(type, { bubbles: true, cancelable: true })); } catch (e) {} + }); + try { target.dispatchEvent(new PointerEvent('pointerover', { bubbles: true })); } catch (e) {} + log('refreshProgramInfoForVisible: dispatched events on', target.dataset && target.dataset.autoScrollSrcid ? target.dataset.autoScrollSrcid : target); + } catch (e) { log('refreshProgramInfoForVisible error', e); } + } + + // Start the watchdog interval that nudges scrollTop if RAF isn't advancing. + function startWatchdog() { + stopWatchdog(); + watchdogInterval = setInterval(() => { + try { + if (!isScrolling || !scroller) return; + const now = performance.now(); + // If RAF hasn't run in last 250ms, nudge scrollTop a tiny amount + if (now - lastFrameTime > 250) { + try { scroller.scrollTop = (scroller.scrollTop || 0) + scrollSpeed; } catch (e) {} + // update lastFrameTime so we don't double-nudge + lastFrameTime = now; + } + } catch (e) {} + }, 150); + } + function stopWatchdog() { try { if (watchdogInterval) { clearInterval(watchdogInterval); watchdogInterval = null; } } catch (e) {} } - for (let i = 0; i < rightClones; i++) { - const src = rows[i]; - if (!src) break; - const clone = src.cloneNode(true); - markClone(clone); - scrollerEl.appendChild(clone); + // frameLoop: primary RAF animation; updates lastFrameTime on each run + function frameLoop() { + if (!isScrolling) return; + if (document.hidden) { rafId = requestAnimationFrame(frameLoop); return; } + try { + lastFrameTime = performance.now(); + if (scroller && scroller.scrollHeight > scroller.clientHeight) { + scroller.scrollTop += scrollSpeed; + const maxScroll = scroller.scrollHeight - scroller.clientHeight; + if (scroller.scrollTop >= maxScroll - 0.5) { + if (scroller.dataset.__autoScrollCloned === '1') { + // immediate deterministic wrap to prep (avoid race) + const prep = Number(scroller.dataset.__autoScrollPrependedHeight) || 0; + log('wrap detected. cur', scroller.scrollTop, 'max', maxScroll, 'prep', prep); + try { scroller.scrollTop = prep; } catch (e) {} + // small tick then refresh and resume + setTimeout(() => { + try { refreshProgramInfoForVisible(scroller); } catch (e) {} + }, 60); + // continue RAF animation after small delay + setTimeout(() => { if (prefEnabled()) { if (!isScrolling) isScrolling = true; rafId = requestAnimationFrame(frameLoop); } }, 120); + return; + } else { + if (loopMode) { + log('wrap reached but no clones present -> attempting cloneOnce() now'); + cloneOnce(scroller).then(() => { setTimeout(() => { if (prefEnabled()) startDrift(); }, 80); }).catch(() => { scroller.scrollTop = maxScroll; stopDrift('end-reached'); }); + return; + } else { + scroller.scrollTop = maxScroll; + stopDrift('end-reached'); + return; + } + } + } } + } catch (e) {} + rafId = requestAnimationFrame(frameLoop); + } - scrollerEl.dataset.__autoScrollCloned = '1'; - log('cloneRowsForSeamlessLoop: prepended', toPrepend.length, 'and appended', rightClones, '(visibleRows=' + visibleRows + ')'); - } catch (err) { - log('cloneRowsForSeamlessLoop error', err); - } + // Start RAF + watchdog + function startAnimation() { + // avoid double-start + if (isScrolling) return; + isScrolling = true; + lastFrameTime = performance.now(); + if (rafId) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(frameLoop); + startWatchdog(); + log('animation started (RAF + watchdog)'); + } + + function stopAnimation() { + try { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } } catch (e) {} + stopWatchdog(); + isScrolling = false; + log('animation stopped'); } + // start/resume auto-scroll drift (cloning step included) function startDrift() { if (!prefEnabled()) { log('pref disabled - not starting'); return; } + endReached = false; + endReachedAt = 0; + if (!scroller) { - const best = findBestScroller(); - if (!best) { log('no scroller found'); return; } - scroller = best.el; - ensureScrollerStyles(scroller); - // If tvguide1990 is active, apply clones for seamlessness - if (document.body.classList.contains('tvguide1990')) { - cloneRowsForSeamlessLoop(scroller); - } + scroller = findScroller(); + if (!scroller) { log('no scroller found'); return; } + ensureStyles(scroller); } - if (isScrolling) return; - isScrolling = true; - if (rafId) cancelAnimationFrame(rafId); - function frame() { - if (!isScrolling) return; - if (document.hidden) { rafId = requestAnimationFrame(frame); return; } - try { - if (scroller && scroller.scrollHeight > scroller.clientHeight) { - scroller.scrollTop += scrollSpeed; - if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 1) scroller.scrollTop = 0; - } - } catch (e) { /* ignore */ } - rafId = requestAnimationFrame(frame); + + if (loopMode && scroller && scroller.dataset.__autoScrollCloned !== '1') { + waitForContent(scroller, waitForContentMs, contentSampleCount).then(() => cloneOnce(scroller)).catch(() => cloneOnce(scroller)).then(() => { + // start animation after a tiny tick to let layout settle + setTimeout(() => startAnimation(), 60); + }); + return; } - rafId = requestAnimationFrame(frame); - log('started (robust)'); + + // clones already present or no cloning needed + startAnimation(); } function stopDrift(reason) { - if (!isScrolling) { - log('stop called (no-op)', reason || ''); - return; + if (!isScrolling) { log('stop called (no-op)', reason || ''); return; } + stopAnimation(); + if (reason === 'end-reached') { + endReached = true; + endReachedAt = Date.now(); + log('end reached: preventing auto-restart until cleared at', endReachedAt); } - isScrolling = false; - if (rafId) { cancelAnimationFrame(rafId); rafId = null; } - log('stopped', reason || ''); + log('auto-scroll stopped', reason || ''); } - // Only treat deliberate interactions that happen inside the scroller as activity. - function onInsidePointerDown(e) { + function onInsideActivity(e) { try { const tgt = e.target; if (scroller && scroller.contains(tgt)) { lastActivity = Date.now(); - stopDrift('pointerdown-inside'); + stopDrift('interaction-inside'); } - } catch (err) { /* ignore */ } - } - function onInsideTouchStart(e) { - try { - const tgt = e.target; - if (scroller && scroller.contains(tgt)) { - lastActivity = Date.now(); - stopDrift('touchstart-inside'); - } - } catch (err) { /* ignore */ } - } - function onInsideFocusIn(e) { - try { - const tgt = e.target; - if (scroller && scroller.contains(tgt)) { - lastActivity = Date.now(); - stopDrift('focusin-inside'); - } - } catch (err) { /* ignore */ } - } - function onInsideClick(e) { - try { - const tgt = e.target; - if (scroller && scroller.contains(tgt)) { - lastActivity = Date.now(); - stopDrift('click-inside'); - } - } catch (err) { /* ignore */ } + } catch (e) {} } function periodicIdle() { - if (!isScrolling && prefEnabled() && (Date.now() - lastActivity > idleDelay)) startDrift(); + if (endReached && autoRestart) { + const since = Date.now() - endReachedAt; + if (since >= autoRestartDelayMs) { + endReached = false; endReachedAt = 0; + if (!isScrolling && prefEnabled() && (Date.now() - lastActivity > idleDelay)) startDrift(); + return; + } + return; + } + if (!isScrolling && !endReached && prefEnabled() && (Date.now() - lastActivity > idleDelay)) startDrift(); } function attachHandlers() { if (!scroller) return; - scroller.addEventListener('pointerdown', onInsidePointerDown, { passive: true }); - scroller.addEventListener('touchstart', onInsideTouchStart, { passive: true }); - scroller.addEventListener('focusin', onInsideFocusIn); - scroller.addEventListener('click', onInsideClick, { passive: true }); + scroller.addEventListener('pointerdown', onInsideActivity, { passive: true }); + scroller.addEventListener('touchstart', onInsideActivity, { passive: true }); + scroller.addEventListener('focusin', onInsideActivity); + scroller.addEventListener('click', onInsideActivity, { passive: true }); document.addEventListener('visibilitychange', () => { if (!document.hidden && prefEnabled() && !isScrolling && (Date.now() - lastActivity > idleDelay)) startDrift(); }); } - // React to runtime theme changes: if tvguide1990 is added, stop + set pref false (UI will show disabled). - function observeBodyThemeChanges() { - if (!document.body || typeof MutationObserver === 'undefined') return; - if (bodyObserver) return; // already observing - bodyObserver = new MutationObserver(mutations => { - for (const m of mutations) { - if (m.type === 'attributes' && m.attributeName === 'class') { - const hasTV1990 = document.body.classList.contains('tvguide1990'); - if (hasTV1990) { - // user asked that switching into tvguide1990 should stop scrolling - try { - log('theme changed: tvguide1990 detected -> stopping auto-scroll and setting preference=false'); - // persist the user's previous preference so we can restore if desired - window.__autoScroll && (window.__autoScroll._previousPref = prefEnabled()); - setPref(false); - stopDrift('theme-tvguide1990-added'); - // apply clones so visual loop is consistent if later enabled - if (scroller) cloneRowsForSeamlessLoop(scroller); - } catch (e) { log('theme-change handling error', e); } - } else { - // theme removed: if we previously recorded a previousPref restore it - try { - const prev = window.__autoScroll && window.__autoScroll._previousPref; - if (typeof prev !== 'undefined') { - setPref(!!prev); - delete window.__autoScroll._previousPref; - log('theme removed: restored previous preference to', prefEnabled()); - if (prefEnabled()) startDrift(); - } - } catch (e) { log('theme-change restore error', e); } - } - } - } - }); - bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); - log('body theme observer installed'); - } - function init() { - const best = findBestScroller(); - if (best) { - scroller = best.el; - ensureScrollerStyles(scroller); - - // If starting while tvguide1990 is already active: stop and set pref false per requirement. - if (document.body.classList.contains('tvguide1990')) { - log('init: tvguide1990 active on load -> stopping auto-scroll and setting preference=false'); - window.__autoScroll && (window.__autoScroll._previousPref = prefEnabled()); - setPref(false); - stopDrift('theme-tvguide1990-onload'); - cloneRowsForSeamlessLoop(scroller); - } else { - // apply clones only when the theme is tvguide1990; otherwise clones are not applied - } - - attachHandlers(); - } else { - log('no scroller found during init'); - } - - observeBodyThemeChanges(); - + scroller = findScroller(); + if (scroller) { ensureStyles(scroller); attachHandlers(); } else { log('no scroller found during init'); } if (idleInterval) clearInterval(idleInterval); idleInterval = setInterval(periodicIdle, 1000); setTimeout(() => { if (prefEnabled() && (Date.now() - lastActivity > idleDelay)) startDrift(); }, idleDelay); - // stable API window.__autoScroll = window.__autoScroll || {}; window.__autoScroll.start = startDrift; window.__autoScroll.stop = stopDrift; - window.__autoScroll.status = () => isScrolling; - window.__autoScroll.pref = prefEnabled; - window.__autoScroll.scrollers = () => scroller ? [scroller] : []; - window.__autoScroll.recompute = () => { scroller = null; }; + window.__autoScroll.enable = function(){ setPref(true); lastActivity = Date.now(); endReached = false; startDrift(); }; + window.__autoScroll.disable = function(){ setPref(false); stopDrift('disabled-via-api'); }; + window.__autoScroll.setLoop = function(on){ loopMode = !!on; log('setLoop ->', loopMode); }; + window.__autoScroll.getLoop = function(){ return !!loopMode; }; + window.__autoScroll.setAutoRestart = function(enabled, delayMs){ autoRestart = !!enabled; if (typeof delayMs === 'number') autoRestartDelayMs = Number(delayMs); }; + window.__autoScroll.clearEnd = function(){ endReached = false; endReachedAt = 0; }; + window.__autoScroll.recompute = function(){ scroller = null; }; + window.__autoScroll.cloneNow = function(){ if (!scroller) scroller = findScroller(); return cloneOnce(scroller); }; + window.__autoScroll.status = function(){ return { isScrolling, pref: prefEnabled(), loopMode, scrollerInfo: scroller ? { id: scroller.id, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, cloned: !!scroller.dataset.__autoScrollCloned, prependedHeight: scroller.dataset.__autoScrollPrependedHeight } : null, rafId: !!rafId, watchdog: !!watchdogInterval }; }; + window.__autoScroll.debug = function(){ return { lastActivity, idleDelay, scrollSpeed, isScrolling, pref: prefEnabled(), loopMode, endReached, endReachedAt, autoRestart, autoRestartDelayMs, scrollerInfo: scroller ? { id: scroller.id, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, cloned: !!scroller.dataset.__autoScrollCloned, prependedHeight: scroller.dataset.__autoScrollPrependedHeight } : null, rafId, lastFrameTime, watchdogInterval }; }; - window.__autoScroll.enable = function() { - try { - setPref(true); - lastActivity = Date.now(); - startDrift(); - log('enabled via API'); - } catch (e) { log('enable error', e); } - }; - window.__autoScroll.disable = function() { - try { - setPref(false); - stopDrift('disabled-via-api'); - log('disabled via API'); - } catch (e) { log('disable error', e); } - }; - window.__autoScroll.toggle = function() { - try { - if (prefEnabled()) { window.__autoScroll.disable(); } else { window.__autoScroll.enable(); } - return prefEnabled(); - } catch (e) { log('toggle error', e); return prefEnabled(); } - }; - window.__autoScroll.debug = function() { - return { lastActivity, idleDelay, scrollSpeed, isScrolling, pref: prefEnabled(), scrollerInfo: scroller ? {id: scroller.id, scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight} : null }; - }; - - try { Object.defineProperty(window.__autoScroll, 'prefValue', { get: prefEnabled, configurable: true }); } catch (e) {} - log('minimal initialized'); + log('auto-scroll (conservative v36.3) initialized'); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); -})(); +})(); \ No newline at end of file From 1c97b7cdd44d9c9f6104f91b41331b2eedb70f39 Mon Sep 17 00:00:00 2001 From: thehack904 <35552907+thehack904@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:42:28 -0500 Subject: [PATCH 6/6] v4.1.0 updates --- CHANGELOG.md | 50 +++++++++++++++---- INSTALL.md | 4 +- README.md | 6 +-- ROADMAP.md | 108 ++++++++++++++++++++---------------------- app.py | 4 +- retroiptv_linux.sh | 2 +- retroiptv_rpi.sh | 2 +- retroiptv_windows.ps1 | 4 +- 8 files changed, 102 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ac3520..b506a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,47 @@ All notable changes to this project will be documented here. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project follows [Semantic Versioning](https://semver.org/). +--- + +## v4.1.0 - 2025-10-25 +### โœจ New Features +- **Auto-Scroll Guide System** + - Added `static/js/auto-scroll.js` enabling smooth, continuous automatic scrolling of the live TV guide. + - Uses `requestAnimationFrame` with a `setInterval` watchdog fallback for consistent performance. + - Deterministic wraparound ensures seamless looping without scroll jitter. + - Waits up to 5 seconds for guide data to populate before activating. + - Stores preference in localStorage (`autoScrollEnabled`) and exposes simple APIs (`cloneNow`, `status`). + +- **Per-Page Modular CSS** + - Introduced separate per-page stylesheets: `about.css`, `change_password.css`, `change_tuner.css`, `logs.css`, and `manage_users.css`. + - Shared global styling moved to `base.css` for consistency. + +- **Unified Template Structure** + - New `base.html` and `_header.html` templates consolidate common layout and navigation. + - All major pages now extend from `base.html` for easier maintenance. + +- **New JavaScript Modules** + - Added `tuner-settings.js` for handling tuner selection and dynamic UI updates. + +### ๐Ÿงฐ Improvements +- Updated `INSTALL.md`, `README.md`, and `ROADMAP.md` to document the new layout and structure. +- `app.py` updated to serve new static assets and integrate template inheritance. +- All installer scripts (`retroiptv_linux.sh`, `retroiptv_rpi.sh`, `retroiptv_windows.ps1`) updated for v4.1.0 compatibility and new folder paths. + +### ๐Ÿž Fixes +- Reduced redundancy across templates by introducing a unified base layout. +- Improved guide performance and browser compatibility with the new auto-scroll implementation. +- Minor visual and layout corrections across settings and guide pages. + +--- + +## [Unreleased] + +- Planned: add `.m3u8` tuner support. +- Planned: move logs to SQLite DB. +- Planned: log filtering and pagination. +--- ## v4.0.0 โ€” 2025-10-19 **Status:** Public Release (Feature Complete) @@ -34,16 +74,6 @@ This project follows [Semantic Versioning](https://semver.org/). - PlutoTV / custom tuner aggregation features - Enhanced guide refresh logic for long-running sessions ---- - -## [Unreleased] - -- Planned: add `.m3u8` tuner support. -- Planned: move logs to SQLite DB. -- Planned: log filtering and pagination. - ---- - ## v3.3.0 - 2025-10-15 ### Added diff --git a/INSTALL.md b/INSTALL.md index b67c31d..421f9a2 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,7 +1,7 @@ # Installation Guide -**Version:** v4.0.0 -**Last Updated:** 2025-10-19 +**Version:** v4.1.0 +**Last Updated:** 2025-10-25 --- diff --git a/README.md b/README.md index cb62500..1a59837 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# ๐Ÿ“บ RetroIPTVGuide v4.0.0 +# ๐Ÿ“บ RetroIPTVGuide v4.1.0

    - Version + Version GHCR @@ -59,7 +59,7 @@ docker run -d -p 5000:5000 ghcr.io/thehack904/retroiptvguide:latest ``` ### ๐Ÿงฉ TrueNAS SCALE App -- Upload the provided `retroiptvguide-3.2.0.zip` chart. +- Upload the provided `retroiptvguide-4.1.0.zip` chart. - Repository: `ghcr.io/thehack904/retroiptvguide` - Tag: `latest` - Exposes port `5000`. diff --git a/ROADMAP.md b/ROADMAP.md index 1ec7f0b..8eca213 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,9 +4,8 @@ This document tracks **planned upgrades** and ideas for improving the IPTV Flask These are **not yet implemented**, but provide a development path for future releases. --- -# Current Version: v4.0.0 (2025-10-19) -The 4.0.0 release merges all Testing branch updates into Main, introducing unified installers, new UI templates, and Android TV optimizations. - +# Current Version: **v4.1.0 (2025-10-25)** +This version refines templates and adds an auto scroll feature w/ and enable/disable feature. This also has background improvements to align HTML/CSS templates. --- ## ๐Ÿ”ฎ Feature Upgrades @@ -15,114 +14,109 @@ The 4.0.0 release merges all Testing branch updates into Main, introducing unifi - [x] Add ability to **add/remove tuners** from the UI (v2.3.0). - [x] Add ability to rename tuners via the UI (v2.3.0). - [ ] Support for **.m3u8 single-channel playlists** as tuner sources (planned v3.2.0). - - Option A: Special-case `.m3u8` handling in parser. - - Option B: Add explicit `hls` column to `tuners.db`. - [x] Validate tuner URLs (ping/check format before saving) (v2.0.0). - [ ] Optional auto-refresh of tuner lineup on a schedule. -- [ ] Add per-user tuner assignment and default tuner preferences. ๐Ÿ†• *(v4.1.x planned)* +- [ ] Add per-user tuner assignment and default tuner preferences. ๐Ÿ†• *(v4.2.x planned)* - [ ] Introduce combined tuner builder (custom tuner aggregation). ๐Ÿ†• *(v5.x.x planned)* --- ### 2. Logging & Monitoring - [ ] Move logs from flat file (`activity.log`) into **SQLite DB** for better querying. -- [ ] Add filtering and pagination in logs view (by user, action, or date). -- [ ] Add system health checks (e.g., tuner reachability, XMLTV freshness) to logs. -- [x] **Admin log management**: add button/route to clear logs (with confirmation) (v2.3.1). -- [x] Display log file size on logs page (v2.3.1). -- [x] Post-install HTTP service verification added in Pi installer (v3.1.0). -- [ ] Add unified โ€œRefresh Guideโ€ scheduler (configurable intervals). ๐Ÿ†• *(v4.2.x planned)* +- [ ] Add filtering and pagination in logs view. +- [ ] Add system health checks (tuner reachability, XMLTV freshness). +- [x] **Admin log management**: clear logs + file size indicator (v2.3.1). +- [x] Post-install HTTP service verification in Pi installer (v3.1.0). +- [ ] Unified โ€œRefresh Guideโ€ scheduler. ๐Ÿ†• *(v4.2.x planned)* --- ### 3. Guide & Playback -- [ ] Add **search/filter box** to guide for channels/programs. -- [ ] Add ability to set **favorites** for quick channel access. -- [x] Add fallback message (โ€œNo Guide Data Availableโ€) for channels missing EPG info (v3.0.1). -- [ ] Add **reminders/notifications** for upcoming programs. +- [x] **Auto-Scroll feature** added for the Live Guide (v4.1.0). + - Uses `requestAnimationFrame` for smooth scroll with fallback watchdog. + - Deterministic looping and localStorage preference tracking. +- [ ] Add search/filter box to guide. +- [ ] Add ability to set favorites. +- [x] Fallback message for missing EPG info (v3.0.1). +- [ ] Add reminders/notifications for upcoming programs. - [ ] Add EPG caching for faster guide reloads. ๐Ÿ†• *(v5.x.x planned)* --- ### 4. User Management - [x] Add **manage_users.html** for integrated user control panel. โœ… *(v4.0.0)* -- [ ] Add role-based access control (admin, regular user, read-only). -- [ ] Add **email or 2FA support** for login (optional). +- [ ] Role-based access control (admin/user/read-only). +- [ ] Add email or 2FA support for login. - [ ] Show last login time in admin panel. -- [ ] Enhance user management (roles, channel restrictions). ๐Ÿ†• *(v5.x.x planned)* +- [ ] User role/channel restrictions. ๐Ÿ†• *(v5.x.x planned)* --- ### 5. UI/UX Improvements -- [x] Unified theming across all templates (Light, Dark, AOL/CompuServe, TV Guide Magazine) (v2.3.2). +- [x] Unified theming across all templates (v2.3.2). - [x] Android / Fire / Google TV optimized mode with CRT glow header. โœ… *(v4.0.0)* -- [x] Consolidated and modernized UI templates (`guide.html`, `login.html`, `about.html`, `logs.html`, etc.). โœ… *(v4.0.0)* -- [ ] Unify CSS across all templates (minimize inline styles). -- [ ] Make guide responsive (mobile/tablet view). -- [ ] Add dark/light theme auto-detect from browser/system. +- [x] Consolidated UI templates (`guide.html`, `login.html`, etc.). โœ… *(v4.0.0)* +- [x] **Refactored UI templates into shared `base.html` and `_header.html` (v4.1.0)**. +- [x] **Modular CSS and JS added (v4.1.0)** โ€“ per-page styling and script loading. +- [x] Introduced new JS modules: `auto-scroll.js`, `tuner-settings.js`. +- [ ] Make guide responsive (mobile/tablet). +- [ ] Add dark/light theme auto-detect. - [ ] Frozen header timeline to prevent scrolling with channel listing. - [x] About page under Settings menu (v2.3.1). --- -### 6. Cross-platform -- [x] Create installable container. +### 6. Cross-Platform - [x] Unified Linux, Windows, and Raspberry Pi installers. โœ… *(v4.0.0)* -- [x] Windows installer via PowerShell + NSSM service (v3.0.0). -- [x] Pi installer auto-configures GPU and verifies HTTP service (v3.1.0). -- [x] Add **Windows update/uninstall parity planned**. ๐Ÿ†• *(v4.1.x target)* +- [x] Windows update/uninstall parity implemented. โœ… *(v4.1.0)* - [ ] Create MacOS install/executable. -- [x] Validate/test installers fully on all Windows environments. +- [x] Validate/test installers on all Windows environments. - [ ] Explore TrueNAS SCALE App Catalog certification. ๐Ÿ†• *(v5.x.x planned)* --- ### 7. New Features -- [ ] Add the ability to have an **auto-play video stream** upon login (ErsatzTV source). -- [ ] Option to play a known or unlisted channel as default auto-play source. +- [ ] Add auto-play stream on login (ErsatzTV integration). +- [ ] Default auto-play source selection. - [ ] Begin integration path for **PlutoTV / external IPTV services**. ๐Ÿ†• *(v5.x.x)* --- ### 8. Planned Enhancements -- [ ] Add **safety checks** in `add_tuner()`: - - Prevent inserting duplicate tuner names. - - Validate XML/M3U URLs before commit. -- [x] Add **GPU verification** after `raspi-config` call (v3.1.0). -- [x] Suppress `rfkill` Wi-Fi message during GPU configuration (v3.1.0). -- [x] Post-install adaptive HTTP check loop (15s poll) (v3.1.0). -- [x] Reorganized project structure and documentation. โœ… *(v4.0.0)* +- [ ] Add safety checks in `add_tuner()` (duplicate prevention + URL validation). +- [x] GPU verification after `raspi-config` (v3.1.0). +- [x] Suppress `rfkill` Wi-Fi message during GPU config (v3.1.0). +- [x] Adaptive HTTP check loop (v3.1.0). +- [x] **Project structure and documentation reorganized** โœ… *(v4.0.0โ€“4.1.0)* --- ## โš™๏ธ Technical Improvements - [x] Add uninstall.sh (v2.3.0). -- [ ] Validate/test uninstall script fully on Windows environments. +- [ ] Validate/test uninstall script fully on Windows. - [ ] Add HTTPS + optional token-based authentication. ๐Ÿ†• *(v4.5.x)* -- [x] Refactor tuner handling for unified DB structure. โœ… *(v4.0.0)* +- [x] Refactor tuner handling for unified DB. โœ… *(v4.0.0)* +- [x] **Updated bump_version and installer scripts to auto-track new version (v4.1.0)** +- [x] Containerize app (Dockerfile + Compose). - [ ] Add migrations for DB schema changes. -- [x] Containerize app (Dockerfile + Compose for deployment). -- [x] Automated version bump tool updates all key scripts (v3.1.0). -- [ ] Add CI/CD automation for official .deb and .zip builds. ๐Ÿ†• *(v5.x.x)* +- [ ] CI/CD automation for official builds. ๐Ÿ†• *(v5.x.x)* - [ ] Add test suite for tuner parsing, authentication, and logging. --- ## ๐Ÿ“ Installer Enhancements -- [x] Unified Linux/Windows/RPi installer architecture. โœ… *(v4.0.0)* -- [ ] Add interactive mode selector (Kiosk vs Headless). -- [ ] Add command-line flag `--mode kiosk` for non-interactive installs. -- [ ] Ensure logs/services properly isolated between modes. +- [x] Unified installer architecture. โœ… *(v4.0.0)* +- [x] Windows update/uninstall parity complete. โœ… *(v4.1.0)* +- [ ] Add kiosk/headless mode selector. +- [ ] Add `--mode kiosk` flag for non-interactive installs. - [ ] Validate update/uninstall paths on all OSes. --- -## โœ… Completed (v4.0.0) -- [x] Unified cross-platform installers (`retroiptv_linux.sh`, `retroiptv_windows.ps1`, `retroiptv_rpi.sh`) -- [x] Android/Fire/Google TV mode added with animated CRT glow -- [x] Added `manage_users.html` for full web-based user management -- [x] Modernized `guide.html`, `login.html`, `about.html`, `logs.html`, etc. -- [x] Refactored `app.py` for unified configuration + session logic -- [x] Removed legacy installers (`install.*`, `uninstall.*`, `iptv-server.service`) -- [x] Reorganized documentation (CHANGELOG, README, ROADMAP) -- [x] Release tagged as **v4.0.0** +## โœ… Completed (v4.1.0) +- [x] Modular CSS/JS introduced. +- [x] Base templating system (`base.html`, `_header.html`) added. +- [x] Auto-scroll feature integrated with toggle memory. +- [x] Updated documentation (CHANGELOG, README, INSTALL, ROADMAP). +- [x] Windows installer parity update. +- [x] Release tagged as **v4.1.0** diff --git a/app.py b/app.py index d40cea6..1f1b150 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ -APP_VERSION = "v4.0.0" -APP_RELEASE_DATE = "2025-10-11" +APP_VERSION = "v4.1.0" +APP_RELEASE_DATE = "2025-10-25" from flask import Flask, render_template, request, redirect, url_for, flash, session from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user diff --git a/retroiptv_linux.sh b/retroiptv_linux.sh index cd38282..16eb72a 100644 --- a/retroiptv_linux.sh +++ b/retroiptv_linux.sh @@ -3,7 +3,7 @@ # License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) set -euo pipefail -VERSION="4.0.0" +VERSION="4.1.0" TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") LOGFILE="retroiptv_${TIMESTAMP}.log" exec > >(tee -a "$LOGFILE") 2>&1 diff --git a/retroiptv_rpi.sh b/retroiptv_rpi.sh index 6546bd3..2a57222 100644 --- a/retroiptv_rpi.sh +++ b/retroiptv_rpi.sh @@ -1,5 +1,5 @@ #!/bin/bash -VERSION="4.0.0" +VERSION="4.1.0" # RetroIPTVGuide Raspberry Pi Installer (Headless, Pi3/4/5) # Installs to /home/iptv/iptv-server for consistency with Debian/Windows # Logs to /var/log/retroiptvguide/install-YYYYMMDD-HHMMSS.log diff --git a/retroiptv_windows.ps1 b/retroiptv_windows.ps1 index 4613cfe..c99bf7c 100644 --- a/retroiptv_windows.ps1 +++ b/retroiptv_windows.ps1 @@ -1,7 +1,7 @@ <# RetroIPTVGuide Windows Installer/Uninstaller Filename: retroiptv_windows.ps1 -Version: 4.0.0 +Version: 4.1.0 License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International https://creativecommons.org/licenses/by-nc-sa/4.0/ @@ -55,7 +55,7 @@ if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdent $ErrorActionPreference = 'Stop' $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' -$VERSION = "4.0.0" +$VERSION = "4.1.0" $ScriptDir = Split-Path -Parent -Path $MyInvocation.MyCommand.Path Set-Location $ScriptDir