From 1b33337c896af1ec576b670350c896c45240d03a Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 14 Oct 2026 20:56:09 +0700 Subject: [PATCH 1/8] feat(schedule): proportional ICT hourly timeline layout through next midnight --- utils/station-schedule.mjs | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/utils/station-schedule.mjs b/utils/station-schedule.mjs index 99c5ef54..afb163a2 100644 --- a/utils/station-schedule.mjs +++ b/utils/station-schedule.mjs @@ -86,3 +86,51 @@ export const scheduleTimelineProgress = (startSeconds, endSeconds, nowSeconds) = || !Number.isFinite(nowSeconds) || endSeconds <= startSeconds) return null; return Math.min(100, Math.max(0, (nowSeconds - startSeconds) * 100 / (endSeconds - startSeconds))); }; + +// Fixed-scale broadcast timeline. Unix timestamps stay in Vietnam's ICT zone: +// no browser-local timezone or daylight-saving conversion can move a programme. +export const SCHEDULE_TIMELINE_HOUR_PX = 76; +export const referenceTimelineLayout = (programs, nowSeconds, hourPixels = SCHEDULE_TIMELINE_HOUR_PX) => { + if (!Array.isArray(programs) || !programs.length || !Number.isFinite(nowSeconds) + || !Number.isFinite(hourPixels) || hourPixels <= 0) return null; + const sorted = programs.filter(item => + Number.isFinite(item?.startTime) && Number.isFinite(item?.endTime) + && item.endTime > item.startTime + ).slice().sort((a, b) => a.startTime - b.startTime); + if (!sorted.length) return null; + const vietnamOffset = 7 * 3600; + const dayStart = Math.floor((sorted[0].startTime + vietnamOffset) / 86400) * 86400 - vietnamOffset; + // Display a continuous hourly ruler through 03:00 of the following day. + // Keep longer source events visible without clipping them at midnight. + const lastEnd = Math.max(...sorted.map(item => item.endTime)); + const hourCount = Math.max(27, Math.ceil((lastEnd - dayStart) / 3600) + 2); + const timeTop = seconds => (seconds - dayStart) / 3600 * hourPixels; + const programsLayout = sorted.map((item, index) => ({ + index, + item, + top: timeTop(item.startTime), + height: Math.max(1, timeTop(item.endTime) - timeTop(item.startTime)), + active: item.startTime <= nowSeconds && nowSeconds < item.endTime, + })); + const gap = referenceScheduleGap(sorted, nowSeconds); + const previous = gap?.insertIndex > 0 ? sorted[gap.insertIndex - 1] : null; + const next = gap && gap.insertIndex < sorted.length ? sorted[gap.insertIndex] : null; + return { + dayStart, + height: hourCount * hourPixels, + hours: Array.from({ length: hourCount + 1 }, (_, index) => ({ + time: dayStart + index * 3600, + top: index * hourPixels, + nextDay: index > 0 && index % 24 === 0, + })), + programmes: programsLayout, + nowTop: nowSeconds >= dayStart && nowSeconds <= dayStart + hourCount * 3600 + ? timeTop(nowSeconds) : null, + // An unknown interval is neutral metadata, never a live station status. + gap: gap ? { + ...gap, + startTime: previous?.endTime ?? dayStart, + endTime: next?.startTime ?? dayStart + hourCount * 3600, + } : null, + }; +}; From fd1b87abe77e3e0cd36dfc63f772de220a0fefbd Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 14 Oct 2026 20:57:47 +0700 Subject: [PATCH 2/8] feat(ui): hourly vertical EPG grid with duration-proportional programme blocks and moving green NOW line --- components/player/ScheduleTimeline.vue | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 components/player/ScheduleTimeline.vue diff --git a/components/player/ScheduleTimeline.vue b/components/player/ScheduleTimeline.vue new file mode 100644 index 00000000..faccc796 --- /dev/null +++ b/components/player/ScheduleTimeline.vue @@ -0,0 +1,109 @@ + + + From c34aa9d2b3ae3a8d9c1b393bbf236c82f511b7c8 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 14 Oct 2026 20:58:38 +0700 Subject: [PATCH 3/8] feat(queue): render time-scaled vertical programme grid for Vietnamese reference schedules and center real-time clock --- components/player/Queue.vue | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/components/player/Queue.vue b/components/player/Queue.vue index 8911a0ef..15b79941 100644 --- a/components/player/Queue.vue +++ b/components/player/Queue.vue @@ -145,8 +145,18 @@ -
-