feat: page preload, card grid span, wallpaper into webview for glass blur
This commit is contained in:
@@ -120,25 +120,22 @@ body {
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* 首页三栏布局:三等分纵向列,卡片向下堆叠 */
|
||||
/* Home three-column layout: three equal vertical columns, cards stack downward */
|
||||
/* 首页卡片 Grid:三等分列,卡片可跨栏(span-2/3),行均分高度 */
|
||||
/* Home card grid: three equal columns, cards can span (span-2/3), rows split evenly */
|
||||
.tri-col {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-auto-rows: 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.card.span-2 { grid-column: span 2; }
|
||||
.card.span-3 { grid-column: span 3; }
|
||||
|
||||
/* 卡片:柔光玻璃拟态(复用 Task 4 token,非纯色背景) */
|
||||
/* Card: soft-glass morphism (reuse Task 4 tokens, not a solid color) */
|
||||
.card {
|
||||
@@ -161,7 +158,9 @@ body {
|
||||
/* 时钟卡缩放权重小:占栏高比例小于其他卡片 */
|
||||
/* Time card grows less: takes a smaller share of column height than other cards */
|
||||
.time-card {
|
||||
flex-grow: 0.5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-card .big {
|
||||
@@ -185,6 +184,13 @@ body {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* 媒体卡:flex column,标题靠上、进度/控制靠下 */
|
||||
/* Media card: flex column, title top, progress/controls bottom */
|
||||
.media-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 媒体卡:正在播放标签 + 标题/艺术家 + 进度条 */
|
||||
/* Media card: playing caption + title/artist + progress bar */
|
||||
.media-card .cap {
|
||||
@@ -212,7 +218,8 @@ body {
|
||||
}
|
||||
|
||||
.media-card .prog {
|
||||
margin-top: 12px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: var(--glass-border);
|
||||
@@ -225,6 +232,32 @@ body {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* 媒体控制按钮:上一首/暂停/下一首 */
|
||||
/* Media controls: previous / play-pause / next */
|
||||
.media-card .controls {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-card .controls button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--glass-border);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media-card .controls button:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 安卓 APP 列表页:搜索框 + 网格 */
|
||||
/* Android app list page: search box + grid */
|
||||
.search { padding: 16px; }
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</main>
|
||||
</div>
|
||||
<script src="js/bridge.js"></script>
|
||||
<script src="js/preload.js"></script>
|
||||
<script src="js/mask.js"></script>
|
||||
<script src="js/cards/cards.js"></script>
|
||||
<script src="js/router.js"></script>
|
||||
@@ -55,6 +56,19 @@
|
||||
// 初始化背景遮罩(夜间主题 + 已启用时生效)
|
||||
// Apply wallpaper dim on startup (active under dark theme when enabled)
|
||||
window.mask.apply();
|
||||
// 加载壁纸为 body 背景:玻璃效果 backdrop-filter 需要 WebView 内部有背景可模糊
|
||||
// Load the wallpaper as the body background: the glass backdrop-filter needs
|
||||
// in-WebView content behind cards to blur
|
||||
bridge.call('getWallpaper').then(wp => {
|
||||
if (wp) {
|
||||
document.body.style.backgroundImage = 'url(' + wp + ')';
|
||||
document.body.style.backgroundSize = 'cover';
|
||||
document.body.style.backgroundPosition = 'center';
|
||||
}
|
||||
}).catch(() => {});
|
||||
// 首屏渲染后延迟预加载其他页面数据,减少切页等待
|
||||
// Preload other pages' data after first paint to reduce switch latency
|
||||
setTimeout(() => window.preload.loadAll(), 800);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,9 @@ window.loadAppList = async function () {
|
||||
page.innerHTML = `
|
||||
<div class="search"><input id="app-search" placeholder="搜索应用…"></div>
|
||||
<div class="grid" id="app-grid"></div>`;
|
||||
const apps = JSON.parse(await bridge.call('listApps'));
|
||||
// 优先用预加载缓存,未就绪则实时加载
|
||||
// Prefer the preloaded cache; fetch live when not ready yet
|
||||
const apps = window.preload.apps || JSON.parse(await bridge.call('listApps'));
|
||||
const grid = document.getElementById('app-grid');
|
||||
const render = (list) => {
|
||||
grid.textContent = '';
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
// (big time card + generic placeholders for the rest)
|
||||
window.updateHomeCards = async function () {
|
||||
const page = document.querySelector('[data-page="home"]');
|
||||
page.innerHTML = `<div class="tri-col" id="tri-col">
|
||||
<div class="col" id="col-0"></div><div class="col" id="col-1"></div><div class="col" id="col-2"></div>
|
||||
</div>`;
|
||||
page.innerHTML = `<div class="tri-col" id="tri-col"></div>`;
|
||||
let catalog = [];
|
||||
try {
|
||||
catalog = JSON.parse(await bridge.call('fetchCards'));
|
||||
// 优先用预加载缓存,未就绪则实时加载
|
||||
// Prefer the preloaded cache; fetch live when not ready yet
|
||||
catalog = window.preload.cards || JSON.parse(await bridge.call('fetchCards'));
|
||||
} catch (e) {
|
||||
// 桥接不可用(如未接线)时按空目录处理
|
||||
// Treat bridge failure as an empty catalog
|
||||
@@ -20,27 +20,33 @@ window.updateHomeCards = async function () {
|
||||
if (!catalog.some((c) => c.id === 'time')) {
|
||||
catalog.push({ id: 'time', name: '大字时间', priority: 0 });
|
||||
}
|
||||
const cols = cards.layout(catalog);
|
||||
cols.forEach((ids, i) => {
|
||||
const col = document.getElementById(`col-${i}`);
|
||||
ids.forEach((id) => {
|
||||
if (id === 'time') col.appendChild(renderTimeCard());
|
||||
else col.appendChild(renderGenericCard(id));
|
||||
});
|
||||
});
|
||||
// 若有当前媒体,覆盖中间栏为媒体卡(切页回来后恢复媒体卡)
|
||||
// If there is active media, replace the middle column with the media card
|
||||
// (restores the media card when navigating back to home)
|
||||
if (currentMedia) {
|
||||
const mediaCol = document.getElementById('col-1');
|
||||
if (mediaCol) {
|
||||
mediaCol.innerHTML = '';
|
||||
mediaCol.appendChild(renderMediaCard(currentMedia));
|
||||
}
|
||||
}
|
||||
renderCards(catalog);
|
||||
startTimeCardTicker();
|
||||
};
|
||||
|
||||
// 卡片 Grid 渲染:media 卡 span 2(跨栏),time/其他卡 span 1
|
||||
// Card grid render: media card spans 2 columns, time/other cards span 1
|
||||
function renderCards(catalog) {
|
||||
const grid = document.getElementById('tri-col');
|
||||
grid.textContent = '';
|
||||
const items = [];
|
||||
// 按 priority 排序(数字小者优先)
|
||||
// Sort by priority (smaller first)
|
||||
const sorted = [...catalog].sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
||||
// time 卡(内置,span 1)
|
||||
if (sorted.some(c => c.id === 'time')) items.push({ span: 1, el: renderTimeCard() });
|
||||
// media 卡(有媒体时 span 2,跨栏更美观)
|
||||
if (currentMedia) items.push({ span: 2, el: renderMediaCard(currentMedia) });
|
||||
// 其他卡(排除 time)span 1
|
||||
sorted.filter(c => c.id !== 'time').forEach(c => {
|
||||
items.push({ span: 1, el: renderGenericCard(c.id) });
|
||||
});
|
||||
items.forEach(item => {
|
||||
item.el.classList.add('span-' + item.span);
|
||||
grid.appendChild(item.el);
|
||||
});
|
||||
}
|
||||
|
||||
// 内置大字时间卡:当前时间(时:分 + 小号秒)+ 日期
|
||||
// Builtin big time card: current time (HH:MM + small seconds) + date
|
||||
function renderTimeCard() {
|
||||
@@ -100,16 +106,11 @@ let currentMedia = null;
|
||||
window.HearthEvents = window.HearthEvents || {};
|
||||
window.HearthEvents.mediaSessionChanged = function (info) {
|
||||
currentMedia = info;
|
||||
const mediaCol = document.getElementById('col-1');
|
||||
if (!mediaCol) return;
|
||||
if (info) {
|
||||
mediaCol.innerHTML = '';
|
||||
mediaCol.appendChild(renderMediaCard(info));
|
||||
} else {
|
||||
// 无媒体:降级到日历/天气(由 cards.layout 重新计算)
|
||||
// No media: fall back to calendar/weather (recomputed by cards.layout)
|
||||
window.updateHomeCards();
|
||||
}
|
||||
const grid = document.getElementById('tri-col');
|
||||
if (!grid) return;
|
||||
// 重新渲染:有媒体时 media 卡 span 2,无媒体时其他卡填满
|
||||
// Re-render: media card spans 2 when present, others fill otherwise
|
||||
window.updateHomeCards();
|
||||
};
|
||||
|
||||
// 媒体卡:正在播放标签 + 标题/艺术家 + 进度条
|
||||
|
||||
@@ -6,7 +6,9 @@ window.loadWebAppList = async function () {
|
||||
page.innerHTML = `
|
||||
<div class="search"><input id="web-search" placeholder="搜索 webAPP…"></div>
|
||||
<div class="grid" id="web-grid"></div>`;
|
||||
const apps = JSON.parse(await bridge.call('fetchWebApps'));
|
||||
// 优先用预加载缓存,未就绪则实时加载
|
||||
// Prefer the preloaded cache; fetch live when not ready yet
|
||||
const apps = window.preload.webApps || JSON.parse(await bridge.call('fetchWebApps'));
|
||||
const grid = document.getElementById('web-grid');
|
||||
const render = (list) => {
|
||||
grid.textContent = '';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 页面数据预加载:启动后后台拉取各页面数据缓存,切页时直接渲染减少延迟
|
||||
// Page data preload: fetch and cache page data in the background after startup,
|
||||
// so page switches render immediately without a fetch delay
|
||||
window.preload = {
|
||||
apps: null,
|
||||
webApps: null,
|
||||
cards: null,
|
||||
|
||||
async loadAll() {
|
||||
try {
|
||||
const [apps, webApps, cards] = await Promise.all([
|
||||
bridge.call('listApps').catch(() => null),
|
||||
bridge.call('fetchWebApps').catch(() => null),
|
||||
bridge.call('fetchCards').catch(() => null),
|
||||
]);
|
||||
this.apps = apps ? JSON.parse(apps) : null;
|
||||
this.webApps = webApps ? JSON.parse(webApps) : null;
|
||||
this.cards = cards ? JSON.parse(cards) : null;
|
||||
} catch (e) {
|
||||
// 预加载失败不阻塞:切页时仍会实时加载
|
||||
// Preload failure is non-blocking: page switches still fetch live
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
package top.yeij.hearth
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -30,8 +34,10 @@ 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.WallpaperProvider
|
||||
import top.yeij.hearth.webview.WebAppHost
|
||||
import top.yeij.hearth.webview.WebViewManager
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
// HOME 桌面 activity,接线各能力层:Repository、媒体监听、多 WebView webAPP 管理
|
||||
@@ -106,6 +112,35 @@ class MainActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
// 壁纸提供实现:WallpaperManager 取当前壁纸转 base64,供 H5 body 背景使用
|
||||
// Wallpaper implementation: WallpaperManager -> base64 for the H5 body background
|
||||
private val wallpaperProvider = object : WallpaperProvider {
|
||||
override fun getWallpaperBase64(): String {
|
||||
return try {
|
||||
val wm = WallpaperManager.getInstance(this@MainActivity)
|
||||
// 动态壁纸(Live Wallpaper)是实时渲染、无静态 Drawable,返回空让 H5 保持
|
||||
// 透明透出动态壁纸;此时玻璃模糊降级为普通半透明(backdrop-filter 无内容可模糊)
|
||||
// Live wallpapers render in real time with no static drawable; return empty
|
||||
// so H5 stays transparent and shows the live wallpaper, with the glass blur
|
||||
// degrading to plain translucency (backdrop-filter has nothing to blur)
|
||||
if (wm.wallpaperInfo != null) return ""
|
||||
val d = wm.drawable ?: return ""
|
||||
val dm = resources.displayMetrics
|
||||
val bmp = Bitmap.createBitmap(dm.widthPixels, dm.heightPixels, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bmp)
|
||||
d.setBounds(0, 0, dm.widthPixels, dm.heightPixels)
|
||||
d.draw(canvas)
|
||||
val out = ByteArrayOutputStream()
|
||||
bmp.compress(Bitmap.CompressFormat.JPEG, 80, out)
|
||||
bmp.recycle()
|
||||
"data:image/jpeg;base64," + Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "getWallpaper: ${e.message}")
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var webAppRepository: WebAppRepository
|
||||
private lateinit var mediaSource: MediaSessionSource
|
||||
private lateinit var root: FrameLayout
|
||||
@@ -155,6 +190,7 @@ class MainActivity : Activity() {
|
||||
notificationAccess = notificationAccess,
|
||||
brightness = brightnessProvider,
|
||||
serverUrl = serverUrlProvider,
|
||||
wallpaper = wallpaperProvider,
|
||||
)
|
||||
root = FrameLayout(this)
|
||||
setContentView(root)
|
||||
|
||||
@@ -36,6 +36,14 @@ interface ServerUrlProvider {
|
||||
fun setServerUrl(url: String)
|
||||
}
|
||||
|
||||
// 壁纸提供接口:由 MainActivity 实现(WallpaperManager 转 base64),
|
||||
// 让壁纸进入 WebView 内部,backdrop-filter 玻璃才能模糊到壁纸
|
||||
// Wallpaper provider implemented by MainActivity (WallpaperManager -> base64), so the
|
||||
// wallpaper lives inside the WebView and the glass backdrop-filter can blur it
|
||||
interface WallpaperProvider {
|
||||
fun getWallpaperBase64(): String
|
||||
}
|
||||
|
||||
class JsBridge(
|
||||
private val deviceWidthPx: Int,
|
||||
private val deviceHeightPx: Int,
|
||||
@@ -58,6 +66,9 @@ class JsBridge(
|
||||
// 服务器地址存储接口
|
||||
// Server URL storage provider
|
||||
private val serverUrl: ServerUrlProvider? = null,
|
||||
// 壁纸提供接口(base64)
|
||||
// Wallpaper provider (base64)
|
||||
private val wallpaper: WallpaperProvider? = null,
|
||||
) {
|
||||
private val gson = com.google.gson.Gson()
|
||||
|
||||
@@ -270,6 +281,11 @@ class JsBridge(
|
||||
serverUrl?.setServerUrl(url)
|
||||
}
|
||||
|
||||
// 返回壁纸 base64(data URI,未接入返回空字符串)
|
||||
// Return the wallpaper base64 (data URI; empty string when not wired)
|
||||
@android.webkit.JavascriptInterface
|
||||
fun getWallpaper(): String = wallpaper?.getWallpaperBase64() ?: ""
|
||||
|
||||
// 重新拉取 webAPP 清单 + 卡片目录,返回 { count }
|
||||
// Re-fetch the webAPP manifest + card catalog, return { count }
|
||||
@android.webkit.JavascriptInterface
|
||||
|
||||
Reference in New Issue
Block a user