收口原生外观查询边界
将 Expo 系统配色读取收口到 appearance 模块 将 Tauri 窗口主题读取收口到 appearance 模块 同步原生壳结构门禁和架构文档清单
This commit is contained in:
@@ -46,6 +46,14 @@ const app = fs.readFileSync(appPath, 'utf8');
|
||||
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
|
||||
const main = fs.readFileSync(mainPath, 'utf8');
|
||||
const rustSourceDir = new URL('../src-tauri/src/', import.meta.url);
|
||||
const desktopHostBridgeAppearancePath = new URL(
|
||||
'../src-tauri/src/host_bridge/appearance.rs',
|
||||
import.meta.url,
|
||||
);
|
||||
const desktopHostBridgeAppearanceSource = fs.readFileSync(
|
||||
desktopHostBridgeAppearancePath,
|
||||
'utf8',
|
||||
);
|
||||
const desktopHostBridgeBadgePath = new URL(
|
||||
'../src-tauri/src/host_bridge/badge.rs',
|
||||
import.meta.url,
|
||||
@@ -201,6 +209,7 @@ const expectedDesktopRustRootEntries = [
|
||||
'file:main.rs',
|
||||
];
|
||||
const expectedDesktopHostBridgeRustFiles = [
|
||||
'appearance.rs',
|
||||
'badge.rs',
|
||||
'capabilities.rs',
|
||||
'clipboard.rs',
|
||||
@@ -1812,6 +1821,7 @@ const expectedRustRootEntries = [
|
||||
'file:main.rs',
|
||||
];
|
||||
const expectedRustHostBridgeFiles = [
|
||||
'appearance.rs',
|
||||
'badge.rs',
|
||||
'capabilities.rs',
|
||||
'clipboard.rs',
|
||||
@@ -2058,6 +2068,25 @@ assertSameList(
|
||||
if (nativeAppHostBridgeSource.includes("'host_bridge_request'")) {
|
||||
throw new Error('H5 native app HostBridge must use HOST_BRIDGE_TAURI_COMMAND');
|
||||
}
|
||||
if (
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'desktop_appearance_color_scheme(&app, &request)',
|
||||
) ||
|
||||
desktopHostBridgeDispatchSource.includes('window.theme()') ||
|
||||
desktopHostBridgeDispatchSource.includes('color_scheme_from_theme')
|
||||
) {
|
||||
throw new Error('desktop shell appearance HostBridge method must delegate to appearance module');
|
||||
}
|
||||
for (const snippet of [
|
||||
'desktop_appearance_color_scheme',
|
||||
'color_scheme_from_theme(theme)',
|
||||
'window.theme()',
|
||||
'"colorScheme"',
|
||||
]) {
|
||||
if (!desktopHostBridgeAppearanceSource.includes(snippet)) {
|
||||
throw new Error(`desktop shell appearance module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!desktopHostBridgeDispatchSource.includes(
|
||||
'set_desktop_app_badge_count(&app, &request)',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::host_bridge::protocol::{failed, ok, HostBridgeRequest, HostBridgeResponse};
|
||||
use crate::shell::webview::color_scheme_from_theme;
|
||||
use serde_json::json;
|
||||
use tauri::Manager;
|
||||
|
||||
pub(crate) fn desktop_appearance_color_scheme(
|
||||
app: &tauri::AppHandle,
|
||||
request: &HostBridgeRequest,
|
||||
) -> HostBridgeResponse {
|
||||
match app.get_webview_window("main") {
|
||||
Some(window) => match window.theme() {
|
||||
Ok(theme) => ok_color_scheme(request, color_scheme_from_theme(theme)),
|
||||
Err(error) => failed(request.id.clone(), "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id.clone(), "host_error", "main window not found"),
|
||||
}
|
||||
}
|
||||
|
||||
fn ok_color_scheme(request: &HostBridgeRequest, color_scheme: &'static str) -> HostBridgeResponse {
|
||||
ok(
|
||||
request.id.clone(),
|
||||
json!({
|
||||
"colorScheme": color_scheme
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::host_bridge::protocol::request;
|
||||
|
||||
#[test]
|
||||
fn ok_color_scheme_reports_contract_shape() {
|
||||
let response = ok_color_scheme(&request("appearance.getColorScheme"), "dark");
|
||||
|
||||
assert!(response.ok);
|
||||
assert_eq!(
|
||||
response.result.expect("appearance result"),
|
||||
json!({
|
||||
"colorScheme": "dark"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::host_bridge::appearance::desktop_appearance_color_scheme;
|
||||
use crate::host_bridge::badge::set_desktop_app_badge_count;
|
||||
use crate::host_bridge::capabilities::capabilities;
|
||||
use crate::host_bridge::clipboard::{read_desktop_clipboard_text, write_desktop_clipboard_text};
|
||||
@@ -13,7 +14,7 @@ use crate::host_bridge::protocol::{
|
||||
};
|
||||
use crate::host_bridge::share::{share_text_from_request, DesktopShareState};
|
||||
use crate::shell::webview::{
|
||||
color_scheme_from_theme, desktop_platform, normalize_external_url, normalize_native_page_url,
|
||||
desktop_platform, normalize_external_url, normalize_native_page_url,
|
||||
open_normalized_desktop_external_url, resolve_desktop_network_status,
|
||||
};
|
||||
use serde_json::json;
|
||||
@@ -85,18 +86,7 @@ pub(super) async fn execute_host_bridge_request(
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
}
|
||||
}
|
||||
"appearance.getColorScheme" => match app.get_webview_window("main") {
|
||||
Some(window) => match window.theme() {
|
||||
Ok(theme) => ok(
|
||||
request.id,
|
||||
json!({
|
||||
"colorScheme": color_scheme_from_theme(theme)
|
||||
}),
|
||||
),
|
||||
Err(error) => failed(request.id, "host_error", error.to_string()),
|
||||
},
|
||||
None => failed(request.id, "host_error", "main window not found"),
|
||||
},
|
||||
"appearance.getColorScheme" => desktop_appearance_color_scheme(&app, &request),
|
||||
"navigation.openNativePage" => {
|
||||
let url = match required_string_payload(&request, "url")
|
||||
.ok()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod appearance;
|
||||
pub(crate) mod capabilities;
|
||||
mod badge;
|
||||
mod clipboard;
|
||||
|
||||
@@ -10,6 +10,8 @@ const shellAppPath = new URL('../src/shell/ShellApp.tsx', import.meta.url);
|
||||
const shellAppSource = fs.readFileSync(shellAppPath, 'utf8');
|
||||
const qrScannerOverlayPath = new URL('../src/shell/QrScannerOverlay.tsx', import.meta.url);
|
||||
const qrScannerOverlaySource = fs.readFileSync(qrScannerOverlayPath, 'utf8');
|
||||
const appearancePath = new URL('../src/host-bridge/appearance.ts', import.meta.url);
|
||||
const appearanceSource = fs.readFileSync(appearancePath, 'utf8');
|
||||
const bridgePath = new URL('../src/host-bridge/bridge.ts', import.meta.url);
|
||||
const bridgeSource = fs.readFileSync(bridgePath, 'utf8');
|
||||
const badgePath = new URL('../src/host-bridge/badge.ts', import.meta.url);
|
||||
@@ -77,6 +79,7 @@ const productionSourceRoots = [
|
||||
const productionFileExtensions = new Set(['.json', '.mjs', '.ts', '.tsx']);
|
||||
const requiredMobileShellSourceModules = [
|
||||
'env.d.ts',
|
||||
'host-bridge/appearance.ts',
|
||||
'host-bridge/badge.ts',
|
||||
'host-bridge/bridge.ts',
|
||||
'host-bridge/capabilities.ts',
|
||||
@@ -1835,8 +1838,21 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
if (!dispatchSource.includes('Appearance.getColorScheme()')) {
|
||||
throw new Error('mobile shell HostBridge must read the native color scheme');
|
||||
if (
|
||||
!dispatchSource.includes('getMobileAppearanceColorScheme()') ||
|
||||
dispatchSource.includes('Appearance.getColorScheme()') ||
|
||||
dispatchSource.includes('normalizeHostBridgeColorScheme')
|
||||
) {
|
||||
throw new Error('mobile shell appearance HostBridge method must delegate to appearance module');
|
||||
}
|
||||
for (const snippet of [
|
||||
'Appearance.getColorScheme()',
|
||||
'normalizeHostBridgeColorScheme',
|
||||
'getMobileAppearanceColorScheme',
|
||||
]) {
|
||||
if (!appearanceSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell appearance module is missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dispatchSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)')) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Appearance } from 'react-native';
|
||||
|
||||
import { normalizeHostBridgeColorScheme } from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
|
||||
export function getMobileAppearanceColorScheme() {
|
||||
return {
|
||||
colorScheme: normalizeHostBridgeColorScheme(Appearance.getColorScheme()),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Linking from 'expo-linking';
|
||||
import { Appearance, Platform } from 'react-native';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import {
|
||||
type ClipboardWriteTextPayload,
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type HostBridgeError,
|
||||
type HostBridgeRequest,
|
||||
type NavigateNativePagePayload,
|
||||
normalizeHostBridgeColorScheme,
|
||||
normalizeHostBridgeExternalUrlPayload,
|
||||
normalizeHostBridgeLocalNotification,
|
||||
type OpenExternalUrlPayload,
|
||||
@@ -47,6 +46,7 @@ import {
|
||||
} from './clipboard';
|
||||
import { runMobileHapticsImpact } from './haptics';
|
||||
import { setMobileAppBadgeCount } from './badge';
|
||||
import { getMobileAppearanceColorScheme } from './appearance';
|
||||
|
||||
let currentShareTarget: unknown = null;
|
||||
let navigation: MobileHostBridgeNavigation | null = null;
|
||||
@@ -108,12 +108,6 @@ async function showLocalNotification(payload: unknown) {
|
||||
return showMobileLocalNotification(notification);
|
||||
}
|
||||
|
||||
function getColorScheme() {
|
||||
return {
|
||||
colorScheme: normalizeHostBridgeColorScheme(Appearance.getColorScheme()),
|
||||
};
|
||||
}
|
||||
|
||||
function getMobileRuntimePlatform() {
|
||||
return Platform.OS === 'ios' ? 'ios' : 'android';
|
||||
}
|
||||
@@ -169,7 +163,7 @@ export async function dispatchMobileHostBridgeRequest(
|
||||
case 'host.getRuntime':
|
||||
return ok(request, getRuntime());
|
||||
case 'appearance.getColorScheme':
|
||||
return ok(request, getColorScheme());
|
||||
return ok(request, getMobileAppearanceColorScheme());
|
||||
case 'app.openExternalUrl':
|
||||
return ok(request, await openExternalUrl(request.payload));
|
||||
case 'app.reloadWebView':
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
- 2026-06-18 草稿生成未读角标:平台壳层把“可见作品架里未读的草稿生成完成更新”同步到 `app.setBadgeCount`;同一草稿的 work/profile/session 等多个恢复 ID 只计 1,已读、失败、生成中和不可见草稿不计入。该角标只消费已有 HostBridge 能力,宿主不支持或设置失败不影响 H5 红点、作品架或后端状态。
|
||||
- 2026-06-19 原生壳角标边界:Expo `app.setBadgeCount` 的 iOS 平台判定、共享上限校验和 `PushNotificationIOS.setApplicationIconBadgeNumber(...)` 调用统一收口在 `apps/mobile-shell/src/host-bridge/badge.ts`;Tauri `app.setBadgeCount` 的 payload 校验、清除语义和主窗口 `set_badge_count` 调用统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/badge.rs`。两端 `dispatch` 只负责委托对应 badge 模块和映射响应,配置检查会拒绝分发层直接导入角标底层 API 或重声明数量边界。
|
||||
- 2026-06-18 宿主外观只读查询:新增 `appearance.getColorScheme` HostBridge capability,Expo 壳通过 React Native `Appearance.getColorScheme()` 读取系统配色,Tauri 壳通过主窗口 `theme()` 读取窗口主题;该能力只返回 `light` / `dark` / `unknown`,不设置 H5 主题、不覆盖系统主题,也不作为强制 UI 样式入口。
|
||||
- 2026-06-19 原生壳外观查询边界:Expo `appearance.getColorScheme` 的系统配色读取和 HostBridge 配色归一统一收口在 `apps/mobile-shell/src/host-bridge/appearance.ts`;Tauri `appearance.getColorScheme` 的主窗口 `theme()` 读取和 `light / dark / unknown` 映射统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/appearance.rs`。两端 `dispatch` 只负责委托对应 appearance 模块和映射响应,配置检查会拒绝分发层直接读取系统配色或窗口主题。
|
||||
- 2026-06-18 原生壳生命周期事件:新增 `app.lifecycle` HostBridge capability,Expo 壳通过 React Native `AppState` 派发 `active` / `inactive` / `background`,Tauri 壳通过主窗口 focus / blur、托盘隐藏 / 恢复和页面加载重放派发统一状态;桌面隐藏到托盘或最小化都归一为 `background`,`hidden`、`minimized`、`focused`、`blurred` 只进入 `nativeState` 便于排障,不扩展共享 `state`。两端都声明 `host.events` 表示事件通过 HostBridge message 注入,但不把它作为 request method,也不开放 Tauri event 插件或 React Native 私有事件 API。H5 只通过 `subscribeHostAppLifecycle()` 订阅统一状态,后续游戏循环、音频和轮询暂停 / 恢复不得直接依赖 Expo / Tauri 平台细节。
|
||||
- 2026-06-18 原生壳网络状态:新增 `network.status` 与 `network.statusChanged` HostBridge capability,Expo 壳通过 `expo-network` 查询和订阅真实系统网络状态,Tauri 壳从 `WEB_APP_ORIGIN` 解析主站 host / port 后做短超时 TCP 可达性查询,并通过 WebView `online` / `offline` 注入变化事件;H5 统一使用 `getHostNetworkStatus()` / `subscribeHostNetworkStatusChange()`,不得直接读取 Expo / Tauri 私有网络 API。
|
||||
- 2026-06-19 移动壳本地通知边界:Expo `notification.showLocal` 的权限确认、iOS 仅 alert 且不请求 badge/sound、Android 固定 channel、即时调度和通知 handler 统一收口在 `apps/mobile-shell/src/host-bridge/notifications.ts`;`dispatch.ts` 只负责 HostBridge payload 归一和调用 `showMobileLocalNotification(notification)`,不得直接调用 `expo-notifications` 调度或权限 API。移动壳配置检查会覆盖该模块结构、固定 channel 和即时调度形态,避免后续混入远程推送、后台推送或散落的本地通知实现。
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -249,6 +249,7 @@ const expectedWechatPageFilesByRoute = {
|
||||
'wechat-pay': ['index.js', 'index.json', 'index.wxml', 'index.wxss'],
|
||||
};
|
||||
const expectedMobileHostBridgeFiles = [
|
||||
'appearance.ts',
|
||||
'badge.ts',
|
||||
'bridge.test.ts',
|
||||
'bridge.ts',
|
||||
@@ -293,6 +294,7 @@ const expectedMobileShellFiles = [
|
||||
'webViewPolicy.ts',
|
||||
];
|
||||
const expectedDesktopHostBridgeRustFiles = [
|
||||
'appearance.rs',
|
||||
'badge.rs',
|
||||
'capabilities.rs',
|
||||
'clipboard.rs',
|
||||
|
||||
Reference in New Issue
Block a user