140 lines
5.6 KiB
JavaScript
140 lines
5.6 KiB
JavaScript
// 首页三栏卡片:拉取目录 -> cards.layout 分配三栏 -> 渲染(time 卡大字 + 其他占位)
|
||
// Home three-column cards: fetch catalog -> cards.layout assigns 3 columns -> render
|
||
// (big time card + generic placeholders for the rest)
|
||
window.updateHomeCards = async function () {
|
||
const page = document.querySelector('[data-page="home"]');
|
||
page.innerHTML = `<div class="tri-col" id="tri-col"></div>`;
|
||
let catalog = [];
|
||
try {
|
||
// 优先用预加载缓存,未就绪则实时加载
|
||
// Prefer the preloaded cache; fetch live when not ready yet
|
||
catalog = window.preload.cards || JSON.parse(await bridge.call('fetchCards'));
|
||
} catch (e) {
|
||
// 桥接不可用(如未接线)时按空目录处理
|
||
// Treat bridge failure as an empty catalog
|
||
catalog = [];
|
||
}
|
||
if (!Array.isArray(catalog)) catalog = [];
|
||
// 兜底:无目录或缺失 time 卡时补内置大字时间卡,保证首页始终有时间卡
|
||
// Fallback: prepend builtin big time card when the catalog is empty or missing the time card
|
||
if (!catalog.some((c) => c.id === 'time')) {
|
||
catalog.push({ id: 'time', name: '大字时间', priority: 0 });
|
||
}
|
||
renderCards(catalog);
|
||
startTimeCardTicker();
|
||
};
|
||
|
||
// 卡片 Grid 渲染:media 卡 span 2(跨栏),time/其他卡 span 1
|
||
// Card grid render: media card spans 2 columns, time/other cards span 1
|
||
function renderCards(catalog) {
|
||
const grid = document.getElementById('tri-col');
|
||
grid.textContent = '';
|
||
const items = [];
|
||
// 按 priority 排序(数字小者优先)
|
||
// Sort by priority (smaller first)
|
||
const sorted = [...catalog].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
||
// time 卡(内置,span 1)
|
||
if (sorted.some(c => c.id === 'time')) items.push({ span: 1, el: renderTimeCard() });
|
||
// media 卡(有媒体时 span 2,跨栏更美观)
|
||
if (currentMedia) items.push({ span: 2, el: renderMediaCard(currentMedia) });
|
||
// 其他卡(排除 time)span 1
|
||
sorted.filter(c => c.id !== 'time').forEach(c => {
|
||
items.push({ span: 1, el: renderGenericCard(c.id) });
|
||
});
|
||
items.forEach(item => {
|
||
item.el.classList.add('span-' + item.span);
|
||
grid.appendChild(item.el);
|
||
});
|
||
}
|
||
|
||
// 内置大字时间卡:当前时间(时:分 + 小号秒)+ 日期
|
||
// Builtin big time card: current time (HH:MM + small seconds) + date
|
||
function renderTimeCard() {
|
||
const el = document.createElement('div');
|
||
el.className = 'card time-card';
|
||
const now = new Date();
|
||
el.innerHTML = `<div class="big"><span class="hm">${fmtTime(now)}</span><span class="sec">${fmtSec(now)}</span></div><div class="sub">${fmtDate(now)}</div>`;
|
||
return el;
|
||
}
|
||
|
||
// 其他卡片占位:仅显示 id,后续 Task 接具体卡片渲染
|
||
// Other cards placeholder: show id only; concrete renderers land in later tasks
|
||
function renderGenericCard(id) {
|
||
const el = document.createElement('div');
|
||
el.className = 'card';
|
||
el.textContent = id;
|
||
return el;
|
||
}
|
||
|
||
function fmtTime(d) {
|
||
const hh = String(d.getHours()).padStart(2, '0');
|
||
const mm = String(d.getMinutes()).padStart(2, '0');
|
||
return `${hh}:${mm}`;
|
||
}
|
||
|
||
function fmtSec(d) {
|
||
return `:${String(d.getSeconds()).padStart(2, '0')}`;
|
||
}
|
||
|
||
function fmtDate(d) {
|
||
const days = ['日', '一', '二', '三', '四', '五', '六'];
|
||
return `${d.getMonth() + 1}月${d.getDate()}日 周${days[d.getDay()]}`;
|
||
}
|
||
|
||
// 每秒刷新一次大字时间卡(含秒显)
|
||
// Refresh the big time card every second (including the seconds display)
|
||
let timeTicker = null;
|
||
function startTimeCardTicker() {
|
||
if (timeTicker) return;
|
||
timeTicker = setInterval(() => {
|
||
const card = document.querySelector('.time-card');
|
||
if (!card) return;
|
||
const now = new Date();
|
||
card.querySelector('.hm').textContent = fmtTime(now);
|
||
card.querySelector('.sec').textContent = fmtSec(now);
|
||
card.querySelector('.sub').textContent = fmtDate(now);
|
||
}, 1000);
|
||
}
|
||
|
||
// 当前媒体信息(null 表示无媒体);切页后 updateHomeCards 据此恢复媒体卡
|
||
// Current media info (null = none); updateHomeCards restores the media card from it
|
||
let currentMedia = null;
|
||
|
||
// 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局
|
||
// Native media session event: render media card in middle column when present,
|
||
// otherwise fall back to re-layout (calendar/weather fill the media card slot)
|
||
window.HearthEvents = window.HearthEvents || {};
|
||
window.HearthEvents.mediaSessionChanged = function (info) {
|
||
currentMedia = info;
|
||
const grid = document.getElementById('tri-col');
|
||
if (!grid) return;
|
||
// 重新渲染:有媒体时 media 卡 span 2,无媒体时其他卡填满
|
||
// Re-render: media card spans 2 when present, others fill otherwise
|
||
window.updateHomeCards();
|
||
};
|
||
|
||
// 媒体卡:正在播放标签 + 标题/艺术家 + 进度条
|
||
// Media card: playing caption + title/artist + progress bar
|
||
function renderMediaCard(info) {
|
||
const el = document.createElement('div');
|
||
el.className = 'card media-card';
|
||
const pct = info.duration ? (info.position / info.duration) * 100 : 0;
|
||
// title/artist 来自任意应用元数据,转义后插入,防 XSS
|
||
// title/artist come from arbitrary app metadata; escape before insert (XSS)
|
||
el.innerHTML = `<div class="cap">正在播放</div>
|
||
<div class="tt">${escapeHtml(info.title)}</div><div class="ar">${escapeHtml(info.artist)}</div>
|
||
<div class="prog"><div class="bar" style="width:${pct}%"></div></div>`;
|
||
return el;
|
||
}
|
||
|
||
// 转义 HTML 特殊字符,避免媒体元数据注入标签
|
||
// Escape HTML special chars to prevent metadata injection
|
||
function escapeHtml(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|