Fix now-line alignment and visibility
This commit is contained in:
@@ -256,27 +256,34 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
|
|||||||
|
|
||||||
.timeline-area {
|
.timeline-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border-bright) transparent;
|
||||||
|
}
|
||||||
|
.timeline-area::-webkit-scrollbar { height: 4px; width: 4px; }
|
||||||
|
.timeline-area::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.timeline-area::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
|
||||||
|
|
||||||
|
/* Conteneur unique — ruler + programmes scrollent ensemble */
|
||||||
|
.timeline-inner {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-ruler {
|
.time-ruler {
|
||||||
height: var(--header-h);
|
height: var(--header-h);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-bottom: 1px solid var(--border-bright);
|
border-bottom: 1px solid var(--border-bright);
|
||||||
position: relative;
|
position: sticky;
|
||||||
flex-shrink: 0;
|
top: 0;
|
||||||
overflow: hidden;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
.time-ruler-inner {
|
.time-ruler-inner {
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 0; left: 0;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
.time-tick {
|
.time-tick {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -299,8 +306,9 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
|
|||||||
|
|
||||||
.now-line {
|
.now-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; bottom: 0;
|
top: 0;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
background: var(--accent-live);
|
background: var(--accent-live);
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -314,25 +322,14 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.programs-scroll {
|
.programs-inner {
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--border-bright) transparent;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 0;
|
|
||||||
background-image: repeating-linear-gradient(
|
background-image: repeating-linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
transparent 0px, transparent 79px,
|
transparent 0px, transparent 79px,
|
||||||
var(--border) 79px, var(--border) 80px
|
var(--border) 79px, var(--border) 80px
|
||||||
);
|
);
|
||||||
background-attachment: local;
|
|
||||||
}
|
}
|
||||||
.programs-scroll::-webkit-scrollbar { height: 4px; width: 4px; }
|
|
||||||
.programs-scroll::-webkit-scrollbar-track { background: transparent; }
|
|
||||||
.programs-scroll::-webkit-scrollbar-thumb { background: var(--border-bright); border-radius: 2px; }
|
|
||||||
|
|
||||||
.programs-inner { position: relative; }
|
|
||||||
|
|
||||||
.program-row {
|
.program-row {
|
||||||
height: var(--row-h);
|
height: var(--row-h);
|
||||||
@@ -655,14 +652,11 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
|
|||||||
<div class="channels-list" id="channelsList"></div>
|
<div class="channels-list" id="channelsList"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-area" id="timelineArea">
|
<div class="timeline-area" id="timelineArea">
|
||||||
<div class="time-ruler" id="timeRuler">
|
<div class="timeline-inner" id="timelineInner">
|
||||||
<div class="time-ruler-inner" id="timeRulerInner"></div>
|
<div class="time-ruler" id="timeRuler">
|
||||||
<div class="now-line" id="nowLineRuler"></div>
|
<div class="time-ruler-inner" id="timeRulerInner"></div>
|
||||||
</div>
|
|
||||||
<div class="programs-scroll" id="programsScroll">
|
|
||||||
<div class="now-line" id="nowLineGrid"></div>
|
|
||||||
<div class="programs-inner" id="programsInner">
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="programs-inner" id="programsInner"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,15 +668,21 @@ $m3u_url = htmlspecialchars($config["m3u_url"] ?? "");
|
|||||||
const DEFAULT_EPG_URL = '<?= $epg_url ?>';
|
const DEFAULT_EPG_URL = '<?= $epg_url ?>';
|
||||||
const DEFAULT_M3U_URL = '<?= $m3u_url ?>';
|
const DEFAULT_M3U_URL = '<?= $m3u_url ?>';
|
||||||
const PX_PER_MIN = 5;
|
const PX_PER_MIN = 5;
|
||||||
const HOURS_BEFORE = 1;
|
const GRID_HOURS = 72;
|
||||||
const HOURS_AFTER = 3;
|
|
||||||
const CORS_PROXY = 'https://corsproxy.io/?';
|
const CORS_PROXY = 'https://corsproxy.io/?';
|
||||||
|
|
||||||
|
// Ancre : 2h avant maintenant, arrondi au quart d'heure inférieur
|
||||||
|
const _d = new Date();
|
||||||
|
_d.setMinutes(Math.floor(_d.getMinutes()/15)*15, 0, 0);
|
||||||
|
const GRID_START = new Date(_d.getTime() - 2*3600000);
|
||||||
|
const GRID_END = new Date(GRID_START.getTime() + GRID_HOURS*3600000);
|
||||||
|
const TOTAL_WIDTH_PX = GRID_HOURS * 60 * PX_PER_MIN;
|
||||||
|
|
||||||
|
function msToX(ms) { return (ms - GRID_START.getTime()) / 60000 * PX_PER_MIN; }
|
||||||
|
|
||||||
let channels = [];
|
let channels = [];
|
||||||
let programs = {};
|
let programs = {};
|
||||||
let viewOffsetMin = 0;
|
let scrollListenerAdded = false; // sync scroll vertical chaînes
|
||||||
let totalWidthPx = 0;
|
|
||||||
let scrollListenerAdded = false;
|
|
||||||
|
|
||||||
function pad(n) { return String(n).padStart(2,'0'); }
|
function pad(n) { return String(n).padStart(2,'0'); }
|
||||||
|
|
||||||
@@ -809,18 +809,12 @@ function parseAndRender(doc) {
|
|||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
if (isMobile()) { renderMobile(); }
|
if (isMobile()) { renderMobile(); }
|
||||||
else { renderGrid(); setTimeout(centerNow, 50); }
|
else { renderGrid(); setTimeout(centerNow, 150); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── GRID ──────────────────────────────────────────────────────────────────────
|
// ── GRID ──────────────────────────────────────────────────────────────────────
|
||||||
function getViewStart() { return new Date(Date.now() - HOURS_BEFORE*3600000 + viewOffsetMin*60000); }
|
|
||||||
function getViewEnd() { return new Date(Date.now() + HOURS_AFTER*3600000 + viewOffsetMin*60000); }
|
|
||||||
function msToX(ms) { return (ms - getViewStart().getTime()) / 60000 * PX_PER_MIN; }
|
|
||||||
|
|
||||||
function renderGrid() {
|
function renderGrid() {
|
||||||
const vs = getViewStart(), ve = getViewEnd();
|
renderChannels(); renderRuler(); renderPrograms(); positionNowLine();
|
||||||
totalWidthPx = (ve - vs) / 60000 * PX_PER_MIN;
|
|
||||||
renderChannels(); renderRuler(vs, ve); renderPrograms(vs, ve); positionNowLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlaceholder(name) {
|
function makePlaceholder(name) {
|
||||||
@@ -849,12 +843,13 @@ function renderChannels() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRuler(vs, ve) {
|
function renderRuler() {
|
||||||
const ruler = document.getElementById('timeRulerInner');
|
const ruler = document.getElementById('timeRulerInner');
|
||||||
ruler.innerHTML = ''; ruler.style.width = totalWidthPx + 'px';
|
ruler.innerHTML = ''; ruler.style.width = TOTAL_WIDTH_PX + 'px';
|
||||||
let t = new Date(vs); t.setSeconds(0,0);
|
document.getElementById('timeRuler').style.width = TOTAL_WIDTH_PX + 'px';
|
||||||
|
let t = new Date(GRID_START); t.setSeconds(0,0);
|
||||||
const rem = t.getMinutes()%15; if (rem) t.setMinutes(t.getMinutes()+(15-rem));
|
const rem = t.getMinutes()%15; if (rem) t.setMinutes(t.getMinutes()+(15-rem));
|
||||||
while (t <= ve) {
|
while (t <= GRID_END) {
|
||||||
const x = msToX(t.getTime()); const isHour = t.getMinutes()===0;
|
const x = msToX(t.getTime()); const isHour = t.getMinutes()===0;
|
||||||
const tick = document.createElement('div'); tick.className='time-tick'; tick.style.left=x+'px';
|
const tick = document.createElement('div'); tick.className='time-tick'; tick.style.left=x+'px';
|
||||||
const lbl = document.createElement('div'); lbl.className='time-tick-label'+(isHour?' hour':''); lbl.textContent=fmtTime(t);
|
const lbl = document.createElement('div'); lbl.className='time-tick-label'+(isHour?' hour':''); lbl.textContent=fmtTime(t);
|
||||||
@@ -864,10 +859,19 @@ function renderRuler(vs, ve) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPrograms(vs, ve) {
|
function renderPrograms() {
|
||||||
const ROW_H = 80;
|
const ROW_H = 80;
|
||||||
const inner = document.getElementById('programsInner');
|
const inner = document.getElementById('programsInner');
|
||||||
inner.innerHTML = ''; inner.style.width = totalWidthPx+'px'; inner.style.height = (channels.length*ROW_H)+'px';
|
const totalH = channels.length * ROW_H;
|
||||||
|
inner.innerHTML = '';
|
||||||
|
inner.style.width = TOTAL_WIDTH_PX+'px';
|
||||||
|
inner.style.height = totalH+'px';
|
||||||
|
|
||||||
|
// Remettre la now-line (détruite par innerHTML='')
|
||||||
|
const nowLine = document.createElement('div');
|
||||||
|
nowLine.className = 'now-line'; nowLine.id = 'nowLine';
|
||||||
|
nowLine.style.height = totalH + 'px';
|
||||||
|
inner.appendChild(nowLine);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
@@ -876,9 +880,9 @@ function renderPrograms(vs, ve) {
|
|||||||
row.className = 'program-row';
|
row.className = 'program-row';
|
||||||
row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`;
|
row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`;
|
||||||
|
|
||||||
(programs[ch.id]||[]).filter(p => p.stop>vs && p.start<ve).forEach(p => {
|
(programs[ch.id]||[]).filter(p => p.stop>GRID_START && p.start<GRID_END).forEach(p => {
|
||||||
const x = Math.max(0, msToX(p.start.getTime()));
|
const x = Math.max(0, msToX(p.start.getTime()));
|
||||||
const w = Math.min(totalWidthPx, msToX(p.stop.getTime())) - x;
|
const w = Math.min(TOTAL_WIDTH_PX, msToX(p.stop.getTime())) - x;
|
||||||
if (w < 2) return;
|
if (w < 2) return;
|
||||||
const block = document.createElement('div'); block.className='program-block';
|
const block = document.createElement('div'); block.className='program-block';
|
||||||
if (p.stop < now) block.classList.add('is-past');
|
if (p.stop < now) block.classList.add('is-past');
|
||||||
@@ -905,11 +909,7 @@ function renderPrograms(vs, ve) {
|
|||||||
positionNowLine();
|
positionNowLine();
|
||||||
if (!scrollListenerAdded) {
|
if (!scrollListenerAdded) {
|
||||||
scrollListenerAdded = true;
|
scrollListenerAdded = true;
|
||||||
document.getElementById('programsScroll').addEventListener('scroll', function() {
|
document.getElementById('timelineArea').addEventListener('scroll', function() {
|
||||||
document.getElementById('timeRulerInner').style.transform = `translateX(-${this.scrollLeft}px)`;
|
|
||||||
const visibleX = msToX(Date.now()) - this.scrollLeft;
|
|
||||||
document.getElementById('nowLineRuler').style.left = visibleX + 'px';
|
|
||||||
document.getElementById('nowLineGrid').style.left = visibleX + 'px';
|
|
||||||
document.getElementById('channelsList').scrollTop = this.scrollTop;
|
document.getElementById('channelsList').scrollTop = this.scrollTop;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -917,26 +917,19 @@ function renderPrograms(vs, ve) {
|
|||||||
|
|
||||||
function positionNowLine() {
|
function positionNowLine() {
|
||||||
const x = msToX(Date.now());
|
const x = msToX(Date.now());
|
||||||
const scroll = document.getElementById('programsScroll');
|
const line = document.getElementById('nowLine');
|
||||||
const scrollLeft = scroll ? scroll.scrollLeft : 0;
|
if (line) line.style.left = x + 'px';
|
||||||
const ruler = document.getElementById('nowLineRuler');
|
|
||||||
const grid = document.getElementById('nowLineGrid');
|
|
||||||
const visibleX = x - scrollLeft;
|
|
||||||
if (ruler) { ruler.style.left = visibleX + 'px'; ruler.style.top='0'; ruler.style.bottom='0'; }
|
|
||||||
if (grid) { grid.style.left = visibleX + 'px'; grid.style.top='0'; grid.style.height='100%'; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function centerNow() {
|
function centerNow() {
|
||||||
viewOffsetMin = 0; renderGrid();
|
const area = document.getElementById('timelineArea');
|
||||||
setTimeout(() => {
|
area.scrollLeft = Math.max(0, msToX(Date.now()) - area.offsetWidth * 0.3);
|
||||||
const scroll = document.getElementById('programsScroll');
|
|
||||||
scroll.scrollLeft = Math.max(0, msToX(Date.now()) - document.getElementById('timelineArea').offsetWidth*0.3);
|
|
||||||
}, 50);
|
|
||||||
document.querySelectorAll('.nav-btn').forEach((b,i) => b.classList.toggle('active', i===1));
|
document.querySelectorAll('.nav-btn').forEach((b,i) => b.classList.toggle('active', i===1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function shiftView(mins) {
|
function shiftView(mins) {
|
||||||
viewOffsetMin += mins; renderGrid();
|
const area = document.getElementById('timelineArea');
|
||||||
|
area.scrollLeft = Math.max(0, area.scrollLeft + mins * PX_PER_MIN);
|
||||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user