From c9dcb88647ad2331505669eb530e2dcc459799f6 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 18:25:28 +0800 Subject: [PATCH] feat: settings interactions (theme/mask/brightness/server-url), svg icons, seconds display, page transition --- app/src/main/assets/h5/css/app.css | 69 +++++- app/src/main/assets/h5/css/tokens.css | 14 +- app/src/main/assets/h5/index.html | 6 + app/src/main/assets/h5/js/mask.js | 40 ++++ app/src/main/assets/h5/js/pages/home.js | 19 +- app/src/main/assets/h5/js/pages/settings.js | 198 +++++++++++++++++- app/src/main/assets/h5/js/router.js | 27 ++- app/src/main/assets/h5/js/theme.js | 37 ++++ .../main/java/top/yeij/hearth/MainActivity.kt | 49 ++++- .../yeij/hearth/webapp/WebAppRepository.kt | 9 +- .../java/top/yeij/hearth/webview/JsBridge.kt | 71 +++++++ 11 files changed, 510 insertions(+), 29 deletions(-) create mode 100644 app/src/main/assets/h5/js/mask.js create mode 100644 app/src/main/assets/h5/js/theme.js diff --git a/app/src/main/assets/h5/css/app.css b/app/src/main/assets/h5/css/app.css index 1910930..84ac6a6 100644 --- a/app/src/main/assets/h5/css/app.css +++ b/app/src/main/assets/h5/css/app.css @@ -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); } diff --git a/app/src/main/assets/h5/css/tokens.css b/app/src/main/assets/h5/css/tokens.css index a045363..a22c151 100644 --- a/app/src/main/assets/h5/css/tokens.css +++ b/app/src/main/assets/h5/css/tokens.css @@ -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; +} diff --git a/app/src/main/assets/h5/index.html b/app/src/main/assets/h5/index.html index 4930704..8286d9e 100644 --- a/app/src/main/assets/h5/index.html +++ b/app/src/main/assets/h5/index.html @@ -6,8 +6,10 @@ Hearth + +
+ @@ -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(); diff --git a/app/src/main/assets/h5/js/mask.js b/app/src/main/assets/h5/js/mask.js new file mode 100644 index 0000000..d48610d --- /dev/null +++ b/app/src/main/assets/h5/js/mask.js @@ -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'; + }, + }; +})(); diff --git a/app/src/main/assets/h5/js/pages/home.js b/app/src/main/assets/h5/js/pages/home.js index 20eb0f4..e2f8f15 100644 --- a/app/src/main/assets/h5/js/pages/home.js +++ b/app/src/main/assets/h5/js/pages/home.js @@ -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 = `
${fmtTime(now)}
${fmtDate(now)}
`; + el.innerHTML = `
${fmtTime(now)}${fmtSec(now)}
${fmtDate(now)}
`; 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); } // 原生媒体会话事件:有媒体时在中间栏渲染媒体卡,无媒体时降级重新布局 diff --git a/app/src/main/assets/h5/js/pages/settings.js b/app/src/main/assets/h5/js/pages/settings.js index 5944f30..cc2e887 100644 --- a/app/src/main/assets/h5/js/pages/settings.js +++ b/app/src/main/assets/h5/js/pages/settings.js @@ -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 = `
显示
-
跟随系统主题
-
屏幕亮度
+
+ 主题 + 跟随系统 +
+
+ 屏幕亮度 + +
+ + +
权限
通知使用权(媒体卡)
网络
-
webAPP 服务器地址
-
检查更新
+
+ webAPP 服务器地址 + +
+
+ 检查更新 + +
关于
版本 0.1.0
`; + 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 = ''; + 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 = ''; + 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 = '失败 ›'; + }); + }; +} diff --git a/app/src/main/assets/h5/js/router.js b/app/src/main/assets/h5/js/router.js index ffeb40a..0458ad7 100644 --- a/app/src/main/assets/h5/js/router.js +++ b/app/src/main/assets/h5/js/router.js @@ -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: '', + immersive: '', + webapp: '', + app: '', + settings: '', +}; + 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(); diff --git a/app/src/main/assets/h5/js/theme.js b/app/src/main/assets/h5/js/theme.js new file mode 100644 index 0000000..cf677c5 --- /dev/null +++ b/app/src/main/assets/h5/js/theme.js @@ -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()); +})(); diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index ea5a112..bf27ca9 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -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) diff --git a/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt index 2622e17..8cd8e6b 100644 --- a/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt +++ b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt @@ -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 { val body = http.get(manifestUrl) if (body != null) { diff --git a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt index 738e39d..1675c0f 100644 --- a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt +++ b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt @@ -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() + if (apps.isNotEmpty()) manifestCache = apps + val cards = cardRepository?.fetchCatalog() ?: emptyList() + 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) {