@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,81 @@
|
|
|
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
|
+
function PreviewImageLoading() {
|
|
10
|
+
return (<View style={styles.loadingContainer}>
|
|
11
|
+
<ActivityIndicator size="large" color="#999"/>
|
|
12
|
+
</View>);
|
|
13
|
+
}
|
|
14
|
+
function renderPreviewImageLoading() {
|
|
15
|
+
return <PreviewImageLoading />;
|
|
16
|
+
}
|
|
17
|
+
export default function NebulaHostPreviewImageModal() {
|
|
18
|
+
const [request, setRequest] = useState(previewImageChannel.getCurrent());
|
|
19
|
+
const didCloseRef = useRef(false);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
return previewImageChannel.subscribe(nextRequest => {
|
|
22
|
+
didCloseRef.current = false;
|
|
23
|
+
setRequest(nextRequest);
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
const imageUrls = useMemo(() => request?.urls.map(url => ({ url })) ?? [], [request]);
|
|
27
|
+
if (!request) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const initialIndex = request.current
|
|
31
|
+
? request.urls.indexOf(request.current)
|
|
32
|
+
: 0;
|
|
33
|
+
const index = initialIndex === -1 ? 0 : initialIndex;
|
|
34
|
+
const close = (result = createHostApiSuccess({
|
|
35
|
+
dismissed: true,
|
|
36
|
+
})) => {
|
|
37
|
+
if (didCloseRef.current) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
didCloseRef.current = true;
|
|
41
|
+
NebulaAPI.dismissHostModal()
|
|
42
|
+
.catch(() => { })
|
|
43
|
+
.finally(() => {
|
|
44
|
+
previewImageChannel.settle(result);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
return (<View style={styles.container}>
|
|
48
|
+
<ImageViewer imageUrls={imageUrls} index={index} onCancel={() => close()} onClick={() => close()} onSwipeDown={() => close()} enableSwipeDown useNativeDriver loadingRender={renderPreviewImageLoading} onSave={async (uri) => {
|
|
49
|
+
try {
|
|
50
|
+
const download = await downloadFileHost({
|
|
51
|
+
url: uri,
|
|
52
|
+
});
|
|
53
|
+
await saveMediaHost({
|
|
54
|
+
url: download.tempFilePath,
|
|
55
|
+
type: 'photo',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
close(createHostApiFailure('PREVIEW_IMAGE_SAVE_FAILED', error instanceof Error
|
|
60
|
+
? error.message
|
|
61
|
+
: 'previewImage:fail unable to save image'));
|
|
62
|
+
}
|
|
63
|
+
}} menuContext={request.showMenu === false
|
|
64
|
+
? undefined
|
|
65
|
+
: {
|
|
66
|
+
saveToLocal: request.saveMediaText ?? 'Save Image',
|
|
67
|
+
cancel: request.cancelText ?? 'Cancel',
|
|
68
|
+
}}/>
|
|
69
|
+
</View>);
|
|
70
|
+
}
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
container: {
|
|
73
|
+
flex: 1,
|
|
74
|
+
backgroundColor: '#000',
|
|
75
|
+
},
|
|
76
|
+
loadingContainer: {
|
|
77
|
+
flex: 1,
|
|
78
|
+
justifyContent: 'center',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Dimensions, Image, PermissionsAndroid, Platform, StatusBar, StyleSheet, TouchableOpacity, View, } from 'react-native';
|
|
3
|
+
import { Camera, useCameraDevice, useCodeScanner, } from 'react-native-vision-camera';
|
|
4
|
+
import { NebulaAPI } from '@nebula-rn/sdk';
|
|
5
|
+
import { scanCodeChannel } from './scanCodeRuntime';
|
|
6
|
+
const { width, height } = Dimensions.get('screen');
|
|
7
|
+
function createFailureResult(code, message) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
error: {
|
|
11
|
+
code,
|
|
12
|
+
message,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function createSuccessResult(code) {
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
data: {
|
|
20
|
+
result: code.value,
|
|
21
|
+
scanType: code.type,
|
|
22
|
+
rawData: code,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export default function NebulaHostScanModal() {
|
|
27
|
+
const [request, setRequest] = useState(scanCodeChannel.getCurrent());
|
|
28
|
+
const [isActive, setIsActive] = useState(true);
|
|
29
|
+
const [hasPermission, setHasPermission] = useState(Platform.OS !== 'android');
|
|
30
|
+
const didHandleScanRef = useRef(false);
|
|
31
|
+
const device = useCameraDevice('back');
|
|
32
|
+
const scanTypes = (request?.scanTypes ?? [
|
|
33
|
+
'qr',
|
|
34
|
+
'ean-13',
|
|
35
|
+
'code-128',
|
|
36
|
+
]);
|
|
37
|
+
const codeScanner = useCodeScanner({
|
|
38
|
+
codeTypes: scanTypes,
|
|
39
|
+
onCodeScanned: codes => {
|
|
40
|
+
if (!codes[0] || !isActive || didHandleScanRef.current) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
didHandleScanRef.current = true;
|
|
44
|
+
setIsActive(false);
|
|
45
|
+
closeWithResult(createSuccessResult(codes[0]));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const closeWithResult = (result) => {
|
|
49
|
+
NebulaAPI.dismissHostModal()
|
|
50
|
+
.catch(() => { })
|
|
51
|
+
.finally(() => {
|
|
52
|
+
scanCodeChannel.settle(result);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
return scanCodeChannel.subscribe(nextRequest => {
|
|
57
|
+
didHandleScanRef.current = false;
|
|
58
|
+
setRequest(nextRequest);
|
|
59
|
+
setIsActive(true);
|
|
60
|
+
setHasPermission(Platform.OS !== 'android');
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!request) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
return () => {
|
|
68
|
+
if (didHandleScanRef.current) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
didHandleScanRef.current = true;
|
|
72
|
+
scanCodeChannel.settle(createFailureResult('USER_CANCELLED', 'scanCode:fail cancel'));
|
|
73
|
+
};
|
|
74
|
+
}, [request]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (Platform.OS !== 'android' || !request) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
const requestAndroidPermission = async () => {
|
|
81
|
+
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
|
|
82
|
+
if (cancelled || didHandleScanRef.current) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
|
|
86
|
+
setHasPermission(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
didHandleScanRef.current = true;
|
|
90
|
+
closeWithResult(createFailureResult('PERMISSION_DENIED', 'scanCode:fail permission denied'));
|
|
91
|
+
};
|
|
92
|
+
requestAndroidPermission().catch(() => {
|
|
93
|
+
if (cancelled || didHandleScanRef.current) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
didHandleScanRef.current = true;
|
|
97
|
+
closeWithResult(createFailureResult('SCAN_FAILED', 'scanCode:fail unable to request camera permission'));
|
|
98
|
+
});
|
|
99
|
+
return () => {
|
|
100
|
+
cancelled = true;
|
|
101
|
+
};
|
|
102
|
+
}, [request]);
|
|
103
|
+
if (!request) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (Platform.OS === 'android' && !hasPermission) {
|
|
107
|
+
return <View style={styles.container}/>;
|
|
108
|
+
}
|
|
109
|
+
if (!device) {
|
|
110
|
+
return <View style={styles.container}/>;
|
|
111
|
+
}
|
|
112
|
+
const safePaddingTop = Platform.OS === 'ios' ? 54 : 20;
|
|
113
|
+
return (<View style={styles.container}>
|
|
114
|
+
<StatusBar translucent backgroundColor="transparent" barStyle="light-content"/>
|
|
115
|
+
<Camera codeScanner={codeScanner} device={device} isActive={isActive} style={StyleSheet.absoluteFill}/>
|
|
116
|
+
|
|
117
|
+
<TouchableOpacity style={[styles.closeIcon, { paddingTop: safePaddingTop }]} onPress={() => {
|
|
118
|
+
if (didHandleScanRef.current) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
didHandleScanRef.current = true;
|
|
122
|
+
closeWithResult(createFailureResult('USER_CANCELLED', 'scanCode:fail cancel'));
|
|
123
|
+
}}>
|
|
124
|
+
<Image source={require('./assets/icon_close.png')} style={styles.closeImg}/>
|
|
125
|
+
</TouchableOpacity>
|
|
126
|
+
|
|
127
|
+
{!request.onlyFromCamera ? (<TouchableOpacity style={styles.albumIcon} onPress={() => {
|
|
128
|
+
if (didHandleScanRef.current) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
didHandleScanRef.current = true;
|
|
132
|
+
closeWithResult(createFailureResult('UNSUPPORTED_SOURCE', 'scanCode:fail album selection not implemented'));
|
|
133
|
+
}}>
|
|
134
|
+
<Image source={require('./assets/icon_pic.png')} style={styles.albumImg}/>
|
|
135
|
+
</TouchableOpacity>) : null}
|
|
136
|
+
</View>);
|
|
137
|
+
}
|
|
138
|
+
const styles = StyleSheet.create({
|
|
139
|
+
container: {
|
|
140
|
+
position: 'absolute',
|
|
141
|
+
top: 0,
|
|
142
|
+
left: 0,
|
|
143
|
+
width,
|
|
144
|
+
height,
|
|
145
|
+
backgroundColor: '#000',
|
|
146
|
+
},
|
|
147
|
+
closeIcon: {
|
|
148
|
+
position: 'absolute',
|
|
149
|
+
left: 20,
|
|
150
|
+
top: 0,
|
|
151
|
+
zIndex: 1010,
|
|
152
|
+
},
|
|
153
|
+
closeImg: {
|
|
154
|
+
width: 26,
|
|
155
|
+
height: 26,
|
|
156
|
+
tintColor: '#FFF',
|
|
157
|
+
},
|
|
158
|
+
albumIcon: {
|
|
159
|
+
position: 'absolute',
|
|
160
|
+
right: 20,
|
|
161
|
+
bottom: 40,
|
|
162
|
+
zIndex: 1010,
|
|
163
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
164
|
+
padding: 12,
|
|
165
|
+
borderRadius: 30,
|
|
166
|
+
},
|
|
167
|
+
albumImg: {
|
|
168
|
+
width: 24,
|
|
169
|
+
height: 24,
|
|
170
|
+
tintColor: '#FFF',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHostModalApiFeature } from '@nebula-rn/sdk';
|
|
2
|
+
import NebulaHostPreviewImageModal from './NebulaHostPreviewImageModal';
|
|
3
|
+
import { previewImageChannel } from './previewImageRuntime';
|
|
4
|
+
export const previewImageHostApi = createHostModalApiFeature({
|
|
5
|
+
apiName: 'previewImage',
|
|
6
|
+
description: {
|
|
7
|
+
summary: 'Open a host-managed fullscreen image preview modal for one or more image URLs.',
|
|
8
|
+
description: 'Supports selecting the initial image and optionally showing save actions in the preview UI.',
|
|
9
|
+
tags: ['media', 'image', 'modal'],
|
|
10
|
+
},
|
|
11
|
+
component: NebulaHostPreviewImageModal,
|
|
12
|
+
channel: previewImageChannel,
|
|
13
|
+
createRequest: payload => ({
|
|
14
|
+
urls: payload.urls,
|
|
15
|
+
current: payload.current,
|
|
16
|
+
showMenu: payload.showMenu,
|
|
17
|
+
saveMediaText: payload.saveMediaText,
|
|
18
|
+
cancelText: payload.cancelText,
|
|
19
|
+
}),
|
|
20
|
+
onUnmountErrorMessage: 'previewImage:fail host preview was unmounted before completion',
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const scanCodeHostApi: import("@nebula-rn/sdk").NebulaHostFeature;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Camera } from 'react-native-vision-camera';
|
|
2
|
+
import { createHostApiFailure, createHostModalApiFeature, } from '@nebula-rn/sdk';
|
|
3
|
+
import NebulaHostScanModal from './NebulaHostScanModal';
|
|
4
|
+
import { scanCodeChannel } from './scanCodeRuntime';
|
|
5
|
+
import { Platform } from 'react-native';
|
|
6
|
+
function normalizeScanTypes(scanTypes) {
|
|
7
|
+
const defaultTypes = ['qr', 'ean-13', 'code-128'];
|
|
8
|
+
if (!scanTypes || scanTypes.length === 0) {
|
|
9
|
+
return defaultTypes;
|
|
10
|
+
}
|
|
11
|
+
const normalized = scanTypes.filter(Boolean);
|
|
12
|
+
return normalized.length > 0 ? normalized : defaultTypes;
|
|
13
|
+
}
|
|
14
|
+
export const scanCodeHostApi = createHostModalApiFeature({
|
|
15
|
+
apiName: 'scanCode',
|
|
16
|
+
description: {
|
|
17
|
+
summary: 'Open a host-managed scanner modal and return the first detected barcode or QR code.',
|
|
18
|
+
description: 'Supports camera-based scanning and returns structured scan metadata when successful.',
|
|
19
|
+
tags: ['camera', 'scanner', 'modal'],
|
|
20
|
+
},
|
|
21
|
+
component: NebulaHostScanModal,
|
|
22
|
+
channel: scanCodeChannel,
|
|
23
|
+
createRequest: payload => ({
|
|
24
|
+
onlyFromCamera: payload.onlyFromCamera,
|
|
25
|
+
scanTypes: normalizeScanTypes(payload.scanType),
|
|
26
|
+
}),
|
|
27
|
+
onBeforeOpen: async () => {
|
|
28
|
+
if (Platform.OS === 'android') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const permission = await Camera.requestCameraPermission();
|
|
32
|
+
if (permission !== 'granted') {
|
|
33
|
+
return createHostApiFailure('PERMISSION_DENIED', 'scanCode:fail permission denied');
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
onUnmountErrorMessage: 'scanCode:fail host scanner was unmounted before completion',
|
|
38
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type SaveMediaPayload = {
|
|
2
|
+
url: string;
|
|
3
|
+
type: 'photo' | 'video';
|
|
4
|
+
album?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const saveMediaHost: (payload: SaveMediaPayload) => Promise<import("@react-native-camera-roll/camera-roll").PhotoIdentifier>;
|
|
7
|
+
export declare const coreHostApis: import("@nebula-rn/sdk").NebulaHostFeature[];
|
|
8
|
+
export {};
|