@ayden-fc2/riffle-bridge-web 1.0.4 → 1.0.6
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 +63 -24
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +38 -0
- package/dist/index.mjs +38 -0
- package/package.json +1 -1
- package/src/controllers/index.ts +59 -0
package/README.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## 1. tweaks和RiffleBridge用法总览
|
|
4
4
|
|
|
5
|
-
本文档为当前Web应用提供Bridge
|
|
5
|
+
本文档为当前Web应用提供Bridge使用指导,分为必选的tweaks和可选的RiffleBridge两个API,从而与app应用通信。
|
|
6
6
|
|
|
7
7
|
**安装依赖:**
|
|
8
8
|
```bash
|
|
9
9
|
npm install @ayden-fc2/riffle-bridge-web
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
tweaks用于联通web应用与
|
|
12
|
+
tweaks用于联通web应用与app应用的自定义参数,传递支持颜色、数值、文本、布尔、选项类型的JsonSchema格式的tweaksConfig,原则上web应用内的颜色、数值、文本、布尔、选项都要通过tweaks层配置,从而保证app应用能够响应式控制,使用参考文章第二节。
|
|
13
13
|
|
|
14
|
-
RiffleBridge用于调用震动、音频、相机等模块的
|
|
14
|
+
RiffleBridge用于调用震动、音频、相机等模块的app应用功能,展开对应文章第三节各个模块,根据所需要实现的web应用功能,按需参考模块取用即可;
|
|
15
15
|
|
|
16
16
|
## 2. tweaks 用法
|
|
17
17
|
|
|
@@ -23,20 +23,20 @@ RiffleBridge用于调用震动、音频、相机等模块的native功能,展
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
const tweaksConfig = {
|
|
26
|
-
// 颜色类型 -
|
|
26
|
+
// 颜色类型 - app应用 端显示颜色选择器
|
|
27
27
|
color: { name: '颜色', type: 'color', value: '#667eea' },
|
|
28
28
|
|
|
29
|
-
// 数值类型 -
|
|
29
|
+
// 数值类型 - app应用 端显示滑块
|
|
30
30
|
size: { name: '大小', type: 'number', value: 150, min: 50, max: 300 },
|
|
31
31
|
opacity: { name: '透明度', type: 'number', value: 1, min: 0, max: 1, step: 0.1 },
|
|
32
32
|
|
|
33
|
-
// 文本类型 -
|
|
33
|
+
// 文本类型 - app应用 端显示文本输入框
|
|
34
34
|
title: { name: '标题', type: 'string', value: 'Hello World' },
|
|
35
35
|
|
|
36
|
-
// 布尔类型 -
|
|
36
|
+
// 布尔类型 - app应用 端显示开关
|
|
37
37
|
enabled: { name: '启用', type: 'boolean', value: true },
|
|
38
38
|
|
|
39
|
-
// 选项类型 -
|
|
39
|
+
// 选项类型 - app应用 端显示下拉选择器
|
|
40
40
|
mode: { name: '模式', type: 'select', value: 'normal', options: ['normal', 'fast', 'slow'] },
|
|
41
41
|
vibration: { name: '震动', type: 'select', value: 'medium', options: [
|
|
42
42
|
{ label: '轻', value: 'light' },
|
|
@@ -48,7 +48,7 @@ const tweaksConfig = {
|
|
|
48
48
|
|
|
49
49
|
### 2.2 初始化(必须传入React实例)
|
|
50
50
|
|
|
51
|
-
通过 { React } 参数显式传入初始化,保证web和
|
|
51
|
+
通过 { React } 参数显式传入初始化,保证web和app应用数据联通,响应式更新,初始化代码如下:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
54
|
import React, { useMemo } from 'react';
|
|
@@ -64,7 +64,7 @@ const tweaks = useMemo(() => createTweaks(tweaksConfig, { React }), []);
|
|
|
64
64
|
|
|
65
65
|
```
|
|
66
66
|
function Demo() {
|
|
67
|
-
// .useState() 自动响应式订阅,
|
|
67
|
+
// .useState() 自动响应式订阅,app应用 修改时自动重渲染
|
|
68
68
|
const color = tweaks.color.useState();
|
|
69
69
|
const size = tweaks.size.useState();
|
|
70
70
|
|
|
@@ -76,7 +76,7 @@ function Demo() {
|
|
|
76
76
|
|
|
77
77
|
## 3. RiffleBridge用法
|
|
78
78
|
|
|
79
|
-
通过 `RiffleBridge` 类调用
|
|
79
|
+
通过 `RiffleBridge` 类调用app应用功能:
|
|
80
80
|
|
|
81
81
|
```
|
|
82
82
|
import { RiffleBridge } from '@ayden-fc2/riffle-bridge-web';
|
|
@@ -85,6 +85,8 @@ const bridge = new RiffleBridge();
|
|
|
85
85
|
|
|
86
86
|
针对RiffleBridge,分为震动模块、传感器模块、音频模块、相机模块、麦克风模块、文件存储模块、设备信息模块,各自的使用方法与注意事项已经分别说明,根据web应用所需模块,按需阅读使用即可。
|
|
87
87
|
|
|
88
|
+
**注意音频模块、相机模块、麦克风模块部分依赖于文件存储模块,可以先阅读文件存储模块的代码,辅助理解这三个模块中的文件存储操作**
|
|
89
|
+
|
|
88
90
|
### 3.1 震动模块
|
|
89
91
|
|
|
90
92
|
```
|
|
@@ -112,7 +114,7 @@ await bridge.haptic.intensity(0.5);
|
|
|
112
114
|
- **y轴**:正值指向设备顶部
|
|
113
115
|
- **z轴**:正值指向屏幕外(朝向用户)
|
|
114
116
|
|
|
115
|
-
> iOS 和 Android 原始坐标系不同,Bridge 已在
|
|
117
|
+
> iOS 和 Android 原始坐标系不同,Bridge 已在 app应用 层自动处理,WebView 端收到的数据保持一致。
|
|
116
118
|
|
|
117
119
|
```typescript
|
|
118
120
|
// 启动传感器
|
|
@@ -160,14 +162,14 @@ const tilt = bridge.utils.getTiltDirection(x, y);
|
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
### 3.3 音频模块(录音、播放)
|
|
163
|
-
> 特别注意拿到的uri文件资源是
|
|
165
|
+
> 特别注意拿到的uri文件资源是app应用文件路径,当前web应用无法直接播放音频,请参考录音模块代码,先调用readAsBase64转码再播放!
|
|
164
166
|
|
|
165
167
|
**播放模块:**
|
|
166
168
|
|
|
167
169
|
单例模式实现,主要用于不需要用户交互就直接播放的背景音乐等,点击音效等仍由web应用本身实现
|
|
168
170
|
|
|
169
171
|
```
|
|
170
|
-
// 播放,支持传入 local://xxx 或 file://xxx 的
|
|
172
|
+
// 播放,支持传入 local://xxx 或 file://xxx 的app应用本地路径,或 https://xxx 的url网址
|
|
171
173
|
await bridge.audio.playOnline({ uri: playUri });
|
|
172
174
|
|
|
173
175
|
// 播放并循环(适用于背景音乐)
|
|
@@ -208,14 +210,14 @@ console.log('时长:', result.durationMillis / 1000, '秒');
|
|
|
208
210
|
|
|
209
211
|
// 保存录音(以aydens-voice为例,实际可以替换为web应用的独特键,或者result.uri的独特文件名)
|
|
210
212
|
if (result?.uri) {
|
|
211
|
-
// 保存文件到
|
|
213
|
+
// 保存文件到app应用应用并记录map
|
|
212
214
|
await bridge.fileStorage.addCustomFileMapping('local://recording/aydens-voice', result.uri);
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
// 1. 后续让
|
|
217
|
+
// 1. 后续让app应用播放
|
|
216
218
|
await bridge.audio.playOnline({ uri: 'local://recording/aydens-voice' });
|
|
217
219
|
|
|
218
|
-
// 2. 后续直接让web
|
|
220
|
+
// 2. 后续直接让web应用播放
|
|
219
221
|
const audioData = await bridge.fileStorage.readAsBase64(result.uri);
|
|
220
222
|
const audio = new Audio(audioData.dataUrl); // dataUrl 已是完整 'data:audio/mp4;base64,...'
|
|
221
223
|
audio.play();
|
|
@@ -227,7 +229,7 @@ audio.play();
|
|
|
227
229
|
|
|
228
230
|
### 3.4 相机模块(实时相机、拍照、相册、闪光灯)
|
|
229
231
|
|
|
230
|
-
>> 特别注意拿到的uri文件资源是
|
|
232
|
+
>> 特别注意拿到的uri文件资源是app应用件路径,当前web应用无法直接渲染,请参考下方**拍照/相册**的代码,先调用readAsBase64转码再渲染!
|
|
231
233
|
|
|
232
234
|
**实时相机**
|
|
233
235
|
|
|
@@ -244,7 +246,7 @@ document.getElementById('root')!.style.background = 'transparent';
|
|
|
244
246
|
|
|
245
247
|
// 拍照
|
|
246
248
|
// ⚠️ 拍照后不要直接渲染,参考下方**拍照/相册**的代码,先调用readAsBase64转码再渲染!
|
|
247
|
-
// ⚠️ 拍照后如果需要保存,也参考下方**拍照/相册**的代码调用
|
|
249
|
+
// ⚠️ 拍照后如果需要保存,也参考下方**拍照/相册**的代码调用app应用的文件存储模块保存并记录map
|
|
248
250
|
const photo = await bridge.camera.takePhoto();
|
|
249
251
|
|
|
250
252
|
// 切换摄像头
|
|
@@ -292,14 +294,14 @@ await bridge.camera.setFilter('none');
|
|
|
292
294
|
// 1. 拍照(以aydens-demo-photo为例,实际web应用使用picked.uri文件名或组特的key)
|
|
293
295
|
const photo = await bridge.fileStorage.takePhoto();
|
|
294
296
|
if (!photo?.cancelled) {
|
|
295
|
-
// 保存文件到
|
|
297
|
+
// 保存文件到app应用并记录map
|
|
296
298
|
await bridge.fileStorage.addCustomFileMapping('local://photos/aydens-demo-photo', photo.uri);
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
// 2. 或从相册选择(以aydens-photo为例,实际web应用使用picked.uri文件名或组特的key)
|
|
300
302
|
const picked = await bridge.fileStorage.pickFromGallery();
|
|
301
303
|
if (!picked?.cancelled) {
|
|
302
|
-
// 保存文件到
|
|
304
|
+
// 保存文件到app应用并记录map
|
|
303
305
|
await bridge.fileStorage.addCustomFileMapping('local://photos/aydens-demo-photo', picked.uri);
|
|
304
306
|
}
|
|
305
307
|
|
|
@@ -309,6 +311,43 @@ const base64 = await bridge.fileStorage.readAsBase64('local://photos/aydens-demo
|
|
|
309
311
|
<img src={`data:image/jpeg;base64,${base64}`} />
|
|
310
312
|
```
|
|
311
313
|
|
|
314
|
+
**保存到系统相册**
|
|
315
|
+
|
|
316
|
+
将图片或视频保存到用户的系统相册(会请求系统相册写入权限)
|
|
317
|
+
|
|
318
|
+
`saveToGallery` 支持自动解析多种 URI 格式,web 应用无需关心传入的是真实路径还是虚拟映射键:
|
|
319
|
+
- `file://...` 真实文件路径:直接使用
|
|
320
|
+
- `local://...` 虚拟映射键:自动从 MAP 查询实际路径
|
|
321
|
+
- `http(s)://...` 网络 URL:自动查找缓存文件
|
|
322
|
+
|
|
323
|
+
```
|
|
324
|
+
// 拍照后保存到系统相册(直接使用 file:// 路径为例)
|
|
325
|
+
const photo = await bridge.fileStorage.takePhoto();
|
|
326
|
+
if (!photo?.cancelled) {
|
|
327
|
+
// 默认存入系统相册Riffle
|
|
328
|
+
const result = await bridge.fileStorage.saveToGallery(photo.uri);
|
|
329
|
+
console.log('保存成功:', result.filename);
|
|
330
|
+
|
|
331
|
+
// 指定自定义相册名
|
|
332
|
+
const result2 = await bridge.fileStorage.saveToGallery(photo.uri, 'MyApp相册');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
返回值类型:
|
|
338
|
+
```typescript
|
|
339
|
+
interface SaveToGalleryResult {
|
|
340
|
+
success: boolean; // 是否成功
|
|
341
|
+
assetId?: string; // 系统相册资产ID
|
|
342
|
+
uri?: string; // 保存后的URI
|
|
343
|
+
filename?: string; // 文件名
|
|
344
|
+
mediaType?: string; // 媒体类型 'photo' | 'video'
|
|
345
|
+
width?: number; // 图片宽度
|
|
346
|
+
height?: number; // 图片高度
|
|
347
|
+
duration?: number; // 视频时长(秒)
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
312
351
|
**闪光灯**
|
|
313
352
|
|
|
314
353
|
闪光灯必须打开实时相机才能调用,参考实时相机的说明。
|
|
@@ -316,7 +355,7 @@ const base64 = await bridge.fileStorage.readAsBase64('local://photos/aydens-demo
|
|
|
316
355
|
当应用需要闪光灯而不要实时相机时,可以使用非透明web应用背景来遮挡实时相机
|
|
317
356
|
|
|
318
357
|
### 3.5 文件存储模块
|
|
319
|
-
>> 特别注意拿到的uri文件资源是
|
|
358
|
+
>> 特别注意拿到的uri文件资源是app应用文件路径,当前web应用无法直接渲染图片或播放音频,请参考下方代码,先调用readAsBase64转码再渲染或播放!相机&录音等模块也都有联动说明
|
|
320
359
|
|
|
321
360
|
文件映射系统维护了一个MAP映射表,支持本地文件和网络资源两种存储,web应用要根据需求选择使用哪种
|
|
322
361
|
|
|
@@ -332,7 +371,7 @@ if (!photo?.cancelled) {
|
|
|
332
371
|
// 查询(查看是否存在)
|
|
333
372
|
const cached = await bridge.fileStorage.getLocalFileByUrl('local://avatar/123');
|
|
334
373
|
|
|
335
|
-
// 拿到base64
|
|
374
|
+
// 拿到base64
|
|
336
375
|
const base64 = await bridge.fileStorage.readAsBase64('local://avatar/123');
|
|
337
376
|
// 在 Web 中渲染
|
|
338
377
|
<img src={`data:image/jpeg;base64,${base64}`} />
|
|
@@ -349,7 +388,7 @@ console.log(result.cached ? '使用缓存' : '已下载', result.uri);
|
|
|
349
388
|
const base64 = await bridge.fileStorage.readAsBase64(url);
|
|
350
389
|
```
|
|
351
390
|
|
|
352
|
-
**读取资源并解码base64,从
|
|
391
|
+
**读取资源并解码base64,从app应用传入web应用**
|
|
353
392
|
```
|
|
354
393
|
// uri为mapRecord记录值
|
|
355
394
|
const base64 = await bridge.fileStorage.readAsBase64(mapRecord);
|
package/dist/index.d.mts
CHANGED
|
@@ -405,6 +405,27 @@ declare class FileStorageController {
|
|
|
405
405
|
added: boolean;
|
|
406
406
|
}>;
|
|
407
407
|
readAsBase64(uriOrKey: string): Promise<Base64Result>;
|
|
408
|
+
/**
|
|
409
|
+
* 解析 URI,返回实际的 file:// 路径
|
|
410
|
+
* @param uri 支持 file://、local:// 或其他映射键
|
|
411
|
+
*/
|
|
412
|
+
private resolveUri;
|
|
413
|
+
/**
|
|
414
|
+
* 保存图片/视频到系统相册
|
|
415
|
+
* 自动解析 URI:支持 file:// 真实路径、local:// 虚拟映射键、http(s):// 缓存 URL
|
|
416
|
+
* @param uri 文件 URI(支持 file://、local:// 或 http(s):// 路径)
|
|
417
|
+
* @param albumName 相册名称(默认 'Riffle')
|
|
418
|
+
*/
|
|
419
|
+
saveToGallery(uri: string, albumName?: string): Promise<{
|
|
420
|
+
success: boolean;
|
|
421
|
+
assetId?: string;
|
|
422
|
+
uri?: string;
|
|
423
|
+
filename?: string;
|
|
424
|
+
mediaType?: string;
|
|
425
|
+
width?: number;
|
|
426
|
+
height?: number;
|
|
427
|
+
duration?: number;
|
|
428
|
+
}>;
|
|
408
429
|
}
|
|
409
430
|
interface PlayOptions {
|
|
410
431
|
uri: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -405,6 +405,27 @@ declare class FileStorageController {
|
|
|
405
405
|
added: boolean;
|
|
406
406
|
}>;
|
|
407
407
|
readAsBase64(uriOrKey: string): Promise<Base64Result>;
|
|
408
|
+
/**
|
|
409
|
+
* 解析 URI,返回实际的 file:// 路径
|
|
410
|
+
* @param uri 支持 file://、local:// 或其他映射键
|
|
411
|
+
*/
|
|
412
|
+
private resolveUri;
|
|
413
|
+
/**
|
|
414
|
+
* 保存图片/视频到系统相册
|
|
415
|
+
* 自动解析 URI:支持 file:// 真实路径、local:// 虚拟映射键、http(s):// 缓存 URL
|
|
416
|
+
* @param uri 文件 URI(支持 file://、local:// 或 http(s):// 路径)
|
|
417
|
+
* @param albumName 相册名称(默认 'Riffle')
|
|
418
|
+
*/
|
|
419
|
+
saveToGallery(uri: string, albumName?: string): Promise<{
|
|
420
|
+
success: boolean;
|
|
421
|
+
assetId?: string;
|
|
422
|
+
uri?: string;
|
|
423
|
+
filename?: string;
|
|
424
|
+
mediaType?: string;
|
|
425
|
+
width?: number;
|
|
426
|
+
height?: number;
|
|
427
|
+
duration?: number;
|
|
428
|
+
}>;
|
|
408
429
|
}
|
|
409
430
|
interface PlayOptions {
|
|
410
431
|
uri: string;
|
package/dist/index.js
CHANGED
|
@@ -419,6 +419,44 @@ var FileStorageController = class {
|
|
|
419
419
|
async readAsBase64(uriOrKey) {
|
|
420
420
|
return this.core.send("fileStorage", "readAsBase64", { uriOrKey });
|
|
421
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* 解析 URI,返回实际的 file:// 路径
|
|
424
|
+
* @param uri 支持 file://、local:// 或其他映射键
|
|
425
|
+
*/
|
|
426
|
+
async resolveUri(uri) {
|
|
427
|
+
if (!uri) throw new Error("URI is required");
|
|
428
|
+
if (uri.startsWith("file://")) {
|
|
429
|
+
return uri;
|
|
430
|
+
}
|
|
431
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) {
|
|
432
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
433
|
+
if (cached?.exists && cached.uri) {
|
|
434
|
+
return cached.uri;
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`File mapping not found: ${uri}`);
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
440
|
+
if (cached?.exists && cached.uri) {
|
|
441
|
+
return cached.uri;
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return uri;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 保存图片/视频到系统相册
|
|
449
|
+
* 自动解析 URI:支持 file:// 真实路径、local:// 虚拟映射键、http(s):// 缓存 URL
|
|
450
|
+
* @param uri 文件 URI(支持 file://、local:// 或 http(s):// 路径)
|
|
451
|
+
* @param albumName 相册名称(默认 'Riffle')
|
|
452
|
+
*/
|
|
453
|
+
async saveToGallery(uri, albumName = "Riffle") {
|
|
454
|
+
const resolvedUri = await this.resolveUri(uri);
|
|
455
|
+
return this.core.send("fileStorage", "saveToGallery", {
|
|
456
|
+
saveUri: resolvedUri,
|
|
457
|
+
albumName
|
|
458
|
+
});
|
|
459
|
+
}
|
|
422
460
|
};
|
|
423
461
|
var AudioController = class {
|
|
424
462
|
constructor(core, fileStorage) {
|
package/dist/index.mjs
CHANGED
|
@@ -377,6 +377,44 @@ var FileStorageController = class {
|
|
|
377
377
|
async readAsBase64(uriOrKey) {
|
|
378
378
|
return this.core.send("fileStorage", "readAsBase64", { uriOrKey });
|
|
379
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* 解析 URI,返回实际的 file:// 路径
|
|
382
|
+
* @param uri 支持 file://、local:// 或其他映射键
|
|
383
|
+
*/
|
|
384
|
+
async resolveUri(uri) {
|
|
385
|
+
if (!uri) throw new Error("URI is required");
|
|
386
|
+
if (uri.startsWith("file://")) {
|
|
387
|
+
return uri;
|
|
388
|
+
}
|
|
389
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) {
|
|
390
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
391
|
+
if (cached?.exists && cached.uri) {
|
|
392
|
+
return cached.uri;
|
|
393
|
+
}
|
|
394
|
+
throw new Error(`File mapping not found: ${uri}`);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
398
|
+
if (cached?.exists && cached.uri) {
|
|
399
|
+
return cached.uri;
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
return uri;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 保存图片/视频到系统相册
|
|
407
|
+
* 自动解析 URI:支持 file:// 真实路径、local:// 虚拟映射键、http(s):// 缓存 URL
|
|
408
|
+
* @param uri 文件 URI(支持 file://、local:// 或 http(s):// 路径)
|
|
409
|
+
* @param albumName 相册名称(默认 'Riffle')
|
|
410
|
+
*/
|
|
411
|
+
async saveToGallery(uri, albumName = "Riffle") {
|
|
412
|
+
const resolvedUri = await this.resolveUri(uri);
|
|
413
|
+
return this.core.send("fileStorage", "saveToGallery", {
|
|
414
|
+
saveUri: resolvedUri,
|
|
415
|
+
albumName
|
|
416
|
+
});
|
|
417
|
+
}
|
|
380
418
|
};
|
|
381
419
|
var AudioController = class {
|
|
382
420
|
constructor(core, fileStorage) {
|
package/package.json
CHANGED
package/src/controllers/index.ts
CHANGED
|
@@ -301,6 +301,65 @@ export class FileStorageController {
|
|
|
301
301
|
async readAsBase64(uriOrKey: string): Promise<Base64Result> {
|
|
302
302
|
return this.core.send<Base64Result>('fileStorage', 'readAsBase64', { uriOrKey });
|
|
303
303
|
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 解析 URI,返回实际的 file:// 路径
|
|
307
|
+
* @param uri 支持 file://、local:// 或其他映射键
|
|
308
|
+
*/
|
|
309
|
+
private async resolveUri(uri: string): Promise<string> {
|
|
310
|
+
if (!uri) throw new Error('URI is required');
|
|
311
|
+
|
|
312
|
+
// file:// 协议直接使用
|
|
313
|
+
if (uri.startsWith('file://')) {
|
|
314
|
+
return uri;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 非 http/https 协议(如 local://),从 MAP 查询
|
|
318
|
+
if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
|
|
319
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
320
|
+
if (cached?.exists && cached.uri) {
|
|
321
|
+
return cached.uri;
|
|
322
|
+
}
|
|
323
|
+
throw new Error(`File mapping not found: ${uri}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// http/https URL,先查缓存
|
|
327
|
+
try {
|
|
328
|
+
const cached = await this.getLocalFileByUrl(uri);
|
|
329
|
+
if (cached?.exists && cached.uri) {
|
|
330
|
+
return cached.uri;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// 查询失败,继续使用原始 URL
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return uri;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 保存图片/视频到系统相册
|
|
341
|
+
* 自动解析 URI:支持 file:// 真实路径、local:// 虚拟映射键、http(s):// 缓存 URL
|
|
342
|
+
* @param uri 文件 URI(支持 file://、local:// 或 http(s):// 路径)
|
|
343
|
+
* @param albumName 相册名称(默认 'Riffle')
|
|
344
|
+
*/
|
|
345
|
+
async saveToGallery(uri: string, albumName = 'Riffle'): Promise<{
|
|
346
|
+
success: boolean;
|
|
347
|
+
assetId?: string;
|
|
348
|
+
uri?: string;
|
|
349
|
+
filename?: string;
|
|
350
|
+
mediaType?: string;
|
|
351
|
+
width?: number;
|
|
352
|
+
height?: number;
|
|
353
|
+
duration?: number;
|
|
354
|
+
}> {
|
|
355
|
+
// 先解析为实际的 file:// 路径
|
|
356
|
+
const resolvedUri = await this.resolveUri(uri);
|
|
357
|
+
|
|
358
|
+
return this.core.send('fileStorage', 'saveToGallery', {
|
|
359
|
+
saveUri: resolvedUri,
|
|
360
|
+
albumName,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
304
363
|
}
|
|
305
364
|
|
|
306
365
|
// ============================================
|