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 */
|
/* Wallpaper transparency: html/body keep transparent to reveal native wallpaper */
|
||||||
html, body { background: transparent; }
|
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 {
|
body {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: -apple-system, "MiSans", "PingFang SC", sans-serif;
|
font-family: -apple-system, "MiSans", "PingFang SC", sans-serif;
|
||||||
overflow: hidden;
|
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 */
|
/* Sidebar: soft-glass container, not a solid color */
|
||||||
@@ -35,7 +48,16 @@ body {
|
|||||||
|
|
||||||
.rail.expanded { width: 240px; align-items: stretch; padding: 12px 8px; }
|
.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 {
|
.rail-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -50,7 +72,8 @@ body {
|
|||||||
cursor: pointer;
|
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 .lbl { font-size: 11px; }
|
||||||
|
|
||||||
.rail-item.active { background: var(--accent); color: #fff; }
|
.rail-item.active { background: var(--accent); color: #fff; }
|
||||||
@@ -58,7 +81,18 @@ body {
|
|||||||
.content { flex: 1; overflow: hidden; }
|
.content { flex: 1; overflow: hidden; }
|
||||||
|
|
||||||
.page { display: none; height: 100%; }
|
.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 */
|
/* Home three-column layout: three equal vertical columns, cards stack downward */
|
||||||
@@ -99,6 +133,15 @@ body {
|
|||||||
line-height: 1;
|
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 {
|
.time-card .sub {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -167,6 +210,9 @@ body {
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
|
/* 行靠顶部对齐,避免结果只有一行时被拉伸到接近容器高度 */
|
||||||
|
/* Align rows to the top so a single result row doesn't stretch to container height */
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
@@ -347,6 +393,19 @@ body {
|
|||||||
|
|
||||||
.st-item .lbl { font-size: 15px; }
|
.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 */
|
/* Trailing arrow: dim color */
|
||||||
.st-item .arrow {
|
.st-item .arrow {
|
||||||
@@ -400,7 +459,7 @@ body {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 60%;
|
width: var(--fill, 60%);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
--accent: #ff6900;
|
--accent: #ff6900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 系统深色:仅在未强制浅色时生效(跟随系统) */
|
||||||
|
/* System dark: only applies when not forced light (follow-system) */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root:not([data-theme="light"]) {
|
||||||
--glass-bg: rgba(21, 21, 24, 0.55); /* 深色半透明 / dark translucent */
|
--glass-bg: rgba(21, 21, 24, 0.55); /* 深色半透明 / dark translucent */
|
||||||
--glass-border: rgba(255, 255, 255, 0.08);
|
--glass-border: rgba(255, 255, 255, 0.08);
|
||||||
--text: #ffffff;
|
--text: #ffffff;
|
||||||
@@ -17,3 +19,13 @@
|
|||||||
--accent: #ff6900;
|
--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>
|
<title>Hearth</title>
|
||||||
<link rel="stylesheet" href="css/tokens.css">
|
<link rel="stylesheet" href="css/tokens.css">
|
||||||
<link rel="stylesheet" href="css/app.css">
|
<link rel="stylesheet" href="css/app.css">
|
||||||
|
<script src="js/theme.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="wallpaper-dim"></div>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<aside class="rail" id="rail">
|
<aside class="rail" id="rail">
|
||||||
<div class="rail-clock" id="rail-clock">14:30</div>
|
<div class="rail-clock" id="rail-clock">14:30</div>
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script src="js/bridge.js"></script>
|
<script src="js/bridge.js"></script>
|
||||||
|
<script src="js/mask.js"></script>
|
||||||
<script src="js/cards/cards.js"></script>
|
<script src="js/cards/cards.js"></script>
|
||||||
<script src="js/router.js"></script>
|
<script src="js/router.js"></script>
|
||||||
<script src="js/pages/applist.js"></script>
|
<script src="js/pages/applist.js"></script>
|
||||||
@@ -48,6 +51,9 @@
|
|||||||
document.getElementById('rail-clock').textContent = `${hh}:${mm}`;
|
document.getElementById('rail-clock').textContent = `${hh}:${mm}`;
|
||||||
}, 10000);
|
}, 10000);
|
||||||
router.navigate('home');
|
router.navigate('home');
|
||||||
|
// 初始化背景遮罩(夜间主题 + 已启用时生效)
|
||||||
|
// Apply wallpaper dim on startup (active under dark theme when enabled)
|
||||||
|
window.mask.apply();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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();
|
startTimeCardTicker();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 内置大字时间卡:当前时间 + 日期
|
// 内置大字时间卡:当前时间(时:分 + 小号秒)+ 日期
|
||||||
// Builtin big time card: current time + date
|
// Builtin big time card: current time (HH:MM + small seconds) + date
|
||||||
function renderTimeCard() {
|
function renderTimeCard() {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = 'card time-card';
|
el.className = 'card time-card';
|
||||||
const now = new Date();
|
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;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,13 +56,17 @@ function fmtTime(d) {
|
|||||||
return `${hh}:${mm}`;
|
return `${hh}:${mm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtSec(d) {
|
||||||
|
return `:${String(d.getSeconds()).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function fmtDate(d) {
|
function fmtDate(d) {
|
||||||
const days = ['日', '一', '二', '三', '四', '五', '六'];
|
const days = ['日', '一', '二', '三', '四', '五', '六'];
|
||||||
return `${d.getMonth() + 1}月${d.getDate()}日 周${days[d.getDay()]}`;
|
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;
|
let timeTicker = null;
|
||||||
function startTimeCardTicker() {
|
function startTimeCardTicker() {
|
||||||
if (timeTicker) return;
|
if (timeTicker) return;
|
||||||
@@ -70,9 +74,10 @@ function startTimeCardTicker() {
|
|||||||
const card = document.querySelector('.time-card');
|
const card = document.querySelector('.time-card');
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const now = new Date();
|
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);
|
card.querySelector('.sub').textContent = fmtDate(now);
|
||||||
}, 10000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局
|
// 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局
|
||||||
|
|||||||
@@ -1,26 +1,112 @@
|
|||||||
// 设置页:Miuix Preference 分组 + 玻璃条目(通知使用权为可用交互项)
|
// 设置页:Miuix Preference 分组 + 玻璃条目,含主题/遮罩/亮度/权限/网络交互
|
||||||
// Settings page: Miuix Preference groups + glass items (notification access is interactive)
|
// Settings page: Miuix Preference groups + glass items with interactions for
|
||||||
|
// theme / mask / brightness / permission / network
|
||||||
window.loadSettings = function () {
|
window.loadSettings = function () {
|
||||||
const page = document.querySelector('[data-page="settings"]');
|
const page = document.querySelector('[data-page="settings"]');
|
||||||
if (page.dataset.loaded) return; page.dataset.loaded = '1';
|
if (page.dataset.loaded) return; page.dataset.loaded = '1';
|
||||||
page.innerHTML = `<div class="st-wrap">
|
page.innerHTML = `<div class="st-wrap">
|
||||||
<div class="st-group">显示</div>
|
<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" id="theme-item">
|
||||||
<div class="st-item"><span class="lbl">屏幕亮度</span><span class="slider"><i></i></span></div>
|
<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-group">权限</div>
|
||||||
<div class="st-item" id="notif-access">
|
<div class="st-item" id="notif-access">
|
||||||
<span class="lbl">通知使用权(媒体卡)</span>
|
<span class="lbl">通知使用权(媒体卡)</span>
|
||||||
<span class="arrow" id="notif-access-state">›</span>
|
<span class="arrow" id="notif-access-state">›</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="st-group">网络</div>
|
<div class="st-group">网络</div>
|
||||||
<div class="st-item"><span class="lbl">webAPP 服务器地址</span><span class="arrow">›</span></div>
|
<div class="st-item" id="server-url-item">
|
||||||
<div class="st-item"><span class="lbl">检查更新</span><span class="arrow">›</span></div>
|
<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-group">关于</div>
|
||||||
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow">›</span></div>
|
<div class="st-item"><span class="lbl">版本 0.1.0</span><span class="arrow">›</span></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
setupTheme(page);
|
||||||
|
setupMask(page);
|
||||||
setupNotificationAccess(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
|
// Notification-access item: async state check, tap to open the system grant page
|
||||||
function setupNotificationAccess(page) {
|
function setupNotificationAccess(page) {
|
||||||
@@ -34,3 +120,103 @@ function setupNotificationAccess(page) {
|
|||||||
bridge.call('requestNotificationAccess').catch(() => {});
|
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 页 + 页面切换 + 顶部时间
|
// 桌面路由:侧边栏 5 页 + 页面切换 + 顶部时间
|
||||||
// Launcher router: 5 sidebar pages + page switching + top clock
|
// 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 = [
|
const PAGES = [
|
||||||
{ id: 'home', label: '首页', icon: '🏠' },
|
{ id: 'home', label: '首页', icon: SVG_ICONS.home },
|
||||||
{ id: 'immersive', label: '沉浸首页', icon: '🚗' },
|
{ id: 'immersive', label: '沉浸首页', icon: SVG_ICONS.immersive },
|
||||||
{ id: 'webapplist', label: 'H5 应用', icon: '🌐' },
|
{ id: 'webapplist', label: 'H5 应用', icon: SVG_ICONS.webapp },
|
||||||
{ id: 'applist', label: '安卓 APP', icon: '📱' },
|
{ id: 'applist', label: '安卓 APP', icon: SVG_ICONS.app },
|
||||||
{ id: 'settings', label: '设置', icon: '⚙️' },
|
{ id: 'settings', label: '设置', icon: SVG_ICONS.settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = {
|
const router = {
|
||||||
@@ -14,10 +24,11 @@ const router = {
|
|||||||
this.current = id;
|
this.current = id;
|
||||||
document.querySelectorAll('[data-page]').forEach(el =>
|
document.querySelectorAll('[data-page]').forEach(el =>
|
||||||
el.classList.toggle('active', el.dataset.page === id));
|
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');
|
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 === 'home') window.updateHomeCards && window.updateHomeCards();
|
||||||
if (id === 'applist') window.loadAppList && window.loadAppList();
|
if (id === 'applist') window.loadAppList && window.loadAppList();
|
||||||
if (id === 'webapplist') window.loadWebAppList && window.loadWebAppList();
|
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.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.text.TextUtils
|
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.Tab
|
||||||
import top.yeij.hearth.webapp.WebAppContainer
|
import top.yeij.hearth.webapp.WebAppContainer
|
||||||
import top.yeij.hearth.webapp.WebAppRepository
|
import top.yeij.hearth.webapp.WebAppRepository
|
||||||
|
import top.yeij.hearth.webview.BrightnessProvider
|
||||||
import top.yeij.hearth.webview.JsBridge
|
import top.yeij.hearth.webview.JsBridge
|
||||||
import top.yeij.hearth.webview.NotificationAccessProvider
|
import top.yeij.hearth.webview.NotificationAccessProvider
|
||||||
|
import top.yeij.hearth.webview.ServerUrlProvider
|
||||||
import top.yeij.hearth.webview.WebAppHost
|
import top.yeij.hearth.webview.WebAppHost
|
||||||
import top.yeij.hearth.webview.WebViewManager
|
import top.yeij.hearth.webview.WebViewManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -63,6 +66,47 @@ class MainActivity : Activity() {
|
|||||||
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
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 mediaSource: MediaSessionSource
|
||||||
private lateinit var root: FrameLayout
|
private lateinit var root: FrameLayout
|
||||||
private lateinit var desktopWebView: WebView
|
private lateinit var desktopWebView: WebView
|
||||||
@@ -96,18 +140,21 @@ class MainActivity : Activity() {
|
|||||||
Configuration.UI_MODE_NIGHT_YES
|
Configuration.UI_MODE_NIGHT_YES
|
||||||
val cache = CacheManager(File(filesDir, "cache"))
|
val cache = CacheManager(File(filesDir, "cache"))
|
||||||
val http = OkHttpHttpClient()
|
val http = OkHttpHttpClient()
|
||||||
|
webAppRepository = WebAppRepository(http, cache, serverUrlProvider.getServerUrl())
|
||||||
bridge = JsBridge(
|
bridge = JsBridge(
|
||||||
deviceWidthPx = dm.widthPixels,
|
deviceWidthPx = dm.widthPixels,
|
||||||
deviceHeightPx = dm.heightPixels,
|
deviceHeightPx = dm.heightPixels,
|
||||||
density = dm.density,
|
density = dm.density,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
appRepository = AppRepository(AndroidAppSource(this)),
|
appRepository = AppRepository(AndroidAppSource(this)),
|
||||||
webAppRepository = WebAppRepository(http, cache, MANIFEST_URL),
|
webAppRepository = webAppRepository,
|
||||||
cardRepository = CardRepository(http, cache, CATALOG_URL),
|
cardRepository = CardRepository(http, cache, CATALOG_URL),
|
||||||
webAppContainer = webAppContainer,
|
webAppContainer = webAppContainer,
|
||||||
webAppHost = webAppHost,
|
webAppHost = webAppHost,
|
||||||
postToMainThread = { desktopWebView.post(it) },
|
postToMainThread = { desktopWebView.post(it) },
|
||||||
notificationAccess = notificationAccess,
|
notificationAccess = notificationAccess,
|
||||||
|
brightness = brightnessProvider,
|
||||||
|
serverUrl = serverUrlProvider,
|
||||||
)
|
)
|
||||||
root = FrameLayout(this)
|
root = FrameLayout(this)
|
||||||
setContentView(root)
|
setContentView(root)
|
||||||
|
|||||||
@@ -27,10 +27,17 @@ interface HttpClient {
|
|||||||
class WebAppRepository(
|
class WebAppRepository(
|
||||||
private val http: HttpClient,
|
private val http: HttpClient,
|
||||||
private val cache: CacheManager,
|
private val cache: CacheManager,
|
||||||
private val manifestUrl: String,
|
private var manifestUrl: String,
|
||||||
) {
|
) {
|
||||||
private val gson = Gson()
|
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> {
|
fun fetchManifest(): List<WebApp> {
|
||||||
val body = http.get(manifestUrl)
|
val body = http.get(manifestUrl)
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
|
|||||||
@@ -19,6 +19,22 @@ interface NotificationAccessProvider {
|
|||||||
fun requestAccess()
|
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(
|
class JsBridge(
|
||||||
private val deviceWidthPx: Int,
|
private val deviceWidthPx: Int,
|
||||||
private val deviceHeightPx: Int,
|
private val deviceHeightPx: Int,
|
||||||
@@ -35,6 +51,12 @@ class JsBridge(
|
|||||||
// 通知使用权访问接口(检查/申请授权),供设置页授权项使用
|
// 通知使用权访问接口(检查/申请授权),供设置页授权项使用
|
||||||
// Notification-access provider (check/request) for the settings permission item
|
// Notification-access provider (check/request) for the settings permission item
|
||||||
private val notificationAccess: NotificationAccessProvider? = null,
|
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()
|
private val gson = com.google.gson.Gson()
|
||||||
|
|
||||||
@@ -210,6 +232,55 @@ class JsBridge(
|
|||||||
notificationAccess?.requestAccess()
|
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")
|
// 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null")
|
||||||
// Register media session listener; push callbacks to H5 (push "null" when info is null)
|
// Register media session listener; push callbacks to H5 (push "null" when info is null)
|
||||||
fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {
|
fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {
|
||||||
|
|||||||
Reference in New Issue
Block a user