diff --git a/apps/mobile-shell/App.tsx b/apps/mobile-shell/App.tsx index 910c907a..3925d79c 100644 --- a/apps/mobile-shell/App.tsx +++ b/apps/mobile-shell/App.tsx @@ -7,8 +7,8 @@ import { Linking, Platform, StyleSheet, - View, } from 'react-native'; +import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'; import type { WebViewMessageEvent } from 'react-native-webview'; import { WebView } from 'react-native-webview'; @@ -24,6 +24,7 @@ import { shouldOpenInMobileShellWebView, } from './src/mobileShellNavigation'; import { subscribeMobileNetworkStatus } from './src/mobileShellNetwork'; +import { MOBILE_SHELL_SAFE_AREA_EDGES } from './src/mobileShellSafeArea'; import { buildMobileShellUrl } from './src/mobileShellUrl'; const defaultWebUrl = 'http://127.0.0.1:3000/'; @@ -165,32 +166,34 @@ export default function App() { }; return ( - - - { - setCanGoBack(event.canGoBack); - webViewRef.current?.injectJavaScript( - buildHostBridgeMessageScript({ - bridge: 'GenarrativeHostBridge', - version: 1, - event: 'navigation.canGoBack', - payload: { - canGoBack: event.canGoBack, - }, - }), - ); - }} - setSupportMultipleWindows={false} - /> - + + + + { + setCanGoBack(event.canGoBack); + webViewRef.current?.injectJavaScript( + buildHostBridgeMessageScript({ + bridge: 'GenarrativeHostBridge', + version: 1, + event: 'navigation.canGoBack', + payload: { + canGoBack: event.canGoBack, + }, + }), + ); + }} + setSupportMultipleWindows={false} + /> + + ); } diff --git a/apps/mobile-shell/package.json b/apps/mobile-shell/package.json index a082ee21..da1de514 100644 --- a/apps/mobile-shell/package.json +++ b/apps/mobile-shell/package.json @@ -26,6 +26,7 @@ "expo-status-bar": "^56.0.4", "react": "^19.0.0", "react-native": "^0.86.0", + "react-native-safe-area-context": "^5.8.0", "react-native-webview": "^13.16.1" }, "devDependencies": { diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index a2f26495..c5a49abf 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -124,6 +124,9 @@ for (const snippet of [ 'subscribeMobileNetworkStatus', 'navigation.canGoBack', 'buildHostBridgeMessageScript', + 'SafeAreaProvider', + 'SafeAreaView', + 'MOBILE_SHELL_SAFE_AREA_EDGES', ]) { if (!appSource.includes(snippet)) { throw new Error(`mobile shell App missing ${snippet}`); @@ -137,6 +140,7 @@ for (const dependency of [ 'expo-network', 'expo-notifications', 'expo-sharing', + 'react-native-safe-area-context', ]) { if (!packageConfig.dependencies?.[dependency]) { throw new Error(`mobile shell package missing ${dependency}`); diff --git a/apps/mobile-shell/src/mobileShellSafeArea.test.ts b/apps/mobile-shell/src/mobileShellSafeArea.test.ts new file mode 100644 index 00000000..65bda42a --- /dev/null +++ b/apps/mobile-shell/src/mobileShellSafeArea.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, test } from 'vitest'; + +import { MOBILE_SHELL_SAFE_AREA_EDGES } from './mobileShellSafeArea'; + +describe('mobile shell safe area', () => { + test('protects the WebView from every device edge', () => { + expect(MOBILE_SHELL_SAFE_AREA_EDGES).toEqual([ + 'top', + 'right', + 'bottom', + 'left', + ]); + }); +}); diff --git a/apps/mobile-shell/src/mobileShellSafeArea.ts b/apps/mobile-shell/src/mobileShellSafeArea.ts new file mode 100644 index 00000000..1430cad4 --- /dev/null +++ b/apps/mobile-shell/src/mobileShellSafeArea.ts @@ -0,0 +1,6 @@ +export const MOBILE_SHELL_SAFE_AREA_EDGES = [ + 'top', + 'right', + 'bottom', + 'left', +] as const; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index b7b86585..0a0b00e7 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -38,6 +38,7 @@ - 2026-06-18 移动图片导入:Expo 壳开始声明并实现 `file.importImage`,通过 `expo-image-picker` 请求相册权限并打开系统相册选择器,只允许 `image/png`、`image/jpeg`、`image/webp` 且单次不超过 10 MiB;成功只回传清洗后的文件名、MIME、base64 内容和字节数,不暴露设备本地 URI,用户取消返回 `cancelled` 并由 H5 facade 归为 `false`。 - 2026-06-18 移动图片拍摄导入:Expo 壳新增 `file.captureImage` HostBridge capability,通过 `expo-image-picker` 请求相机权限并打开系统相机拍摄图片,沿用 `file.importImage` 的 MIME、体积、base64 和文件名清洗规则,成功回传 `action=captured`,不暴露设备本地 URI,也不请求麦克风权限;Tauri 壳不声明该能力,不伪造桌面拍摄。 - 2026-06-18 H5 图片上传接入宿主导入:`CreativeImageInputPanel` 在 `native_app` 且声明 `file.importImage` / `file.captureImage` 时,主图上传和描述参考图上传可分别调用 `importHostImageFile()` / `captureHostImageFile()`,并把宿主返回的 base64 图片转换为现有 `File` 回调;浏览器、小程序和未声明能力的裁剪壳继续走原生 `` 路径,不新增玩法侧上传分叉。 +- 2026-06-18 移动壳安全区:Expo 壳根布局使用 `react-native-safe-area-context` 的 `SafeAreaProvider` 与四边 `SafeAreaView` 保护 WebView,避免 H5 主站内容贴进 iOS 刘海、底部 Home Indicator、Android 状态栏或横屏边缘;该能力属于宿主壳布局保护,不新增 H5 占位 UI,不改变玩法 runtime 或 HostBridge capability。 - 2026-06-18 桌面图片拖入接入主图槽位:`CreativeImageInputPanel` 在桌面壳声明 `file.imageDropped` 时订阅宿主拖入事件,只在拖入坐标命中当前主图卡片且未被上层元素遮挡时消费事件,避免窗口级拖入被多个创作面板同时接收;成功后仍转换为现有 `File` 上传回调。 - 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。 - 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `