feat: settings interactions (theme/mask/brightness/server-url), svg icons, seconds display, page transition

This commit is contained in:
2026-08-16 18:25:28 +08:00
parent e2a289a4b0
commit c9dcb88647
11 changed files with 510 additions and 29 deletions
+64 -5
View File
@@ -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);
}
+13 -1
View File
@@ -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
View File
@@ -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>
+40
View File
@@ -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';
},
};
})();
+12 -7
View File
@@ -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);
}
// 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局
+192 -6
View File
@@ -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, '&quot;') + '">';
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 = '失败 ';
});
};
}
+19 -8
View File
@@ -1,11 +1,21 @@
// 桌面路由:侧边栏 5 页 + 页面切换 + 顶部时间
// Launcher router: 5 sidebar pages + page switching + top clock
// 侧边栏图标:内联 SVGstroke=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();
+37
View File
@@ -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) {