1const mobileSchedule = document.querySelector(".schedule-mobile"); 2 3// Footer padding 4const updateMobileScheduleHeight = () => { 5 document.documentElement.style.setProperty( 6 "--schedule-mobile-height", 7 `${mobileSchedule.offsetHeight}px`, 8 ); 9}; 10const scheduleResizeObserver = new ResizeObserver(() => { 11 updateMobileScheduleHeight(); 12}); 13scheduleResizeObserver.observe(mobileSchedule); 14updateMobileScheduleHeight(); 15 16// Upcoming program (all broadcast times are in Taiwan time). 17const scheduleBlocks = document.querySelectorAll(".schedule"); 18const scheduleWeekdays = ["æ¥", "ä¸", "äº", "ä¸", "å", "äº", "å "]; 19const taiwanDateFormatter = new Intl.DateTimeFormat("en-CA", { 20 timeZone: "Asia/Taipei", 21 year: "numeric", 22 month: "2-digit", 23 day: "2-digit", 24}); 25let schedulePrograms = []; 26let displayedScheduleKey; 27let scheduleRequest; 28 29const expandedSchedule = document.querySelector(".schedule-expanded"); 30const scheduleToggle = expandedSchedule?.querySelector(".schedule-toggle"); 31const expandedScheduleText = expandedSchedule?.querySelector(".schedule"); 32 33const updateExpandedScheduleAlignment = () => { 34 if (!expandedSchedule || !expandedScheduleText) return; 35 expandedSchedule.style.setProperty( 36 "--schedule-text-offset", 37 `${expandedScheduleText.offsetLeft}px`, 38 ); 39}; 40 41if (expandedSchedule) { 42 const expandedScheduleResizeObserver = new ResizeObserver( 43 updateExpandedScheduleAlignment, 44 ); 45 expandedScheduleResizeObserver.observe(expandedSchedule); 46 updateExpandedScheduleAlignment(); 47} 48 49scheduleToggle?.addEventListener("click", () => { 50 const isExpanded = expandedSchedule.classList.toggle("is-open"); 51 scheduleToggle.setAttribute("aria-expanded", String(isExpanded)); 52 scheduleToggle.setAttribute( 53 "aria-label", 54 isExpanded ? "æ¶åå¾çºæ¯è³½å ´æ¬¡" : "å±éå¾çºæ¯è³½å ´æ¬¡", 55 ); 56}); 57 58function updateSchedule() { 59 const now = Date.now(); 60 const programIndex = schedulePrograms.findIndex((item) => item.endsAt > now); 61 const program = programIndex >= 0 ? schedulePrograms[programIndex] : null; 62 63 const getTaiwanDate = (timestamp) => { 64 const parts = Object.fromEntries( 65 taiwanDateFormatter 66 .formatToParts(new Date(timestamp)) 67 .map(({ type, value }) => [type, value]), 68 ); 69 return `${parts.year}-${parts.month}-${parts.day}`; 70 }; 71 const today = getTaiwanDate(now); 72 const tomorrow = getTaiwanDate(now + 86400000); 73 const todayPrograms = schedulePrograms.filter( 74 (item) => item.date === today && item.endsAt > now && item !== program, 75 ); 76 const upcomingPrograms = todayPrograms.length 77 ? todayPrograms 78 : schedulePrograms.filter( 79 (item) => item.date === tomorrow && item !== program, 80 ); 81 const scheduleKey = `${programIndex}:${upcomingPrograms.map((item) => item.startsAt).join(",")}`; 82 if (scheduleKey === displayedScheduleKey) return; 83 displayedScheduleKey = scheduleKey; 84 85 let requiredText = "æ«ç¡å³å°æåºçç¯ç®"; 86 if (program) { 87 const [year, month, day] = program.date.split("-").map(Number); 88 const weekday = 89 scheduleWeekdays[new Date(Date.UTC(year, month - 1, day)).getUTCDay()]; 90 requiredText = `${month}/${day}(${weekday}) ${program.startTime} ${program.title}`; 91 } 92 const optionalTexts = 93 program && Array.isArray(program.texts) ? program.texts : []; 94 scheduleBlocks.forEach((block) => { 95 block.querySelector(".required").textContent = requiredText; 96 const optional = block.querySelector(".optional"); 97 optional.textContent = optionalTexts.join(" "); 98 optional.classList.toggle("hidden", optionalTexts.length === 0); 99 }); 100 101 const upcomingElement = 102 expandedSchedule?.querySelector(".schedule-upcoming"); 103 if (upcomingElement) { 104 upcomingElement.replaceChildren(); 105 if (upcomingPrograms.length) { 106 upcomingPrograms.forEach((upcomingProgram) => { 107 const [year, month, day] = upcomingProgram.date.split("-").map(Number); 108 const weekday = 109 scheduleWeekdays[ 110 new Date(Date.UTC(year, month - 1, day)).getUTCDay() 111 ]; 112 const details = Array.isArray(upcomingProgram.texts) 113 ? upcomingProgram.texts.join(" ") 114 : "";
115 const row = document.createElement("div"); 116 const required = document.createElement("span"); 117 required.className = "required"; 118 required.textContent = `${month}/${day}(${weekday}) ${upcomingProgram.startTime} ${upcomingProgram.title}`; 119 row.append(required); 120 const optional = document.createElement("span"); 121 optional.className = details ? "optional" : "optional hidden"; 122 optional.textContent = details; 123 row.append(optional); 124 upcomingElement.append(row); 125 }); 126 } else { 127 const row = document.createElement("div"); 128 row.className = "schedule-empty"; 129 row.textContent = "è«æå¾ ç²¾å½©çæ¯è³½è½æ"; 130 upcomingElement.append(row); 131 } 132 upcomingElement.classList.remove("hidden"); 133 scheduleToggle?.classList.remove("hidden"); 134 } 135 updateMobileScheduleHeight(); 136} 137 138async function loadSchedule() { 139 scheduleRequest?.abort(); 140 const controller = new AbortController(); 141 scheduleRequest = controller; 142 try { 143 const dataBase = (window.SITE_DATA_BASE || "").replace(/\/$/, ""); 144 const response = await fetch(dataBase ? `${dataBase}/schedule.json` : `json/schedule.json?refresh=${Date.now()}`, { 145 cache: "no-store", 146 signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000)]), 147 }); 148 if (!response.ok) 149 throw new Error(`Schedule request failed: ${response.status}`); 150 const payload = await response.json(); 151 if (scheduleRequest !== controller) return; 152 if (!Array.isArray(payload.programs)) 153 throw new Error("Schedule programs is not an array"); 154 schedulePrograms = payload.programs 155 .filter( 156 (program) => 157 program && 158 program.date && 159 program.startTime && 160 program.endTime && 161 program.title, 162 ) 163 .map((program) => ({ 164 ...program, 165 startsAt: Date.parse(`${program.date}T${program.startTime}:00+08:00`), 166 endsAt: Date.parse(`${program.date}T${program.endTime}:00+08:00`), 167 })) 168 .filter( 169 (program) => 170 Number.isFinite(program.startsAt) && 171 Number.isFinite(program.endsAt) && 172 program.endsAt > program.startsAt, 173 ) 174 .sort((first, second) => first.startsAt - second.startsAt); 175 // å³ä½¿æ¥æèææ®µä¸è®ï¼å¾å°ä¿®æ¹æ¨é¡æå°æ°æåä¹å¿ é é繪ã 176 displayedScheduleKey = undefined; 177 document.querySelectorAll(".schedule-container, #schedule .note").forEach((element) => { element.style.display = ""; }); 178 updateSchedule(); 179 } catch (error) { 180 if (controller.signal.aborted || scheduleRequest !== controller) return; 181 schedulePrograms = []; 182 displayedScheduleKey = undefined; 183 document 184 .querySelectorAll(".schedule-container, #schedule .note") 185 .forEach((element) => { 186 element.style.display = "none"; 187 }); 188 updateMobileScheduleHeight(); 189 console.error(error); 190 } 191} 192 193loadSchedule(); 194setInterval(updateSchedule, 1000); 195document.addEventListener("visibilitychange", () => { 196 if (!document.hidden) { updateSchedule(); if (window.SITE_DATA_BASE) loadSchedule(); } 197}); 198if (window.SITE_DATA_BASE) setInterval(() => { if (!document.hidden) loadSchedule(); }, 30000);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.