@nebula-rn/host-apis 0.0.1
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/dist/NebulaHostPreviewImageModal.d.ts +2 -0
- package/dist/NebulaHostPreviewImageModal.js +81 -0
- package/dist/NebulaHostScanModal.d.ts +2 -0
- package/dist/NebulaHostScanModal.js +172 -0
- package/dist/PreviewImageHostBridge.d.ts +8 -0
- package/dist/PreviewImageHostBridge.js +21 -0
- package/dist/ScanCodeHostBridge.d.ts +1 -0
- package/dist/ScanCodeHostBridge.js +38 -0
- package/dist/coreHostApis.d.ts +8 -0
- package/dist/coreHostApis.js +833 -0
- package/dist/hostApiBridge.d.ts +23 -0
- package/dist/hostApiBridge.js +54 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +11 -0
- package/dist/previewImageRuntime.d.ts +8 -0
- package/dist/previewImageRuntime.js +2 -0
- package/dist/scanCodeRuntime.d.ts +5 -0
- package/dist/scanCodeRuntime.js +2 -0
- package/dist/taskHosts.d.ts +27 -0
- package/dist/taskHosts.js +204 -0
- package/package.json +51 -0
- package/src/NebulaHostPreviewImageModal.tsx +119 -0
- package/src/NebulaHostScanModal.tsx +263 -0
- package/src/PreviewImageHostBridge.tsx +36 -0
- package/src/ScanCodeHostBridge.tsx +58 -0
- package/src/assets/icon_close.png +0 -0
- package/src/assets/icon_pic.png +0 -0
- package/src/coreHostApis.ts +1072 -0
- package/src/hostApiBridge.ts +103 -0
- package/src/index.ts +23 -0
- package/src/previewImageRuntime.ts +12 -0
- package/src/scanCodeRuntime.ts +8 -0
- package/src/taskHosts.ts +269 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHostApiFailure, createHostApiSuccess } from '@nebula-rn/sdk';
|
|
2
|
+
import type { NebulaApiInvokeResult } from '@nebula-rn/sdk';
|
|
3
|
+
export type HostApiEventMessage<TPayload = unknown> = {
|
|
4
|
+
__nebulaApiEvent: 'v1';
|
|
5
|
+
apiName: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
subscriptionId?: string;
|
|
8
|
+
taskId?: string;
|
|
9
|
+
payload?: TPayload;
|
|
10
|
+
};
|
|
11
|
+
export declare function createHostBridgeId(prefix: string): string;
|
|
12
|
+
export declare function emitHostApiEvent<TPayload>(appId: string, message: HostApiEventMessage<TPayload>): Promise<void>;
|
|
13
|
+
export declare function createSubscriptionStartResult(subscriptionId: string): NebulaApiInvokeResult<{
|
|
14
|
+
subscriptionId: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createTaskStartResult(taskId: string): NebulaApiInvokeResult<{
|
|
17
|
+
taskId: string;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function emitSubscriptionEvent<TPayload>(appId: string, apiName: string, subscriptionId: string, payload: TPayload): Promise<void>;
|
|
20
|
+
export declare function emitTaskProgress<TPayload>(appId: string, apiName: string, taskId: string, payload: TPayload): Promise<void>;
|
|
21
|
+
export declare function emitTaskHeaders<TPayload>(appId: string, apiName: string, taskId: string, payload: TPayload): Promise<void>;
|
|
22
|
+
export declare function emitTaskResult<TPayload>(appId: string, apiName: string, taskId: string, result: NebulaApiInvokeResult<TPayload>): Promise<void>;
|
|
23
|
+
export { createHostApiFailure, createHostApiSuccess };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NebulaAPI, createHostApiFailure, createHostApiSuccess, } from '@nebula-rn/sdk';
|
|
2
|
+
export function createHostBridgeId(prefix) {
|
|
3
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
4
|
+
}
|
|
5
|
+
export async function emitHostApiEvent(appId, message) {
|
|
6
|
+
await NebulaAPI.postMessageToMiniApp(appId, message);
|
|
7
|
+
}
|
|
8
|
+
export function createSubscriptionStartResult(subscriptionId) {
|
|
9
|
+
return createHostApiSuccess({
|
|
10
|
+
subscriptionId,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function createTaskStartResult(taskId) {
|
|
14
|
+
return createHostApiSuccess({
|
|
15
|
+
taskId,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function emitSubscriptionEvent(appId, apiName, subscriptionId, payload) {
|
|
19
|
+
return emitHostApiEvent(appId, {
|
|
20
|
+
__nebulaApiEvent: 'v1',
|
|
21
|
+
apiName,
|
|
22
|
+
channel: 'subscription',
|
|
23
|
+
subscriptionId,
|
|
24
|
+
payload,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function emitTaskProgress(appId, apiName, taskId, payload) {
|
|
28
|
+
return emitHostApiEvent(appId, {
|
|
29
|
+
__nebulaApiEvent: 'v1',
|
|
30
|
+
apiName,
|
|
31
|
+
channel: 'task.progress',
|
|
32
|
+
taskId,
|
|
33
|
+
payload,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function emitTaskHeaders(appId, apiName, taskId, payload) {
|
|
37
|
+
return emitHostApiEvent(appId, {
|
|
38
|
+
__nebulaApiEvent: 'v1',
|
|
39
|
+
apiName,
|
|
40
|
+
channel: 'task.headers',
|
|
41
|
+
taskId,
|
|
42
|
+
payload,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function emitTaskResult(appId, apiName, taskId, result) {
|
|
46
|
+
return emitHostApiEvent(appId, {
|
|
47
|
+
__nebulaApiEvent: 'v1',
|
|
48
|
+
apiName,
|
|
49
|
+
channel: 'task.result',
|
|
50
|
+
taskId,
|
|
51
|
+
payload: result,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export { createHostApiFailure, createHostApiSuccess };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { previewImageHostApi } from './PreviewImageHostBridge';
|
|
2
|
+
import { scanCodeHostApi } from './ScanCodeHostBridge';
|
|
3
|
+
import { coreHostApis, saveMediaHost } from './coreHostApis';
|
|
4
|
+
import { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost } from './taskHosts';
|
|
5
|
+
export { previewImageHostApi, scanCodeHostApi, coreHostApis, saveMediaHost };
|
|
6
|
+
export { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, };
|
|
7
|
+
export declare const defaultHostApis: import("@nebula-rn/sdk").NebulaHostFeature[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { previewImageHostApi } from './PreviewImageHostBridge';
|
|
2
|
+
import { scanCodeHostApi } from './ScanCodeHostBridge';
|
|
3
|
+
import { coreHostApis, saveMediaHost } from './coreHostApis';
|
|
4
|
+
import { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, } from './taskHosts';
|
|
5
|
+
export { previewImageHostApi, scanCodeHostApi, coreHostApis, saveMediaHost };
|
|
6
|
+
export { downloadFileHostApi, downloadFileHost, uploadFileHostApi, uploadFileHost, };
|
|
7
|
+
export const defaultHostApis = [
|
|
8
|
+
scanCodeHostApi,
|
|
9
|
+
previewImageHostApi,
|
|
10
|
+
...coreHostApis,
|
|
11
|
+
];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type DownloadFileHostOption = {
|
|
2
|
+
url: string;
|
|
3
|
+
header?: Record<string, string>;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
filePath?: string;
|
|
6
|
+
};
|
|
7
|
+
type DownloadFileHostResult = {
|
|
8
|
+
tempFilePath: string;
|
|
9
|
+
statusCode: number;
|
|
10
|
+
};
|
|
11
|
+
type UploadFileHostOption = {
|
|
12
|
+
url: string;
|
|
13
|
+
filePath: string;
|
|
14
|
+
name: string;
|
|
15
|
+
header?: Record<string, string>;
|
|
16
|
+
formData?: Record<string, string>;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
};
|
|
19
|
+
type UploadFileHostResult = {
|
|
20
|
+
data: string;
|
|
21
|
+
statusCode: number;
|
|
22
|
+
};
|
|
23
|
+
export declare const downloadFileHost: (options: DownloadFileHostOption) => Promise<DownloadFileHostResult>;
|
|
24
|
+
export declare const uploadFileHost: (options: UploadFileHostOption) => Promise<UploadFileHostResult>;
|
|
25
|
+
export declare const downloadFileHostApi: import("@nebula-rn/sdk").NebulaHostFeature[];
|
|
26
|
+
export declare const uploadFileHostApi: import("@nebula-rn/sdk").NebulaHostFeature[];
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as RNFS from '@dr.pogodin/react-native-fs';
|
|
2
|
+
import { createHostApiFeature } from '@nebula-rn/sdk';
|
|
3
|
+
import { createTaskStartResult, emitTaskHeaders, emitTaskProgress, emitTaskResult, } from './hostApiBridge';
|
|
4
|
+
const activeDownloads = new Map();
|
|
5
|
+
const activeUploads = new Map();
|
|
6
|
+
const TASK_API_DESCRIPTIONS = {
|
|
7
|
+
'downloadFile.start': {
|
|
8
|
+
summary: 'Start a host-managed file download task with progress events.',
|
|
9
|
+
tags: ['file', 'task', 'network'],
|
|
10
|
+
},
|
|
11
|
+
'downloadFile.abort': {
|
|
12
|
+
summary: 'Abort an in-flight host download task by task id.',
|
|
13
|
+
tags: ['file', 'task', 'network'],
|
|
14
|
+
},
|
|
15
|
+
'uploadFile.start': {
|
|
16
|
+
summary: 'Start a host-managed file upload task with progress events.',
|
|
17
|
+
tags: ['file', 'task', 'network'],
|
|
18
|
+
},
|
|
19
|
+
'uploadFile.abort': {
|
|
20
|
+
summary: 'Abort an in-flight host upload task by task id.',
|
|
21
|
+
tags: ['file', 'task', 'network'],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export const downloadFileHost = async (options) => {
|
|
25
|
+
const { url, header = {}, timeout = 60000, filePath } = options;
|
|
26
|
+
const destPath = filePath || `${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
|
|
27
|
+
const result = await RNFS.downloadFile({
|
|
28
|
+
fromUrl: url,
|
|
29
|
+
toFile: destPath,
|
|
30
|
+
headers: header,
|
|
31
|
+
connectionTimeout: timeout,
|
|
32
|
+
readTimeout: timeout,
|
|
33
|
+
}).promise;
|
|
34
|
+
return {
|
|
35
|
+
tempFilePath: destPath,
|
|
36
|
+
statusCode: result.statusCode,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export const uploadFileHost = async (options) => {
|
|
40
|
+
const nativeTask = RNFS.uploadFiles({
|
|
41
|
+
toUrl: options.url,
|
|
42
|
+
files: [
|
|
43
|
+
{
|
|
44
|
+
name: options.name,
|
|
45
|
+
filename: options.filePath.split('/').pop() || 'file',
|
|
46
|
+
filepath: options.filePath,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
headers: options.header ?? {},
|
|
50
|
+
fields: options.formData ?? {},
|
|
51
|
+
method: 'POST',
|
|
52
|
+
});
|
|
53
|
+
const result = await nativeTask.promise;
|
|
54
|
+
return {
|
|
55
|
+
data: result.body,
|
|
56
|
+
statusCode: result.statusCode,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export const downloadFileHostApi = [
|
|
60
|
+
createHostApiFeature({
|
|
61
|
+
apiName: 'downloadFile.start',
|
|
62
|
+
description: TASK_API_DESCRIPTIONS['downloadFile.start'],
|
|
63
|
+
async handle(payload, context) {
|
|
64
|
+
const taskId = `download-${Date.now()}-${Math.random()
|
|
65
|
+
.toString(36)
|
|
66
|
+
.slice(2, 10)}`;
|
|
67
|
+
const options = payload;
|
|
68
|
+
const destPath = options.filePath ||
|
|
69
|
+
`${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
|
|
70
|
+
const nativeTask = RNFS.downloadFile({
|
|
71
|
+
fromUrl: options.url,
|
|
72
|
+
toFile: destPath,
|
|
73
|
+
headers: options.header ?? {},
|
|
74
|
+
connectionTimeout: options.timeout ?? 60000,
|
|
75
|
+
readTimeout: options.timeout ?? 60000,
|
|
76
|
+
begin: res => {
|
|
77
|
+
emitTaskHeaders(context.appId, 'downloadFile', taskId, {
|
|
78
|
+
header: res.headers,
|
|
79
|
+
}).catch(() => { });
|
|
80
|
+
},
|
|
81
|
+
progress: res => {
|
|
82
|
+
emitTaskProgress(context.appId, 'downloadFile', taskId, {
|
|
83
|
+
progress: Math.floor((res.bytesWritten / res.contentLength) * 100),
|
|
84
|
+
totalBytesWritten: res.bytesWritten,
|
|
85
|
+
totalBytesExpectedToWrite: res.contentLength,
|
|
86
|
+
}).catch(() => { });
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
activeDownloads.set(taskId, nativeTask.jobId);
|
|
90
|
+
nativeTask.promise
|
|
91
|
+
.then(res => emitTaskResult(context.appId, 'downloadFile', taskId, {
|
|
92
|
+
ok: true,
|
|
93
|
+
data: {
|
|
94
|
+
tempFilePath: destPath,
|
|
95
|
+
statusCode: res.statusCode,
|
|
96
|
+
},
|
|
97
|
+
}))
|
|
98
|
+
.catch(error => emitTaskResult(context.appId, 'downloadFile', taskId, {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: {
|
|
101
|
+
code: 'DOWNLOAD_FAILED',
|
|
102
|
+
message: error instanceof Error
|
|
103
|
+
? error.message
|
|
104
|
+
: 'downloadFile:fail host download failed',
|
|
105
|
+
},
|
|
106
|
+
}))
|
|
107
|
+
.finally(() => {
|
|
108
|
+
activeDownloads.delete(taskId);
|
|
109
|
+
});
|
|
110
|
+
return createTaskStartResult(taskId);
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
createHostApiFeature({
|
|
114
|
+
apiName: 'downloadFile.abort',
|
|
115
|
+
description: TASK_API_DESCRIPTIONS['downloadFile.abort'],
|
|
116
|
+
async handle(payload) {
|
|
117
|
+
const taskId = String(payload.taskId ?? '');
|
|
118
|
+
const jobId = activeDownloads.get(taskId);
|
|
119
|
+
if (jobId) {
|
|
120
|
+
RNFS.stopDownload(jobId);
|
|
121
|
+
activeDownloads.delete(taskId);
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
data: null,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
];
|
|
130
|
+
export const uploadFileHostApi = [
|
|
131
|
+
createHostApiFeature({
|
|
132
|
+
apiName: 'uploadFile.start',
|
|
133
|
+
description: TASK_API_DESCRIPTIONS['uploadFile.start'],
|
|
134
|
+
async handle(payload, context) {
|
|
135
|
+
const taskId = `upload-${Date.now()}-${Math.random()
|
|
136
|
+
.toString(36)
|
|
137
|
+
.slice(2, 10)}`;
|
|
138
|
+
const options = payload;
|
|
139
|
+
const nativeTask = RNFS.uploadFiles({
|
|
140
|
+
toUrl: options.url,
|
|
141
|
+
files: [
|
|
142
|
+
{
|
|
143
|
+
name: options.name,
|
|
144
|
+
filename: options.filePath.split('/').pop() || 'file',
|
|
145
|
+
filepath: options.filePath,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
headers: options.header ?? {},
|
|
149
|
+
fields: options.formData ?? {},
|
|
150
|
+
method: 'POST',
|
|
151
|
+
progress: res => {
|
|
152
|
+
emitTaskProgress(context.appId, 'uploadFile', taskId, {
|
|
153
|
+
progress: Math.floor((res.totalBytesSent / res.totalBytesExpectedToSend) * 100),
|
|
154
|
+
totalBytesSent: res.totalBytesSent,
|
|
155
|
+
totalBytesExpectedToSend: res.totalBytesExpectedToSend,
|
|
156
|
+
}).catch(() => { });
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
activeUploads.set(taskId, nativeTask.jobId);
|
|
160
|
+
nativeTask.promise
|
|
161
|
+
.then(res => Promise.all([
|
|
162
|
+
emitTaskHeaders(context.appId, 'uploadFile', taskId, {
|
|
163
|
+
header: res.headers,
|
|
164
|
+
}),
|
|
165
|
+
emitTaskResult(context.appId, 'uploadFile', taskId, {
|
|
166
|
+
ok: true,
|
|
167
|
+
data: {
|
|
168
|
+
data: res.body,
|
|
169
|
+
statusCode: res.statusCode,
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
]))
|
|
173
|
+
.catch(error => emitTaskResult(context.appId, 'uploadFile', taskId, {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: {
|
|
176
|
+
code: 'UPLOAD_FAILED',
|
|
177
|
+
message: error instanceof Error
|
|
178
|
+
? error.message
|
|
179
|
+
: 'uploadFile:fail host upload failed',
|
|
180
|
+
},
|
|
181
|
+
}))
|
|
182
|
+
.finally(() => {
|
|
183
|
+
activeUploads.delete(taskId);
|
|
184
|
+
});
|
|
185
|
+
return createTaskStartResult(taskId);
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
createHostApiFeature({
|
|
189
|
+
apiName: 'uploadFile.abort',
|
|
190
|
+
description: TASK_API_DESCRIPTIONS['uploadFile.abort'],
|
|
191
|
+
async handle(payload) {
|
|
192
|
+
const taskId = String(payload.taskId ?? '');
|
|
193
|
+
const jobId = activeUploads.get(taskId);
|
|
194
|
+
if (jobId) {
|
|
195
|
+
RNFS.stopUpload(jobId);
|
|
196
|
+
activeUploads.delete(taskId);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
data: null,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nebula-rn/host-apis",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Official Nebula host API feature bundle",
|
|
5
|
+
"author": "Hector Zhuang",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Hector-Zhuang/nebula.git",
|
|
10
|
+
"directory": "packages/host-apis"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Hector-Zhuang/nebula/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Hector-Zhuang/nebula/tree/main/packages/host-apis#readme",
|
|
16
|
+
"keywords": ["nebula", "superapp", "miniapp", "react-native"],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"react-native": "./src/index.ts",
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"files": ["dist", "src", "README.md"],
|
|
22
|
+
"publishConfig": { "access": "public" },
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"lint": "eslint src --ext .ts,.tsx"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
|
30
|
+
"@dr.pogodin/react-native-fs": "^2.37.0",
|
|
31
|
+
"@nebula-rn/sdk": "^0.0.1",
|
|
32
|
+
"@react-native-camera-roll/camera-roll": "^7.10.2",
|
|
33
|
+
"@react-native-clipboard/clipboard": "^1.16.3",
|
|
34
|
+
"@react-native-community/geolocation": "^3.4.0",
|
|
35
|
+
"@react-native-community/netinfo": "^12.0.1",
|
|
36
|
+
"react": ">=19",
|
|
37
|
+
"react-native": ">=0.83",
|
|
38
|
+
"react-native-device-info": "^15.0.2",
|
|
39
|
+
"react-native-image-picker": "^8.2.1",
|
|
40
|
+
"react-native-image-zoom-viewer": "^3.0.1",
|
|
41
|
+
"react-native-mmkv": "^4.2.0",
|
|
42
|
+
"react-native-safe-area-context": "^5.5.2",
|
|
43
|
+
"react-native-screenshot-aware": "^2.0.0",
|
|
44
|
+
"react-native-sensors": "^7.3.6",
|
|
45
|
+
"react-native-vision-camera": "^4.7.3"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=19",
|
|
49
|
+
"react-native": ">=0.83"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
|
3
|
+
import ImageViewer from 'react-native-image-zoom-viewer';
|
|
4
|
+
import { NebulaAPI } from '@nebula-rn/sdk';
|
|
5
|
+
import { createHostApiFailure, createHostApiSuccess } from './hostApiBridge';
|
|
6
|
+
import { previewImageChannel } from './previewImageRuntime';
|
|
7
|
+
import { saveMediaHost } from './coreHostApis';
|
|
8
|
+
import { downloadFileHost } from './taskHosts';
|
|
9
|
+
|
|
10
|
+
function PreviewImageLoading(): React.ReactElement {
|
|
11
|
+
return (
|
|
12
|
+
<View style={styles.loadingContainer}>
|
|
13
|
+
<ActivityIndicator size="large" color="#999" />
|
|
14
|
+
</View>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function renderPreviewImageLoading() {
|
|
19
|
+
return <PreviewImageLoading />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function NebulaHostPreviewImageModal(): React.ReactElement | null {
|
|
23
|
+
const [request, setRequest] = useState(previewImageChannel.getCurrent());
|
|
24
|
+
const didCloseRef = useRef(false);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
return previewImageChannel.subscribe(nextRequest => {
|
|
28
|
+
didCloseRef.current = false;
|
|
29
|
+
setRequest(nextRequest);
|
|
30
|
+
});
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const imageUrls = useMemo(
|
|
34
|
+
() => request?.urls.map(url => ({ url })) ?? [],
|
|
35
|
+
[request],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (!request) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const initialIndex = request.current
|
|
43
|
+
? request.urls.indexOf(request.current)
|
|
44
|
+
: 0;
|
|
45
|
+
const index = initialIndex === -1 ? 0 : initialIndex;
|
|
46
|
+
|
|
47
|
+
const close = (
|
|
48
|
+
result:
|
|
49
|
+
| ReturnType<typeof createHostApiSuccess<{ dismissed: boolean }>>
|
|
50
|
+
| ReturnType<typeof createHostApiFailure> = createHostApiSuccess({
|
|
51
|
+
dismissed: true,
|
|
52
|
+
}),
|
|
53
|
+
) => {
|
|
54
|
+
if (didCloseRef.current) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
didCloseRef.current = true;
|
|
58
|
+
NebulaAPI.dismissHostModal()
|
|
59
|
+
.catch(() => {})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
previewImageChannel.settle(result);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<View style={styles.container}>
|
|
67
|
+
<ImageViewer
|
|
68
|
+
imageUrls={imageUrls}
|
|
69
|
+
index={index}
|
|
70
|
+
onCancel={() => close()}
|
|
71
|
+
onClick={() => close()}
|
|
72
|
+
onSwipeDown={() => close()}
|
|
73
|
+
enableSwipeDown
|
|
74
|
+
useNativeDriver
|
|
75
|
+
loadingRender={renderPreviewImageLoading}
|
|
76
|
+
onSave={async uri => {
|
|
77
|
+
try {
|
|
78
|
+
const download = await downloadFileHost({
|
|
79
|
+
url: uri,
|
|
80
|
+
});
|
|
81
|
+
await saveMediaHost({
|
|
82
|
+
url: download.tempFilePath,
|
|
83
|
+
type: 'photo',
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
close(
|
|
87
|
+
createHostApiFailure(
|
|
88
|
+
'PREVIEW_IMAGE_SAVE_FAILED',
|
|
89
|
+
error instanceof Error
|
|
90
|
+
? error.message
|
|
91
|
+
: 'previewImage:fail unable to save image',
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
menuContext={
|
|
97
|
+
request.showMenu === false
|
|
98
|
+
? undefined
|
|
99
|
+
: {
|
|
100
|
+
saveToLocal: request.saveMediaText ?? 'Save Image',
|
|
101
|
+
cancel: request.cancelText ?? 'Cancel',
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const styles = StyleSheet.create({
|
|
110
|
+
container: {
|
|
111
|
+
flex: 1,
|
|
112
|
+
backgroundColor: '#000',
|
|
113
|
+
},
|
|
114
|
+
loadingContainer: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
justifyContent: 'center',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
},
|
|
119
|
+
});
|