feat: page preload, card grid span, wallpaper into webview for glass blur

This commit is contained in:
2026-08-16 19:51:08 +08:00
parent 1a79b4ed55
commit 8c42dca7bc
8 changed files with 175 additions and 47 deletions
+46 -13
View File
@@ -120,25 +120,22 @@ body {
to { opacity: 1; transform: none; }
}
/* 首页三栏布局:三等分纵向列,卡片向下堆叠 */
/* Home three-column layout: three equal vertical columns, cards stack downward */
/* 首页卡片 Grid:三等分列,卡片可跨栏(span-2/3),行均分高度 */
/* Home card grid: three equal columns, cards can span (span-2/3), rows split evenly */
.tri-col {
flex: 1;
min-height: 0;
display: flex;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
gap: 16px;
padding: 16px;
}
.col {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
}
.card.span-2 { grid-column: span 2; }
.card.span-3 { grid-column: span 3; }
/* 卡片:柔光玻璃拟态(复用 Task 4 token,非纯色背景) */
/* Card: soft-glass morphism (reuse Task 4 tokens, not a solid color) */
.card {
@@ -161,7 +158,9 @@ body {
/* 时钟卡缩放权重小:占栏高比例小于其他卡片 */
/* Time card grows less: takes a smaller share of column height than other cards */
.time-card {
flex-grow: 0.5;
display: flex;
flex-direction: column;
justify-content: center;
}
.time-card .big {
@@ -185,6 +184,13 @@ body {
color: var(--text-dim);
}
/* 媒体卡:flex column,标题靠上、进度/控制靠下 */
/* Media card: flex column, title top, progress/controls bottom */
.media-card {
display: flex;
flex-direction: column;
}
/* 媒体卡:正在播放标签 + 标题/艺术家 + 进度条 */
/* Media card: playing caption + title/artist + progress bar */
.media-card .cap {
@@ -212,7 +218,8 @@ body {
}
.media-card .prog {
margin-top: 12px;
margin-top: auto;
padding-top: 12px;
height: 4px;
border-radius: 999px;
background: var(--glass-border);
@@ -225,6 +232,32 @@ body {
background: var(--accent);
}
/* 媒体控制按钮:上一首/暂停/下一首 */
/* Media controls: previous / play-pause / next */
.media-card .controls {
margin-top: 12px;
display: flex;
gap: 10px;
}
.media-card .controls button {
width: 36px;
height: 36px;
border: none;
border-radius: 999px;
background: var(--glass-border);
color: var(--text);
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.media-card .controls button:active {
opacity: 0.6;
}
/* 安卓 APP 列表页:搜索框 + 网格 */
/* Android app list page: search box + grid */
.search { padding: 16px; }
+14
View File
@@ -25,6 +25,7 @@
</main>
</div>
<script src="js/bridge.js"></script>
<script src="js/preload.js"></script>
<script src="js/mask.js"></script>
<script src="js/cards/cards.js"></script>
<script src="js/router.js"></script>
@@ -55,6 +56,19 @@
// 初始化背景遮罩(夜间主题 + 已启用时生效)
// Apply wallpaper dim on startup (active under dark theme when enabled)
window.mask.apply();
// 加载壁纸为 body 背景:玻璃效果 backdrop-filter 需要 WebView 内部有背景可模糊
// Load the wallpaper as the body background: the glass backdrop-filter needs
// in-WebView content behind cards to blur
bridge.call('getWallpaper').then(wp => {
if (wp) {
document.body.style.backgroundImage = 'url(' + wp + ')';
document.body.style.backgroundSize = 'cover';
document.body.style.backgroundPosition = 'center';
}
}).catch(() => {});
// 首屏渲染后延迟预加载其他页面数据,减少切页等待
// Preload other pages' data after first paint to reduce switch latency
setTimeout(() => window.preload.loadAll(), 800);
</script>
</body>
</html>
+3 -1
View File
@@ -6,7 +6,9 @@ window.loadAppList = async function () {
page.innerHTML = `
<div class="search"><input id="app-search" placeholder="搜索应用…"></div>
<div class="grid" id="app-grid"></div>`;
const apps = JSON.parse(await bridge.call('listApps'));
// 优先用预加载缓存,未就绪则实时加载
// Prefer the preloaded cache; fetch live when not ready yet
const apps = window.preload.apps || JSON.parse(await bridge.call('listApps'));
const grid = document.getElementById('app-grid');
const render = (list) => {
grid.textContent = '';
+33 -32
View File
@@ -3,12 +3,12 @@
// (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 class="col" id="col-0"></div><div class="col" id="col-1"></div><div class="col" id="col-2"></div>
</div>`;
page.innerHTML = `<div class="tri-col" id="tri-col"></div>`;
let catalog = [];
try {
catalog = JSON.parse(await bridge.call('fetchCards'));
// 优先用预加载缓存,未就绪则实时加载
// 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
@@ -20,27 +20,33 @@ window.updateHomeCards = async function () {
if (!catalog.some((c) => c.id === 'time')) {
catalog.push({ id: 'time', name: '大字时间', priority: 0 });
}
const cols = cards.layout(catalog);
cols.forEach((ids, i) => {
const col = document.getElementById(`col-${i}`);
ids.forEach((id) => {
if (id === 'time') col.appendChild(renderTimeCard());
else col.appendChild(renderGenericCard(id));
});
});
// 若有当前媒体,覆盖中间栏为媒体卡(切页回来后恢复媒体卡)
// If there is active media, replace the middle column with the media card
// (restores the media card when navigating back to home)
if (currentMedia) {
const mediaCol = document.getElementById('col-1');
if (mediaCol) {
mediaCol.innerHTML = '';
mediaCol.appendChild(renderMediaCard(currentMedia));
}
}
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) });
// 其他卡(排除 timespan 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() {
@@ -100,16 +106,11 @@ let currentMedia = null;
window.HearthEvents = window.HearthEvents || {};
window.HearthEvents.mediaSessionChanged = function (info) {
currentMedia = info;
const mediaCol = document.getElementById('col-1');
if (!mediaCol) return;
if (info) {
mediaCol.innerHTML = '';
mediaCol.appendChild(renderMediaCard(info));
} else {
// 无媒体:降级到日历/天气(由 cards.layout 重新计算)
// No media: fall back to calendar/weather (recomputed by cards.layout)
window.updateHomeCards();
}
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();
};
// 媒体卡:正在播放标签 + 标题/艺术家 + 进度条
@@ -6,7 +6,9 @@ window.loadWebAppList = async function () {
page.innerHTML = `
<div class="search"><input id="web-search" placeholder="搜索 webAPP…"></div>
<div class="grid" id="web-grid"></div>`;
const apps = JSON.parse(await bridge.call('fetchWebApps'));
// 优先用预加载缓存,未就绪则实时加载
// Prefer the preloaded cache; fetch live when not ready yet
const apps = window.preload.webApps || JSON.parse(await bridge.call('fetchWebApps'));
const grid = document.getElementById('web-grid');
const render = (list) => {
grid.textContent = '';
+24
View File
@@ -0,0 +1,24 @@
// 页面数据预加载:启动后后台拉取各页面数据缓存,切页时直接渲染减少延迟
// Page data preload: fetch and cache page data in the background after startup,
// so page switches render immediately without a fetch delay
window.preload = {
apps: null,
webApps: null,
cards: null,
async loadAll() {
try {
const [apps, webApps, cards] = await Promise.all([
bridge.call('listApps').catch(() => null),
bridge.call('fetchWebApps').catch(() => null),
bridge.call('fetchCards').catch(() => null),
]);
this.apps = apps ? JSON.parse(apps) : null;
this.webApps = webApps ? JSON.parse(webApps) : null;
this.cards = cards ? JSON.parse(cards) : null;
} catch (e) {
// 预加载失败不阻塞:切页时仍会实时加载
// Preload failure is non-blocking: page switches still fetch live
}
},
};