feat: settings interactions (theme/mask/brightness/server-url), svg icons, seconds display, page transition
This commit is contained in:
@@ -9,13 +9,26 @@ html, body { height: 100%; }
|
||||
/* Wallpaper transparency: html/body keep transparent to reveal native wallpaper */
|
||||
html, body { background: transparent; }
|
||||
|
||||
/* 背景遮罩层:夜间主题下盖在壁纸上、UI 之下,透明度由 mask.js 控制 */
|
||||
/* Wallpaper dim overlay: sits above the wallpaper and below the UI; opacity is
|
||||
controlled by mask.js */
|
||||
#wallpaper-dim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
transition: opacity .3s;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text);
|
||||
font-family: -apple-system, "MiSans", "PingFang SC", sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app { display: flex; height: 100vh; }
|
||||
.app { position: relative; z-index: 1; display: flex; height: 100vh; }
|
||||
|
||||
/* 侧边栏:柔光玻璃容器,非纯色背景 */
|
||||
/* Sidebar: soft-glass container, not a solid color */
|
||||
@@ -35,7 +48,16 @@ body {
|
||||
|
||||
.rail.expanded { width: 240px; align-items: stretch; padding: 12px 8px; }
|
||||
|
||||
.rail-clock { font-size: 14px; color: var(--text-dim); display: none; }
|
||||
.rail-clock {
|
||||
font-size: 14px;
|
||||
color: var(--text-dim);
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
/* 始终占位固定高度,首页隐藏文字但保留高度,避免撑动下方图标 */
|
||||
/* Always occupy a fixed height; hide text on home but keep the height to
|
||||
avoid shifting the icons below */
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.rail-item {
|
||||
display: flex;
|
||||
@@ -50,7 +72,8 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rail-item .ico { font-size: 20px; }
|
||||
.rail-item .ico { width: 24px; height: 24px; }
|
||||
.rail-item .ico svg { width: 100%; height: 100%; display: block; }
|
||||
.rail-item .lbl { font-size: 11px; }
|
||||
|
||||
.rail-item.active { background: var(--accent); color: #fff; }
|
||||
@@ -58,7 +81,18 @@ body {
|
||||
.content { flex: 1; overflow: hidden; }
|
||||
|
||||
.page { display: none; height: 100%; }
|
||||
.page.active { display: flex; flex-direction: column; }
|
||||
.page.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 页面切换淡入 + 轻微上移动画 */
|
||||
/* Fade-in + slight rise transition on page switch */
|
||||
animation: page-fade .22s ease;
|
||||
}
|
||||
|
||||
@keyframes page-fade {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* 首页三栏布局:三等分纵向列,卡片向下堆叠 */
|
||||
/* Home three-column layout: three equal vertical columns, cards stack downward */
|
||||
@@ -99,6 +133,15 @@ body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 秒显:小号、弱化,紧跟时:分之后 */
|
||||
/* Seconds: small and dim, trailing the HH:MM */
|
||||
.time-card .sec {
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
color: var(--text-dim);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.time-card .sub {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
@@ -167,6 +210,9 @@ body {
|
||||
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 0 16px 16px;
|
||||
/* 行靠顶部对齐,避免结果只有一行时被拉伸到接近容器高度 */
|
||||
/* Align rows to the top so a single result row doesn't stretch to container height */
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.cell {
|
||||
@@ -347,6 +393,19 @@ body {
|
||||
|
||||
.st-item .lbl { font-size: 15px; }
|
||||
|
||||
/* 服务器地址输入框:点击设置项后展开 */
|
||||
/* Server URL input: expanded inside the item on tap */
|
||||
.url-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--glass-border);
|
||||
background: var(--glass-bg);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 右侧箭头:弱化色 */
|
||||
/* Trailing arrow: dim color */
|
||||
.st-item .arrow {
|
||||
@@ -400,7 +459,7 @@ body {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 60%;
|
||||
width: var(--fill, 60%);
|
||||
border-radius: 999px;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
--accent: #ff6900;
|
||||
}
|
||||
|
||||
/* 系统深色:仅在未强制浅色时生效(跟随系统) */
|
||||
/* System dark: only applies when not forced light (follow-system) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
:root:not([data-theme="light"]) {
|
||||
--glass-bg: rgba(21, 21, 24, 0.55); /* 深色半透明 / dark translucent */
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--text: #ffffff;
|
||||
@@ -17,3 +19,13 @@
|
||||
--accent: #ff6900;
|
||||
}
|
||||
}
|
||||
|
||||
/* 强制深色:无论系统主题 */
|
||||
/* Forced dark: regardless of system theme */
|
||||
:root[data-theme="dark"] {
|
||||
--glass-bg: rgba(21, 21, 24, 0.55);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--text: #ffffff;
|
||||
--text-dim: #9a9aa0;
|
||||
--accent: #ff6900;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
<title>Hearth</title>
|
||||
<link rel="stylesheet" href="css/tokens.css">
|
||||
<link rel="stylesheet" href="css/app.css">
|
||||
<script src="js/theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wallpaper-dim"></div>
|
||||
<div class="app">
|
||||
<aside class="rail" id="rail">
|
||||
<div class="rail-clock" id="rail-clock">14:30</div>
|
||||
@@ -22,6 +24,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="js/bridge.js"></script>
|
||||
<script src="js/mask.js"></script>
|
||||
<script src="js/cards/cards.js"></script>
|
||||
<script src="js/router.js"></script>
|
||||
<script src="js/pages/applist.js"></script>
|
||||
@@ -48,6 +51,9 @@
|
||||
document.getElementById('rail-clock').textContent = `${hh}:${mm}`;
|
||||
}, 10000);
|
||||
router.navigate('home');
|
||||
// 初始化背景遮罩(夜间主题 + 已启用时生效)
|
||||
// Apply wallpaper dim on startup (active under dark theme when enabled)
|
||||
window.mask.apply();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 背景遮罩:夜间主题下给壁纸加暗色遮罩,可开关 + 调明暗度(存 localStorage)
|
||||
// Wallpaper dim: darkens the wallpaper under dark theme, with toggle + opacity
|
||||
// control (persisted in localStorage)
|
||||
(function () {
|
||||
const KEY_ON = 'hearth-mask-enabled';
|
||||
const KEY_ALPHA = 'hearth-mask-alpha';
|
||||
|
||||
// 判断当前是否深色主题(强制深色,或跟随系统且系统为深色)
|
||||
// Whether the current theme is dark (forced dark, or follow-system + system dark)
|
||||
function isDark() {
|
||||
const t = window.theme.get();
|
||||
if (t === 'dark') return true;
|
||||
if (t === 'light') return false;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
window.mask = {
|
||||
enabled() {
|
||||
return localStorage.getItem(KEY_ON) === '1';
|
||||
},
|
||||
alpha() {
|
||||
return parseFloat(localStorage.getItem(KEY_ALPHA) || '0.55');
|
||||
},
|
||||
setEnabled(on) {
|
||||
localStorage.setItem(KEY_ON, on ? '1' : '0');
|
||||
this.apply();
|
||||
},
|
||||
setAlpha(a) {
|
||||
localStorage.setItem(KEY_ALPHA, String(a));
|
||||
this.apply();
|
||||
},
|
||||
// 应用遮罩:仅深色主题且启用时显示(透明度 = 明暗度)
|
||||
// Apply: show only under dark theme when enabled (opacity = alpha)
|
||||
apply() {
|
||||
const el = document.getElementById('wallpaper-dim');
|
||||
if (!el) return;
|
||||
el.style.opacity = (isDark() && this.enabled()) ? String(this.alpha()) : '0';
|
||||
},
|
||||
};
|
||||
})();
|
||||
@@ -31,13 +31,13 @@ window.updateHomeCards = async function () {
|
||||
startTimeCardTicker();
|
||||
};
|
||||
|
||||
// 内置大字时间卡:当前时间 + 日期
|
||||
// Builtin big time card: current time + date
|
||||
// 内置大字时间卡:当前时间(时:分 + 小号秒)+ 日期
|
||||
// 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">${fmtTime(now)}</div><div class="sub">${fmtDate(now)}</div>`;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -56,13 +56,17 @@ function fmtTime(d) {
|
||||
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()]}`;
|
||||
}
|
||||
|
||||
// 每 10s 刷新一次大字时间卡
|
||||
// Refresh the big time card every 10 seconds
|
||||
// 每秒刷新一次大字时间卡(含秒显)
|
||||
// Refresh the big time card every second (including the seconds display)
|
||||
let timeTicker = null;
|
||||
function startTimeCardTicker() {
|
||||
if (timeTicker) return;
|
||||
@@ -70,9 +74,10 @@ function startTimeCardTicker() {
|
||||
const card = document.querySelector('.time-card');
|
||||
if (!card) return;
|
||||
const now = new Date();
|
||||
card.querySelector('.big').textContent = fmtTime(now);
|
||||
card.querySelector('.hm').textContent = fmtTime(now);
|
||||
card.querySelector('.sec').textContent = fmtSec(now);
|
||||
card.querySelector('.sub').textContent = fmtDate(now);
|
||||
}, 10000);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局
|
||||
|
||||
@@ -1,26 +1,112 @@
|
||||
// 设置页:Miuix Preference 分组 + 玻璃条目(通知使用权为可用交互项)
|
||||
// Settings page: Miuix Preference groups + glass items (notification access is interactive)
|
||||
// 设置页:Miuix Preference 分组 + 玻璃条目,含主题/遮罩/亮度/权限/网络交互
|
||||
// Settings page: Miuix Preference groups + glass items with interactions for
|
||||
// theme / mask / brightness / permission / network
|
||||
window.loadSettings = function () {
|
||||
const page = document.querySelector('[data-page="settings"]');
|
||||
if (page.dataset.loaded) return; page.dataset.loaded = '1';
|
||||
page.innerHTML = `<div class="st-wrap">
|
||||
<div class="st-group">显示</div>
|
||||
<div class="st-item"><span class="lbl">跟随系统主题</span><span class="sw on"><i></i></span></div>
|
||||
<div class="st-item"><span class="lbl">屏幕亮度</span><span class="slider"><i></i></span></div>
|
||||
<div class="st-item" id="theme-item">
|
||||
<span class="lbl">主题</span>
|
||||
<span class="arrow" id="theme-state">跟随系统</span>
|
||||
</div>
|
||||
<div class="st-item" id="brightness-item">
|
||||
<span class="lbl">屏幕亮度</span>
|
||||
<span class="arrow" id="brightness-state">›</span>
|
||||
</div>
|
||||
<div class="st-group" id="mask-group" style="display:none">背景遮罩</div>
|
||||
<div class="st-item" id="mask-item" style="display:none">
|
||||
<span class="lbl">启用背景遮罩</span>
|
||||
<span class="sw" id="mask-sw"><i></i></span>
|
||||
</div>
|
||||
<div class="st-item" id="mask-alpha-item" style="display:none">
|
||||
<span class="lbl">遮罩明暗度</span>
|
||||
<span class="slider" id="mask-slider"><i></i></span>
|
||||
</div>
|
||||
<div class="st-group">权限</div>
|
||||
<div class="st-item" id="notif-access">
|
||||
<span class="lbl">通知使用权(媒体卡)</span>
|
||||
<span class="arrow" id="notif-access-state">›</span>
|
||||
</div>
|
||||
<div class="st-group">网络</div>
|
||||
<div class="st-item"><span class="lbl">webAPP 服务器地址</span><span class="arrow">›</span></div>
|
||||
<div class="st-item"><span class="lbl">检查更新</span><span class="arrow">›</span></div>
|
||||
<div class="st-item" id="server-url-item">
|
||||
<span class="lbl">webAPP 服务器地址</span>
|
||||
<span class="arrow" id="server-url-state">›</span>
|
||||
</div>
|
||||
<div class="st-item" id="check-update-item">
|
||||
<span class="lbl">检查更新</span>
|
||||
<span class="arrow" id="check-update-state">›</span>
|
||||
</div>
|
||||
<div class="st-group">关于</div>
|
||||
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow">›</span></div>
|
||||
</div>`;
|
||||
setupTheme(page);
|
||||
setupMask(page);
|
||||
setupNotificationAccess(page);
|
||||
setupBrightness(page);
|
||||
setupServerUrl(page);
|
||||
setupCheckUpdate(page);
|
||||
};
|
||||
|
||||
// 判断当前是否深色主题(与 mask.js 逻辑一致)
|
||||
// Whether the current theme is dark (same logic as mask.js)
|
||||
function isDarkTheme() {
|
||||
const t = theme.get();
|
||||
if (t === 'dark') return true;
|
||||
if (t === 'light') return false;
|
||||
return matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
// 主题三态循环:跟随系统 → 浅色 → 深色
|
||||
// Theme tri-state cycle: system → light → dark
|
||||
function setupTheme(page) {
|
||||
const state = page.querySelector('#theme-state');
|
||||
const refresh = () => { state.textContent = theme.label(theme.get()); };
|
||||
refresh();
|
||||
page.querySelector('#theme-item').onclick = () => {
|
||||
theme.cycle();
|
||||
refresh();
|
||||
window.mask.apply();
|
||||
setupMask(page); // 主题切换后重算遮罩项的显示与状态
|
||||
};
|
||||
}
|
||||
|
||||
// 背景遮罩:开关 + 明暗度(仅夜间主题显示)
|
||||
// Wallpaper dim: toggle + opacity (shown only under dark theme)
|
||||
function setupMask(page) {
|
||||
const group = page.querySelector('#mask-group');
|
||||
const maskItem = page.querySelector('#mask-item');
|
||||
const alphaItem = page.querySelector('#mask-alpha-item');
|
||||
const sw = page.querySelector('#mask-sw');
|
||||
const slider = page.querySelector('#mask-slider');
|
||||
const thumb = slider.querySelector('i');
|
||||
|
||||
const dark = isDarkTheme();
|
||||
const show = dark;
|
||||
group.style.display = show ? '' : 'none';
|
||||
maskItem.style.display = show ? '' : 'none';
|
||||
alphaItem.style.display = show ? '' : 'none';
|
||||
|
||||
const refresh = () => {
|
||||
sw.classList.toggle('on', window.mask.enabled());
|
||||
const a = window.mask.alpha();
|
||||
thumb.style.left = (a * 100) + '%';
|
||||
slider.style.setProperty('--fill', (a * 100) + '%');
|
||||
};
|
||||
refresh();
|
||||
|
||||
sw.onclick = () => {
|
||||
window.mask.setEnabled(!window.mask.enabled());
|
||||
refresh();
|
||||
};
|
||||
slider.onclick = (e) => {
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const a = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
|
||||
window.mask.setAlpha(Math.round(a * 100) / 100);
|
||||
refresh();
|
||||
};
|
||||
}
|
||||
|
||||
// 通知使用权授权项:异步查状态,未授权点击跳转系统授权页
|
||||
// Notification-access item: async state check, tap to open the system grant page
|
||||
function setupNotificationAccess(page) {
|
||||
@@ -34,3 +120,103 @@ function setupNotificationAccess(page) {
|
||||
bridge.call('requestNotificationAccess').catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
// 屏幕亮度:未授权「修改系统设置」时跳授权页,已授权点击滑块调整系统亮度
|
||||
// Screen brightness: jump to grant page when lacking WRITE_SETTINGS; adjust via
|
||||
// slider click once granted
|
||||
function setupBrightness(page) {
|
||||
const item = page.querySelector('#brightness-item');
|
||||
const state = page.querySelector('#brightness-state');
|
||||
let sliderEl = null;
|
||||
|
||||
const renderSlider = (value) => {
|
||||
if (sliderEl) sliderEl.remove();
|
||||
sliderEl = document.createElement('span');
|
||||
sliderEl.className = 'slider brightness-slider';
|
||||
sliderEl.innerHTML = '<i></i>';
|
||||
sliderEl.style.width = '160px';
|
||||
const pct = value / 255 * 100;
|
||||
sliderEl.style.setProperty('--fill', pct + '%');
|
||||
sliderEl.querySelector('i').style.left = pct + '%';
|
||||
sliderEl.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const rect = sliderEl.getBoundingClientRect();
|
||||
const v = Math.round(Math.min(255, Math.max(0, (e.clientX - rect.left) / rect.width)) * 255);
|
||||
bridge.call('setSystemBrightness', v).then(() => renderSlider(v)).catch(() => {});
|
||||
};
|
||||
item.appendChild(sliderEl);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
bridge.call('canWriteSettings').then(canWrite => {
|
||||
if (!canWrite) {
|
||||
state.textContent = '去授权 ›';
|
||||
if (sliderEl) { sliderEl.remove(); sliderEl = null; }
|
||||
} else {
|
||||
bridge.call('getSystemBrightness').then(v => {
|
||||
state.textContent = '';
|
||||
renderSlider(v);
|
||||
}).catch(() => {});
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
refresh();
|
||||
|
||||
item.onclick = () => {
|
||||
bridge.call('canWriteSettings').then(canWrite => {
|
||||
if (!canWrite) bridge.call('requestWriteSettings');
|
||||
}).catch(() => {});
|
||||
};
|
||||
}
|
||||
|
||||
// webAPP 服务器地址:点击展开输入框,保存后重新拉取清单
|
||||
// webAPP server URL: expand an input on tap, re-fetch manifest after saving
|
||||
function setupServerUrl(page) {
|
||||
const item = page.querySelector('#server-url-item');
|
||||
const state = page.querySelector('#server-url-state');
|
||||
let editing = false;
|
||||
|
||||
const startEdit = (current) => {
|
||||
editing = true;
|
||||
item.innerHTML = '<input class="url-input" id="server-url-input" value="' + (current || '').replace(/"/g, '"') + '">';
|
||||
const input = item.querySelector('#server-url-input');
|
||||
input.focus();
|
||||
const save = () => {
|
||||
bridge.call('setServerUrl', input.value.trim()).then(() => {
|
||||
editing = false;
|
||||
refresh();
|
||||
}).catch(() => {});
|
||||
};
|
||||
input.onkeydown = (e) => { if (e.key === 'Enter') save(); };
|
||||
input.onblur = () => { save(); };
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
bridge.call('getServerUrl').then(url => {
|
||||
state.textContent = (url || '未设置') + ' ›';
|
||||
}).catch(() => {});
|
||||
};
|
||||
refresh();
|
||||
|
||||
item.onclick = () => {
|
||||
if (editing) return;
|
||||
bridge.call('getServerUrl').then(startEdit).catch(() => startEdit(''));
|
||||
};
|
||||
}
|
||||
|
||||
// 检查更新:触发重新拉取 webAPP 清单 + 卡片目录,完成后提示
|
||||
// Check update: re-fetch the webAPP manifest + card catalog, then show a hint
|
||||
function setupCheckUpdate(page) {
|
||||
const item = page.querySelector('#check-update-item');
|
||||
const state = page.querySelector('#check-update-state');
|
||||
item.onclick = () => {
|
||||
state.textContent = '检查中…';
|
||||
bridge.call('refreshManifest').then(result => {
|
||||
let n = 0;
|
||||
try { n = JSON.parse(result).count || 0; } catch (e) {}
|
||||
state.textContent = n > 0 ? '已更新 ' + n + ' 个 ›' : '无更新 ›';
|
||||
}).catch(() => {
|
||||
state.textContent = '失败 ›';
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
// 桌面路由:侧边栏 5 页 + 页面切换 + 顶部时间
|
||||
// Launcher router: 5 sidebar pages + page switching + top clock
|
||||
// 侧边栏图标:内联 SVG(stroke=currentColor,跟随主题色),替代 emoji
|
||||
// Sidebar icons: inline SVG (stroke=currentColor, follows theme color), replacing emoji
|
||||
const SVG_ICONS = {
|
||||
home: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 9.5V21h14V9.5"/><path d="M10 21v-6h4v6"/></svg>',
|
||||
immersive: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 11l1.5-4.5A2 2 0 018.4 5h7.2a2 2 0 011.9 1.5L19 11"/><path d="M3 11v5a1 1 0 001 1h1a1 1 0 001-1v-1h12v1a1 1 0 001 1h1a1 1 0 001-1v-5"/><circle cx="7.5" cy="16" r="1.6"/><circle cx="16.5" cy="16" r="1.6"/></svg>',
|
||||
webapp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a15 15 0 010 18a15 15 0 010-18"/></svg>',
|
||||
app: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="2"/><rect x="13" y="3" width="8" height="8" rx="2"/><rect x="3" y="13" width="8" height="8" rx="2"/><rect x="13" y="13" width="8" height="8" rx="2"/></svg>',
|
||||
settings: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33a1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82a1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51a1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 110 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>',
|
||||
};
|
||||
|
||||
const PAGES = [
|
||||
{ id: 'home', label: '首页', icon: '🏠' },
|
||||
{ id: 'immersive', label: '沉浸首页', icon: '🚗' },
|
||||
{ id: 'webapplist', label: 'H5 应用', icon: '🌐' },
|
||||
{ id: 'applist', label: '安卓 APP', icon: '📱' },
|
||||
{ id: 'settings', label: '设置', icon: '⚙️' },
|
||||
{ id: 'home', label: '首页', icon: SVG_ICONS.home },
|
||||
{ id: 'immersive', label: '沉浸首页', icon: SVG_ICONS.immersive },
|
||||
{ id: 'webapplist', label: 'H5 应用', icon: SVG_ICONS.webapp },
|
||||
{ id: 'applist', label: '安卓 APP', icon: SVG_ICONS.app },
|
||||
{ id: 'settings', label: '设置', icon: SVG_ICONS.settings },
|
||||
];
|
||||
|
||||
const router = {
|
||||
@@ -14,10 +24,11 @@ const router = {
|
||||
this.current = id;
|
||||
document.querySelectorAll('[data-page]').forEach(el =>
|
||||
el.classList.toggle('active', el.dataset.page === id));
|
||||
// 非首页时侧边栏顶部显示小时间
|
||||
// Show the small clock at the sidebar top when not on home
|
||||
// 非首页时侧边栏顶部显示小时间(容器始终占位,仅切换可见性避免撑动图标)
|
||||
// Show the small clock at the sidebar top when not on home (the container
|
||||
// always occupies space; only toggle visibility to avoid shifting icons)
|
||||
const clock = document.getElementById('rail-clock');
|
||||
if (clock) clock.style.display = (id === 'home') ? 'none' : 'block';
|
||||
if (clock) clock.style.visibility = (id === 'home') ? 'hidden' : 'visible';
|
||||
if (id === 'home') window.updateHomeCards && window.updateHomeCards();
|
||||
if (id === 'applist') window.loadAppList && window.loadAppList();
|
||||
if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList();
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 主题三态管理:跟随系统 / 浅色 / 深色,存 localStorage,通过 data-theme 覆盖 CSS token
|
||||
// Theme tri-state management: system / light / dark, persisted in localStorage,
|
||||
// overriding CSS tokens via the data-theme attribute
|
||||
(function () {
|
||||
const KEY = 'hearth-theme';
|
||||
const ORDER = ['system', 'light', 'dark'];
|
||||
const LABELS = { system: '跟随系统', light: '浅色', dark: '深色' };
|
||||
|
||||
window.theme = {
|
||||
// 当前主题(默认跟随系统)
|
||||
get() {
|
||||
return localStorage.getItem(KEY) || 'system';
|
||||
},
|
||||
// 应用主题:写入 data-theme 属性(system 时移除)
|
||||
apply(t) {
|
||||
localStorage.setItem(KEY, t);
|
||||
const root = document.documentElement;
|
||||
if (t === 'light') root.setAttribute('data-theme', 'light');
|
||||
else if (t === 'dark') root.setAttribute('data-theme', 'dark');
|
||||
else root.removeAttribute('data-theme');
|
||||
},
|
||||
// 三态循环,返回新主题
|
||||
cycle() {
|
||||
const cur = this.get();
|
||||
const next = ORDER[(ORDER.indexOf(cur) + 1) % ORDER.length];
|
||||
this.apply(next);
|
||||
return next;
|
||||
},
|
||||
// 主题显示名
|
||||
label(t) {
|
||||
return LABELS[t] || t;
|
||||
},
|
||||
};
|
||||
|
||||
// 启动即应用已存主题
|
||||
window.theme.apply(window.theme.get());
|
||||
})();
|
||||
@@ -3,6 +3,7 @@ package top.yeij.hearth
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.TextUtils
|
||||
@@ -25,8 +26,10 @@ import top.yeij.hearth.webapp.OkHttpHttpClient
|
||||
import top.yeij.hearth.webapp.Tab
|
||||
import top.yeij.hearth.webapp.WebAppContainer
|
||||
import top.yeij.hearth.webapp.WebAppRepository
|
||||
import top.yeij.hearth.webview.BrightnessProvider
|
||||
import top.yeij.hearth.webview.JsBridge
|
||||
import top.yeij.hearth.webview.NotificationAccessProvider
|
||||
import top.yeij.hearth.webview.ServerUrlProvider
|
||||
import top.yeij.hearth.webview.WebAppHost
|
||||
import top.yeij.hearth.webview.WebViewManager
|
||||
import java.io.File
|
||||
@@ -63,6 +66,47 @@ class MainActivity : Activity() {
|
||||
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
// 系统亮度访问实现:读/写 Settings.System.SCREEN_BRIGHTNESS,需 WRITE_SETTINGS 授权
|
||||
// System brightness implementation: read/write Settings.System.SCREEN_BRIGHTNESS,
|
||||
// requiring the WRITE_SETTINGS grant
|
||||
private val brightnessProvider = object : BrightnessProvider {
|
||||
override fun getSystemBrightness(): Int =
|
||||
Settings.System.getInt(contentResolver, Settings.System.SCREEN_BRIGHTNESS, 128)
|
||||
|
||||
override fun setSystemBrightness(value: Int) {
|
||||
Settings.System.putInt(
|
||||
contentResolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS,
|
||||
value.coerceIn(0, 255)
|
||||
)
|
||||
}
|
||||
|
||||
override fun canWriteSettings(): Boolean = Settings.System.canWrite(this@MainActivity)
|
||||
|
||||
override fun requestWriteSettings() {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:$packageName"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器地址存储实现:SharedPreferences 持久化,设置时同步更新 WebAppRepository
|
||||
// Server URL storage implementation: persisted in SharedPreferences, syncing the
|
||||
// WebAppRepository manifest URL on change
|
||||
private val serverUrlProvider = object : ServerUrlProvider {
|
||||
private val prefs by lazy { getSharedPreferences("hearth", MODE_PRIVATE) }
|
||||
|
||||
override fun getServerUrl(): String =
|
||||
prefs.getString("server_url", MANIFEST_URL) ?: MANIFEST_URL
|
||||
|
||||
override fun setServerUrl(url: String) {
|
||||
prefs.edit().putString("server_url", url).apply()
|
||||
if (::webAppRepository.isInitialized) webAppRepository.setManifestUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var webAppRepository: WebAppRepository
|
||||
private lateinit var mediaSource: MediaSessionSource
|
||||
private lateinit var root: FrameLayout
|
||||
private lateinit var desktopWebView: WebView
|
||||
@@ -96,18 +140,21 @@ class MainActivity : Activity() {
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
val cache = CacheManager(File(filesDir, "cache"))
|
||||
val http = OkHttpHttpClient()
|
||||
webAppRepository = WebAppRepository(http, cache, serverUrlProvider.getServerUrl())
|
||||
bridge = JsBridge(
|
||||
deviceWidthPx = dm.widthPixels,
|
||||
deviceHeightPx = dm.heightPixels,
|
||||
density = dm.density,
|
||||
darkMode = darkMode,
|
||||
appRepository = AppRepository(AndroidAppSource(this)),
|
||||
webAppRepository = WebAppRepository(http, cache, MANIFEST_URL),
|
||||
webAppRepository = webAppRepository,
|
||||
cardRepository = CardRepository(http, cache, CATALOG_URL),
|
||||
webAppContainer = webAppContainer,
|
||||
webAppHost = webAppHost,
|
||||
postToMainThread = { desktopWebView.post(it) },
|
||||
notificationAccess = notificationAccess,
|
||||
brightness = brightnessProvider,
|
||||
serverUrl = serverUrlProvider,
|
||||
)
|
||||
root = FrameLayout(this)
|
||||
setContentView(root)
|
||||
|
||||
@@ -27,10 +27,17 @@ interface HttpClient {
|
||||
class WebAppRepository(
|
||||
private val http: HttpClient,
|
||||
private val cache: CacheManager,
|
||||
private val manifestUrl: String,
|
||||
private var manifestUrl: String,
|
||||
) {
|
||||
private val gson = Gson()
|
||||
|
||||
// 更新清单地址(设置页修改服务器地址后调用)
|
||||
// Update the manifest URL (called after the settings page changes the server URL)
|
||||
fun setManifestUrl(url: String) {
|
||||
manifestUrl = url
|
||||
Log.d(TAG, "setManifestUrl: $url")
|
||||
}
|
||||
|
||||
fun fetchManifest(): List<WebApp> {
|
||||
val body = http.get(manifestUrl)
|
||||
if (body != null) {
|
||||
|
||||
@@ -19,6 +19,22 @@ interface NotificationAccessProvider {
|
||||
fun requestAccess()
|
||||
}
|
||||
|
||||
// 系统亮度访问接口:由 MainActivity 实现(Settings.System + WRITE_SETTINGS 授权)
|
||||
// System brightness provider implemented by MainActivity (Settings.System + WRITE_SETTINGS)
|
||||
interface BrightnessProvider {
|
||||
fun getSystemBrightness(): Int
|
||||
fun setSystemBrightness(value: Int)
|
||||
fun canWriteSettings(): Boolean
|
||||
fun requestWriteSettings()
|
||||
}
|
||||
|
||||
// 服务器地址存储接口:由 MainActivity 实现(SharedPreferences 持久化)
|
||||
// Server URL storage provider implemented by MainActivity (SharedPreferences)
|
||||
interface ServerUrlProvider {
|
||||
fun getServerUrl(): String
|
||||
fun setServerUrl(url: String)
|
||||
}
|
||||
|
||||
class JsBridge(
|
||||
private val deviceWidthPx: Int,
|
||||
private val deviceHeightPx: Int,
|
||||
@@ -35,6 +51,12 @@ class JsBridge(
|
||||
// 通知使用权访问接口(检查/申请授权),供设置页授权项使用
|
||||
// Notification-access provider (check/request) for the settings permission item
|
||||
private val notificationAccess: NotificationAccessProvider? = null,
|
||||
// 系统亮度接口(读取/设置 + WRITE_SETTINGS 授权)
|
||||
// System brightness provider (get/set + WRITE_SETTINGS grant)
|
||||
private val brightness: BrightnessProvider? = null,
|
||||
// 服务器地址存储接口
|
||||
// Server URL storage provider
|
||||
private val serverUrl: ServerUrlProvider? = null,
|
||||
) {
|
||||
private val gson = com.google.gson.Gson()
|
||||
|
||||
@@ -210,6 +232,55 @@ class JsBridge(
|
||||
notificationAccess?.requestAccess()
|
||||
}
|
||||
|
||||
// 返回系统亮度(0-255,未接入返回 -1)
|
||||
// Return system brightness (0-255, -1 when not wired)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun getSystemBrightness(): Int = brightness?.getSystemBrightness() ?: -1
|
||||
|
||||
// 设置系统亮度(0-255)
|
||||
// Set system brightness (0-255)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun setSystemBrightness(value: Int) {
|
||||
brightness?.setSystemBrightness(value)
|
||||
}
|
||||
|
||||
// 是否已授予「修改系统设置」权限(WRITE_SETTINGS)
|
||||
// Whether the WRITE_SETTINGS permission is granted
|
||||
@android.webkit.JavascriptInterface
|
||||
fun canWriteSettings(): Boolean = brightness?.canWriteSettings() ?: false
|
||||
|
||||
// 跳转系统「修改系统设置」授权页
|
||||
// Jump to the system WRITE_SETTINGS grant page
|
||||
@android.webkit.JavascriptInterface
|
||||
fun requestWriteSettings() {
|
||||
brightness?.requestWriteSettings()
|
||||
}
|
||||
|
||||
// 返回 webAPP 服务器地址(未接入返回空字符串)
|
||||
// Return the webAPP server URL (empty string when not wired)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun getServerUrl(): String = serverUrl?.getServerUrl() ?: ""
|
||||
|
||||
// 设置 webAPP 服务器地址(持久化 + 后续拉取使用)
|
||||
// Set the webAPP server URL (persisted and used by subsequent fetches)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun setServerUrl(url: String) {
|
||||
Log.d("HearthBridge", "setServerUrl: $url")
|
||||
serverUrl?.setServerUrl(url)
|
||||
}
|
||||
|
||||
// 重新拉取 webAPP 清单 + 卡片目录,返回 { count }
|
||||
// Re-fetch the webAPP manifest + card catalog, return { count }
|
||||
@android.webkit.JavascriptInterface
|
||||
fun refreshManifest(): String {
|
||||
manifestCache = null
|
||||
val apps = webAppRepository?.fetchManifest() ?: emptyList<WebApp>()
|
||||
if (apps.isNotEmpty()) manifestCache = apps
|
||||
val cards = cardRepository?.fetchCatalog() ?: emptyList<Card>()
|
||||
Log.d("HearthBridge", "refreshManifest: ${apps.size} apps, ${cards.size} cards")
|
||||
return gson.toJson(mapOf("count" to apps.size))
|
||||
}
|
||||
|
||||
// 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null")
|
||||
// Register media session listener; push callbacks to H5 (push "null" when info is null)
|
||||
fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {
|
||||
|
||||
Reference in New Issue
Block a user