feat: offline package version management, long-press menu

This commit is contained in:
2026-08-17 18:02:53 +08:00
parent 49b84f83ad
commit 811e2c9716
5 changed files with 186 additions and 35 deletions
+27
View File
@@ -538,6 +538,33 @@ body.webapp-active [data-page="webapplist"] {
transition: width .2s ease;
}
/* webapp 长按管理菜单 */
/* webapp long-press management menu */
.wapp-menu {
position: fixed;
z-index: 300;
min-width: 180px;
padding: 8px;
border-radius: 16px;
background: var(--glass-bg);
backdrop-filter: var(--blur-filter);
-webkit-backdrop-filter: var(--blur-filter);
border: 1px solid var(--glass-border);
box-shadow: 0 4px 20px rgba(0, 0, 0, .3);
}
.wapp-menu-item {
padding: 10px 12px;
border-radius: 10px;
font-size: 14px;
color: var(--text);
cursor: pointer;
}
.wapp-menu-item:active {
background: var(--glass-border);
}
#web-topbar {
position: fixed;
top: 12px;
+90 -25
View File
@@ -1,5 +1,6 @@
// H5 应用列表页:富卡片 + 在线/离线标签 + 搜索 + 长按导入离线包
// H5 web app list page: rich cards + online/offline badge + search + long-press import
// H5 应用列表页:富卡片 + 在线/离线/可更新标签 + 搜索 + 长按菜单(更新/删除/导入/属性)
// H5 web app list page: rich cards + online/offline/updatable badge + search +
// long-press menu (update/delete/import/properties)
window.loadWebAppList = async function () {
const page = document.querySelector('[data-page="webapplist"]');
if (page.dataset.loaded) return; page.dataset.loaded = '1';
@@ -12,8 +13,6 @@ window.loadWebAppList = async function () {
const grid = document.getElementById('web-grid');
const render = (list) => {
grid.textContent = '';
// 用 createElement + textContent/dataset 渲染,避免 innerHTML 拼接远端数据(XSS
// Render with createElement + textContent/dataset to avoid innerHTML remote data (XSS)
list.forEach(a => {
const btn = document.createElement('button');
btn.className = 'wcell';
@@ -31,13 +30,10 @@ window.loadWebAppList = async function () {
tag.textContent = a.offline ? '离线' : '在线';
btn.append(img, lbl, tag);
btn.onclick = () => bridge.call('openWebApp', btn.dataset.id);
// 长按导入离线包
setupImportLongPress(btn, a.id);
setupLongPress(btn, a);
grid.appendChild(btn);
// 异步检查本地离线包状态,更新标签
bridge.call('hasLocalPackage', a.id).then(has => {
if (has) tag.textContent = '离线';
}).catch(() => {});
// 异步检查本地离线包状态 + 版本对比,更新标签
refreshBadge(tag, a);
});
};
render(apps);
@@ -47,17 +43,90 @@ window.loadWebAppList = async function () {
};
};
// 长按(600ms)导入离线包
// Long-press (600ms) to import an offline package
function setupImportLongPress(btn, id) {
// 异步刷新列表项标签(离线 / 可更新)
// Refresh the badge asynchronously (offline / updatable)
async function refreshBadge(tag, app) {
try {
const hasLocal = await bridge.call('hasLocalPackage', app.id);
const localVer = await bridge.call('getLocalVersion', app.id);
const cloudVer = app.offlineVersion || '';
if (hasLocal && cloudVer && cloudVer !== localVer) {
tag.textContent = '可更新';
} else if (hasLocal) {
tag.textContent = '离线';
} else if (app.offline) {
tag.textContent = '离线';
} else {
tag.textContent = '在线';
}
} catch (e) { /* 忽略 */ }
}
// 长按(600ms)弹出管理菜单
// Long-press (600ms) to show the management menu
function setupLongPress(btn, app) {
let timer = null;
btn.addEventListener('touchstart', () => {
const start = (e) => {
timer = setTimeout(() => {
bridge.call('importOfflinePackage', id).catch(() => {});
showWebAppMenu(app, e.touches[0].clientX, e.touches[0].clientY);
}, 600);
}, { passive: true });
btn.addEventListener('touchend', () => { if (timer) clearTimeout(timer); }, { passive: true });
btn.addEventListener('touchmove', () => { if (timer) clearTimeout(timer); }, { passive: true });
};
const cancel = () => { if (timer) clearTimeout(timer); };
btn.addEventListener('touchstart', start, { passive: true });
btn.addEventListener('touchend', cancel, { passive: true });
btn.addEventListener('touchmove', cancel, { passive: true });
}
// 弹出管理菜单:更新 / 删除 / 导入 / 属性
// Show the management menu: update / delete / import / properties
async function showWebAppMenu(app, x, y) {
document.querySelectorAll('.wapp-menu').forEach(m => m.remove());
const hasLocal = await bridge.call('hasLocalPackage', app.id);
const localVer = await bridge.call('getLocalVersion', app.id);
const cloudVer = app.offlineVersion || '';
const menu = document.createElement('div');
menu.className = 'wapp-menu';
const addItem = (label, onClick) => {
const item = document.createElement('div');
item.className = 'wapp-menu-item';
item.textContent = label;
item.onclick = () => { menu.remove(); if (onClick) onClick(); };
menu.appendChild(item);
};
// 更新(本地有包 + 云端版本不同)
if (hasLocal && cloudVer && cloudVer !== localVer) {
addItem('更新离线包(' + (localVer || '?') + ' → ' + cloudVer + '', () => {
bridge.call('updateOfflinePackage', app.id).catch(() => {});
});
}
// 删除(本地有包)
if (hasLocal) {
addItem('删除本地包', () => {
bridge.call('deleteLocalPackage', app.id).then(() => refreshList()).catch(() => {});
});
}
// 导入
addItem('导入离线包', () => {
bridge.call('importOfflinePackage', app.id).catch(() => {});
});
// 属性
addItem('属性:本地 ' + (localVer || '无') + ' / 云端 ' + (cloudVer || '无'), null);
menu.style.left = Math.min(x, window.innerWidth - 220) + 'px';
menu.style.top = Math.min(y, window.innerHeight - 180) + 'px';
document.body.appendChild(menu);
}
// 刷新列表(删除/导入后重新加载)
// Refresh the list (reload after delete/import)
function refreshList() {
const page = document.querySelector('[data-page="webapplist"]');
if (page) {
page.dataset.loaded = '';
if (page.classList.contains('active')) window.loadWebAppList();
}
}
// webapp 加载/下载进度:显示覆盖层
@@ -74,13 +143,9 @@ window.HearthEvents.webappProgress = function (id, progress) {
loading.querySelector('.wl-fill').style.width = progress + '%';
};
// 导入完成:刷新列表(离线标签更新)
// Import complete: refresh the list (badge updates)
// 导入完成:刷新列表
// Import complete: refresh the list
window.HearthEvents.webappImported = function (id, ok) {
if (!ok) return;
const page = document.querySelector('[data-page="webapplist"]');
if (page) {
page.dataset.loaded = '';
if (page.classList.contains('active')) window.loadWebAppList();
}
refreshList();
};