tauri-plugin-edge-to-edge-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Tauri Plugin Edge-to-Edge
2
+
3
+ 为 Tauri iOS/Android 应用提供 **Edge-to-Edge 全屏沉浸式体验**。
4
+
5
+ ## 功能
6
+
7
+ - **iOS**: 让 WKWebView 忽略安全区域,内容延伸到状态栏和底部,禁用原生手势导航(防止返回到空白页)
8
+ - **Android**: 启用 Edge-to-Edge 模式,透明系统栏
9
+ - **首帧 CSS 变量**: 在 HTML 解析前自动注入 `--safe-area-inset-*` 等 CSS 变量
10
+ - **刷新自动恢复**: 页面刷新或导航后自动重新读取并注入当前原生 Insets
11
+ - **启动图标**: Android / iOS 默认显示居中的应用图标,页面 `window.load` 后自动移除原生启动层
12
+ - **键盘支持**: 监听键盘显示/隐藏,动态更新键盘高度变量
13
+ - **webproxy 自定义协议**: 通过 `toWebproxyUrl(<url>)` 转发任意 http(s) 请求,绕过 CSP 限制
14
+
15
+ ## 安装
16
+
17
+ ### 1. Rust 依赖 (src-tauri/Cargo.toml)
18
+
19
+ ```toml
20
+ [dependencies]
21
+ tauri-plugin-edge-to-edge = { path = "../tauri-plugin-edge-to-edge" }
22
+ ```
23
+
24
+ ### 2. 初始化插件 (src-tauri/src/lib.rs)
25
+
26
+ ```rust
27
+ pub fn run() {
28
+ tauri::Builder::default()
29
+ .plugin(tauri_plugin_edge_to_edge::init())
30
+ .run(tauri::generate_context!())
31
+ .expect("error while running tauri application");
32
+ }
33
+ ```
34
+
35
+ ## 使用
36
+
37
+ 插件会在 document-start 阶段确保 viewport 含有 `viewport-fit=cover`,让 WebView 在
38
+ 原生值返回前直接使用浏览器提供的安全区域值。仍建议在应用 HTML 中显式声明:
39
+
40
+ ```html
41
+ <meta
42
+ name="viewport"
43
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
44
+ />
45
+ ```
46
+
47
+ 无需在前端调用初始化 API。插件的 document-start 脚本会在首次加载、刷新和导航时先
48
+ 写入 CSS `env(safe-area-inset-*)`,随后用原生精确值更新。Android 会同步恢复上一次
49
+ 缓存的原生状态;iOS 首帧先使用 WebKit 的 `env()`,然后通过原生 bridge 更新圆角和
50
+ 键盘状态。原生值尚未到达时 `--edge-to-edge-ready` 为 `0`,更新完成后为 `1`。
51
+
52
+ `--safe-area-inset-*` 和 `--safe-area-*` 始终表示未经加工的系统安全区。键盘显示时,
53
+ 只有用于内容布局的 `--safe-area-bottom-computed` 和 `--content-bottom-padding` 变为
54
+ `0px`;插件不再内置 `34px`、`48px` 或额外 `16px` 这类应用层间距。
55
+
56
+ ### 启动画面
57
+
58
+ Android 在系统启动屏结束后显示独立的原生启动层,使用应用图标并以完整窗口居中。
59
+ Android 12+ 按系统无图标背景时的 `192 / 108` 缩放比例显示 80dp 图标(约 142.2dp),
60
+ 对应 AgentDeck 当前启动 drawable 的实际显示尺寸;Android 11 及以下仍为 80dp。
61
+ 背景跟随系统深浅色。这里的尺寸匹配基于当前启动资源,不代表任意应用主题或厂商
62
+ 修改后的启动布局都相同。
63
+
64
+ 无需前端调用。页面触发 `window.load` 后移除;Android 6+ 还会等待 WebView
65
+ 确认页面可绘制。刷新、后续导航、切回前台不会再次显示。
66
+
67
+ iOS 优先复用应用 `UILaunchStoryboardName` 指定的 storyboard,让系统启动屏与插件
68
+ 启动层使用相同布局;未指定时从 `CFBundleIcons` / `CFBundleIcons~ipad` 读取主图标。
69
+ 此启动层覆盖插件初始化到页面加载完成的阶段,不替代操作系统在原生插件初始化前
70
+ 显示的 Launch Screen / SplashScreen。`load` 不代表应用异步数据已经加载完成;
71
+ 若页面始终未触发 `load`,启动层会一直保留。
72
+
73
+ ### CSS 变量
74
+
75
+ 插件自动注入以下 CSS 变量:
76
+
77
+ | 变量名 | 描述 |
78
+ |--------|------|
79
+ | `--safe-area-inset-top` | 顶部安全区域 (状态栏) |
80
+ | `--safe-area-inset-right` | 右侧安全区域 |
81
+ | `--safe-area-inset-bottom` | 底部安全区域 (Home Indicator) |
82
+ | `--safe-area-inset-left` | 左侧安全区域 |
83
+ | `--safe-area-top/right/bottom/left` | 上述四个值的短别名 |
84
+ | `--safe-area-bottom-computed` | 键盘感知的底部安全区 |
85
+ | `--content-bottom-padding` | 当前建议的内容底部 padding |
86
+ | `--screen-corner-radius` | 屏幕物理圆角半径 |
87
+ | `--keyboard-height` | 键盘高度 |
88
+ | `--keyboard-visible` | 键盘是否可见 (1/0) |
89
+ | `--edge-to-edge-ready` | 是否已收到原生精确值 (1/0) |
90
+
91
+ ### CSS 示例
92
+
93
+ ```css
94
+ .app-container {
95
+ padding-top: var(--safe-area-top, 0px);
96
+ padding-bottom: var(--safe-area-bottom-computed, 0px);
97
+ }
98
+
99
+ .bottom-input {
100
+ position: fixed;
101
+ bottom: 0;
102
+ padding-bottom: var(--safe-area-bottom, 0px);
103
+ }
104
+
105
+ .screen-corner-aware {
106
+ border-radius: var(--screen-corner-radius, 0px);
107
+ }
108
+ ```
109
+
110
+ ### 事件监听
111
+
112
+ ```javascript
113
+ window.addEventListener('safeAreaChanged', (event) => {
114
+ const {
115
+ top,
116
+ bottom,
117
+ bottomComputed,
118
+ screenCornerRadius,
119
+ keyboardHeight,
120
+ keyboardVisible,
121
+ } = event.detail;
122
+ console.log('Safe area changed:', event.detail);
123
+ });
124
+ ```
125
+
126
+ 首次 `safeAreaChanged` 会延迟到 `DOMContentLoaded`,以便页面模块有时间注册监听器;
127
+ 后续系统栏和键盘变化会立即触发。
128
+
129
+ 如果需要命令式 API,可使用 npm 包导出的真实插件方法(不再包含脚手架的 `ping`):
130
+
131
+ ```ts
132
+ import {
133
+ getSafeAreaInsets,
134
+ getKeyboardInfo,
135
+ onSafeAreaChanged,
136
+ toWebproxyUrl,
137
+ } from 'tauri-plugin-edge-to-edge-api'
138
+
139
+ const stop = onSafeAreaChanged((state) => console.log(state))
140
+ const insets = await getSafeAreaInsets()
141
+ const res = await fetch(toWebproxyUrl('https://api.example.com/data'))
142
+ ```
143
+
144
+ ## 平台支持
145
+
146
+ | 平台 | 支持 |
147
+ |------|------|
148
+ | iOS | 按硬件型号注入已验证 iPhone 的圆角半径;未知型号为 `0px`(不使用私有 API) |
149
+ | Android | Android 12+ 注入设备圆角半径;低版本为 `0px` |
150
+ | macOS/Windows/Linux | (返回默认值)
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ var core = require('@tauri-apps/api/core');
4
+
5
+ const SAFE_AREA_CHANGED_EVENT = 'safeAreaChanged';
6
+ /**
7
+ * Converts an HTTPS URL into the host-preserving `webproxy` URL understood by
8
+ * the native protocol handler.
9
+ *
10
+ * Relative URLs inside a proxied HTML document keep working because only the
11
+ * scheme changes logically: `https://example.com/a` -> `webproxy://example.com/a`.
12
+ * On Android/Windows Wry exposes custom protocols through its http(s) workaround
13
+ * (`http(s)://webproxy.example.com/a`); `convertFileSrc` is used only to discover
14
+ * which outer protocol the current WebView was configured to use.
15
+ */
16
+ function toWebproxyUrl(url) {
17
+ const target = new URL(url);
18
+ if (target.protocol !== 'https:') {
19
+ throw new TypeError(`webproxy only supports HTTPS URLs, got ${target.protocol}`);
20
+ }
21
+ if (target.username || target.password) {
22
+ throw new TypeError('webproxy URLs must not contain credentials');
23
+ }
24
+ if (target.hostname.includes(':')) {
25
+ throw new TypeError('webproxy does not currently support IPv6 literal hosts');
26
+ }
27
+ const suffix = `${target.pathname}${target.search}${target.hash}`;
28
+ const probe = new URL(core.convertFileSrc('', 'webproxy'));
29
+ // macOS / iOS / Linux register the real custom scheme.
30
+ if (probe.protocol === 'webproxy:') {
31
+ return `webproxy://${target.host}${suffix}`;
32
+ }
33
+ // Android / Windows use Wry's http(s) custom-protocol workaround:
34
+ // webproxy://example.com/a <-> http(s)://webproxy.example.com/a
35
+ return `${probe.protocol}//webproxy.${target.host}${suffix}`;
36
+ }
37
+ function getSafeAreaInsets() {
38
+ return core.invoke('plugin:edge-to-edge|get_safe_area_insets');
39
+ }
40
+ function getKeyboardInfo() {
41
+ return core.invoke('plugin:edge-to-edge|get_keyboard_info');
42
+ }
43
+ function enable() {
44
+ return core.invoke('plugin:edge-to-edge|enable');
45
+ }
46
+ function disable() {
47
+ return core.invoke('plugin:edge-to-edge|disable');
48
+ }
49
+ function showKeyboard() {
50
+ return core.invoke('plugin:edge-to-edge|show_keyboard');
51
+ }
52
+ function hideKeyboard() {
53
+ return core.invoke('plugin:edge-to-edge|hide_keyboard');
54
+ }
55
+ function onSafeAreaChanged(listener) {
56
+ const eventListener = (event) => {
57
+ listener(event.detail);
58
+ };
59
+ window.addEventListener(SAFE_AREA_CHANGED_EVENT, eventListener);
60
+ return () => window.removeEventListener(SAFE_AREA_CHANGED_EVENT, eventListener);
61
+ }
62
+
63
+ exports.SAFE_AREA_CHANGED_EVENT = SAFE_AREA_CHANGED_EVENT;
64
+ exports.disable = disable;
65
+ exports.enable = enable;
66
+ exports.getKeyboardInfo = getKeyboardInfo;
67
+ exports.getSafeAreaInsets = getSafeAreaInsets;
68
+ exports.hideKeyboard = hideKeyboard;
69
+ exports.onSafeAreaChanged = onSafeAreaChanged;
70
+ exports.showKeyboard = showKeyboard;
71
+ exports.toWebproxyUrl = toWebproxyUrl;
@@ -0,0 +1,36 @@
1
+ export interface SafeAreaInsets {
2
+ top: number;
3
+ right: number;
4
+ bottom: number;
5
+ left: number;
6
+ }
7
+ export interface KeyboardInfo {
8
+ keyboardHeight: number;
9
+ isVisible: boolean;
10
+ }
11
+ export interface EdgeToEdgeState extends SafeAreaInsets {
12
+ bottomComputed: number;
13
+ contentBottomPadding: number;
14
+ screenCornerRadius: number;
15
+ keyboardHeight: number;
16
+ keyboardVisible: boolean;
17
+ }
18
+ export declare const SAFE_AREA_CHANGED_EVENT = "safeAreaChanged";
19
+ /**
20
+ * Converts an HTTPS URL into the host-preserving `webproxy` URL understood by
21
+ * the native protocol handler.
22
+ *
23
+ * Relative URLs inside a proxied HTML document keep working because only the
24
+ * scheme changes logically: `https://example.com/a` -> `webproxy://example.com/a`.
25
+ * On Android/Windows Wry exposes custom protocols through its http(s) workaround
26
+ * (`http(s)://webproxy.example.com/a`); `convertFileSrc` is used only to discover
27
+ * which outer protocol the current WebView was configured to use.
28
+ */
29
+ export declare function toWebproxyUrl(url: string): string;
30
+ export declare function getSafeAreaInsets(): Promise<SafeAreaInsets>;
31
+ export declare function getKeyboardInfo(): Promise<KeyboardInfo>;
32
+ export declare function enable(): Promise<void>;
33
+ export declare function disable(): Promise<void>;
34
+ export declare function showKeyboard(): Promise<void>;
35
+ export declare function hideKeyboard(): Promise<void>;
36
+ export declare function onSafeAreaChanged(listener: (state: EdgeToEdgeState) => void): () => void;
@@ -0,0 +1,61 @@
1
+ import { convertFileSrc, invoke } from '@tauri-apps/api/core';
2
+
3
+ const SAFE_AREA_CHANGED_EVENT = 'safeAreaChanged';
4
+ /**
5
+ * Converts an HTTPS URL into the host-preserving `webproxy` URL understood by
6
+ * the native protocol handler.
7
+ *
8
+ * Relative URLs inside a proxied HTML document keep working because only the
9
+ * scheme changes logically: `https://example.com/a` -> `webproxy://example.com/a`.
10
+ * On Android/Windows Wry exposes custom protocols through its http(s) workaround
11
+ * (`http(s)://webproxy.example.com/a`); `convertFileSrc` is used only to discover
12
+ * which outer protocol the current WebView was configured to use.
13
+ */
14
+ function toWebproxyUrl(url) {
15
+ const target = new URL(url);
16
+ if (target.protocol !== 'https:') {
17
+ throw new TypeError(`webproxy only supports HTTPS URLs, got ${target.protocol}`);
18
+ }
19
+ if (target.username || target.password) {
20
+ throw new TypeError('webproxy URLs must not contain credentials');
21
+ }
22
+ if (target.hostname.includes(':')) {
23
+ throw new TypeError('webproxy does not currently support IPv6 literal hosts');
24
+ }
25
+ const suffix = `${target.pathname}${target.search}${target.hash}`;
26
+ const probe = new URL(convertFileSrc('', 'webproxy'));
27
+ // macOS / iOS / Linux register the real custom scheme.
28
+ if (probe.protocol === 'webproxy:') {
29
+ return `webproxy://${target.host}${suffix}`;
30
+ }
31
+ // Android / Windows use Wry's http(s) custom-protocol workaround:
32
+ // webproxy://example.com/a <-> http(s)://webproxy.example.com/a
33
+ return `${probe.protocol}//webproxy.${target.host}${suffix}`;
34
+ }
35
+ function getSafeAreaInsets() {
36
+ return invoke('plugin:edge-to-edge|get_safe_area_insets');
37
+ }
38
+ function getKeyboardInfo() {
39
+ return invoke('plugin:edge-to-edge|get_keyboard_info');
40
+ }
41
+ function enable() {
42
+ return invoke('plugin:edge-to-edge|enable');
43
+ }
44
+ function disable() {
45
+ return invoke('plugin:edge-to-edge|disable');
46
+ }
47
+ function showKeyboard() {
48
+ return invoke('plugin:edge-to-edge|show_keyboard');
49
+ }
50
+ function hideKeyboard() {
51
+ return invoke('plugin:edge-to-edge|hide_keyboard');
52
+ }
53
+ function onSafeAreaChanged(listener) {
54
+ const eventListener = (event) => {
55
+ listener(event.detail);
56
+ };
57
+ window.addEventListener(SAFE_AREA_CHANGED_EVENT, eventListener);
58
+ return () => window.removeEventListener(SAFE_AREA_CHANGED_EVENT, eventListener);
59
+ }
60
+
61
+ export { SAFE_AREA_CHANGED_EVENT, disable, enable, getKeyboardInfo, getSafeAreaInsets, hideKeyboard, onSafeAreaChanged, showKeyboard, toWebproxyUrl };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "tauri-plugin-edge-to-edge-api",
3
+ "version": "0.1.0",
4
+ "author": "mantou132",
5
+ "description": "JavaScript API for tauri-plugin-edge-to-edge",
6
+ "type": "module",
7
+ "types": "./dist-js/index.d.ts",
8
+ "main": "./dist-js/index.cjs",
9
+ "module": "./dist-js/index.js",
10
+ "exports": {
11
+ "types": "./dist-js/index.d.ts",
12
+ "import": "./dist-js/index.js",
13
+ "require": "./dist-js/index.cjs"
14
+ },
15
+ "files": [
16
+ "dist-js",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "rollup -c",
21
+ "prepublishOnly": "pnpm build",
22
+ "pretest": "pnpm build"
23
+ },
24
+ "dependencies": {
25
+ "@tauri-apps/api": "^2.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@rollup/plugin-typescript": "^12.0.0",
29
+ "rollup": "^4.9.6",
30
+ "typescript": "^5.3.3",
31
+ "tslib": "^2.6.2"
32
+ }
33
+ }