@unif/react-native-hms-scan 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/LICENSE +21 -0
- package/README.md +149 -0
- package/ReactNativeHmsScan.podspec +30 -0
- package/android/build.gradle +79 -0
- package/android/consumer-rules.pro +13 -0
- package/android/src/main/AndroidManifest.xml +23 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +192 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +145 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +257 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +66 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/ReactNativeHmsScanPackage.kt +49 -0
- package/ios/HmsScanModule.h +20 -0
- package/ios/HmsScanModule.mm +161 -0
- package/ios/HmsScanResultMapper.h +45 -0
- package/ios/HmsScanResultMapper.mm +297 -0
- package/ios/HmsScanView.h +24 -0
- package/ios/HmsScanView.mm +322 -0
- package/lib/module/HmsScanView.js +43 -0
- package/lib/module/HmsScanView.js.map +1 -0
- package/lib/module/HmsScanViewNativeComponent.ts +44 -0
- package/lib/module/NativeHmsScan.js +11 -0
- package/lib/module/NativeHmsScan.js.map +1 -0
- package/lib/module/Scanner/DeniedOverlay.js +60 -0
- package/lib/module/Scanner/DeniedOverlay.js.map +1 -0
- package/lib/module/Scanner/ResultFail.js +79 -0
- package/lib/module/Scanner/ResultFail.js.map +1 -0
- package/lib/module/Scanner/ResultFocus.js +174 -0
- package/lib/module/Scanner/ResultFocus.js.map +1 -0
- package/lib/module/Scanner/ScanHint.js +44 -0
- package/lib/module/Scanner/ScanHint.js.map +1 -0
- package/lib/module/Scanner/ScanToolbar.js +103 -0
- package/lib/module/Scanner/ScanToolbar.js.map +1 -0
- package/lib/module/Scanner/ScanTopBar.js +87 -0
- package/lib/module/Scanner/ScanTopBar.js.map +1 -0
- package/lib/module/Scanner/Scanner.js +184 -0
- package/lib/module/Scanner/Scanner.js.map +1 -0
- package/lib/module/Scanner/Viewfinder.js +159 -0
- package/lib/module/Scanner/Viewfinder.js.map +1 -0
- package/lib/module/Scanner/scanChrome.js +23 -0
- package/lib/module/Scanner/scanChrome.js.map +1 -0
- package/lib/module/decodeImage.js +32 -0
- package/lib/module/decodeImage.js.map +1 -0
- package/lib/module/format.js +66 -0
- package/lib/module/format.js.map +1 -0
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/mock.js +30 -0
- package/lib/module/mock.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.js +28 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/types.js +46 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/jest.setup.d.ts +1 -0
- package/lib/typescript/jest.setup.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/HmsScanView.d.ts +36 -0
- package/lib/typescript/src/HmsScanView.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +32 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/NativeHmsScan.d.ts +17 -0
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts +7 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFail.d.ts +7 -0
- package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus.d.ts +12 -0
- package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts +8 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts +9 -0
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts +10 -0
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts +34 -0
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/Viewfinder.d.ts +11 -0
- package/lib/typescript/src/Scanner/Viewfinder.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/scanChrome.d.ts +19 -0
- package/lib/typescript/src/Scanner/scanChrome.d.ts.map +1 -0
- package/lib/typescript/src/decodeImage.d.ts +12 -0
- package/lib/typescript/src/decodeImage.d.ts.map +1 -0
- package/lib/typescript/src/format.d.ts +13 -0
- package/lib/typescript/src/format.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/mock.d.ts +12 -0
- package/lib/typescript/src/mock.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +9 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +62 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +174 -0
- package/src/HmsScanView.tsx +87 -0
- package/src/HmsScanViewNativeComponent.ts +44 -0
- package/src/NativeHmsScan.ts +24 -0
- package/src/Scanner/DeniedOverlay.tsx +59 -0
- package/src/Scanner/ResultFail.tsx +71 -0
- package/src/Scanner/ResultFocus.tsx +118 -0
- package/src/Scanner/ScanHint.tsx +38 -0
- package/src/Scanner/ScanToolbar.tsx +93 -0
- package/src/Scanner/ScanTopBar.tsx +74 -0
- package/src/Scanner/Scanner.tsx +238 -0
- package/src/Scanner/Viewfinder.tsx +115 -0
- package/src/Scanner/scanChrome.ts +20 -0
- package/src/decodeImage.ts +36 -0
- package/src/format.ts +93 -0
- package/src/index.tsx +35 -0
- package/src/mock.ts +43 -0
- package/src/permissions.ts +27 -0
- package/src/types.ts +138 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HmsScanResultMapper.mm
|
|
3
|
+
// @unif/react-native-hms-scan
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
#import "HmsScanResultMapper.h"
|
|
7
|
+
|
|
8
|
+
#import <ScanKitFrameWork/ScanKitFrameWork.h>
|
|
9
|
+
|
|
10
|
+
// NOTE on naming collisions: HMSScanFormatTypeCode (in HmsScanFormat.h) declares
|
|
11
|
+
// *global, unscoped* enum constants named QR_CODE, EAN_13, DATA_MATRIX, etc.
|
|
12
|
+
// Those identifiers therefore refer to integers, NOT to our output strings.
|
|
13
|
+
// We only ever compare against those enum constants and emit string *literals*
|
|
14
|
+
// (@"QR_CODE" ...), so there is no clash.
|
|
15
|
+
|
|
16
|
+
@implementation HmsScanResultMapper
|
|
17
|
+
|
|
18
|
+
#pragma mark - CSV -> scanFormatType bitmask
|
|
19
|
+
|
|
20
|
+
// Maps one of our BarcodeFormat tokens to the matching HMSScanFormatTypeCode bit.
|
|
21
|
+
// Returns 0 for tokens HUAWEI iOS does not support (e.g. MULTI_FUNCTIONAL) or
|
|
22
|
+
// UNKNOWN, so they contribute nothing to the OR-ed mask.
|
|
23
|
+
+ (unsigned int)bitForFormatToken:(NSString *)token
|
|
24
|
+
{
|
|
25
|
+
NSString *t = [[token stringByTrimmingCharactersInSet:
|
|
26
|
+
[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
|
|
27
|
+
if (t.length == 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static NSDictionary<NSString *, NSNumber *> *map = nil;
|
|
32
|
+
static dispatch_once_t onceToken;
|
|
33
|
+
dispatch_once(&onceToken, ^{
|
|
34
|
+
map = @{
|
|
35
|
+
@"QR_CODE": @(QR_CODE),
|
|
36
|
+
@"AZTEC": @(AZTEC),
|
|
37
|
+
@"DATA_MATRIX": @(DATA_MATRIX),
|
|
38
|
+
@"PDF417": @(PDF_417),
|
|
39
|
+
@"CODABAR": @(CODABAR),
|
|
40
|
+
@"CODE_39": @(CODE_39),
|
|
41
|
+
@"CODE_93": @(CODE_93),
|
|
42
|
+
@"CODE_128": @(CODE_128),
|
|
43
|
+
@"EAN_8": @(EAN_8),
|
|
44
|
+
@"EAN_13": @(EAN_13),
|
|
45
|
+
@"UPC_A": @(UPC_A),
|
|
46
|
+
@"UPC_E": @(UPC_E),
|
|
47
|
+
// Our ITF14 maps to HUAWEI's ITF bit.
|
|
48
|
+
@"ITF14": @(ITF),
|
|
49
|
+
// MULTI_FUNCTIONAL / UNKNOWN have no HUAWEI iOS equivalent -> 0.
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
NSNumber *bit = map[t];
|
|
54
|
+
return bit ? (unsigned int)bit.unsignedIntValue : 0u;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
+ (unsigned int)scanFormatTypeFromCsv:(NSString *)csv
|
|
58
|
+
{
|
|
59
|
+
NSString *trimmed = [csv stringByTrimmingCharactersInSet:
|
|
60
|
+
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
61
|
+
if (trimmed.length == 0) {
|
|
62
|
+
return (unsigned int)ALL; // empty string == all formats
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
unsigned int mask = 0u;
|
|
66
|
+
for (NSString *token in [trimmed componentsSeparatedByString:@","]) {
|
|
67
|
+
mask |= [self bitForFormatToken:token];
|
|
68
|
+
}
|
|
69
|
+
// If nothing resolved (e.g. only MULTI_FUNCTIONAL/UNKNOWN was requested),
|
|
70
|
+
// fall back to ALL so we still attempt a decode rather than scanning nothing.
|
|
71
|
+
return mask == 0u ? (unsigned int)ALL : mask;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#pragma mark - formatValue -> our BarcodeFormat string
|
|
75
|
+
|
|
76
|
+
// HUAWEI's `formatValue` is documented as the HMSScanFormatTypeCode integer.
|
|
77
|
+
// We tolerate it being delivered as an NSNumber (bit value) OR an NSString
|
|
78
|
+
// (either a numeric string or already a token like "QR_CODE"), since the
|
|
79
|
+
// public headers do not pin the type and demos have shown both shapes.
|
|
80
|
+
+ (NSString *)formatStringFromValue:(id)formatValue
|
|
81
|
+
{
|
|
82
|
+
// Case 1: numeric (NSNumber, or numeric NSString) -> match the enum bit.
|
|
83
|
+
NSInteger code = NSNotFound;
|
|
84
|
+
if ([formatValue isKindOfClass:[NSNumber class]]) {
|
|
85
|
+
code = [(NSNumber *)formatValue integerValue];
|
|
86
|
+
} else if ([formatValue isKindOfClass:[NSString class]]) {
|
|
87
|
+
NSString *s = (NSString *)formatValue;
|
|
88
|
+
NSString *trimmed = [s stringByTrimmingCharactersInSet:
|
|
89
|
+
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
90
|
+
// Pure-integer string?
|
|
91
|
+
NSScanner *scanner = [NSScanner scannerWithString:trimmed];
|
|
92
|
+
NSInteger parsed = 0;
|
|
93
|
+
if ([scanner scanInteger:&parsed] && scanner.isAtEnd) {
|
|
94
|
+
code = parsed;
|
|
95
|
+
} else {
|
|
96
|
+
// Case 2: already a token string -> normalize known spellings.
|
|
97
|
+
NSString *upper = [trimmed uppercaseString];
|
|
98
|
+
if ([upper isEqualToString:@"PDF_417"]) return @"PDF417";
|
|
99
|
+
if ([upper isEqualToString:@"ITF"]) return @"ITF14";
|
|
100
|
+
static NSSet<NSString *> *known = nil;
|
|
101
|
+
static dispatch_once_t onceToken;
|
|
102
|
+
dispatch_once(&onceToken, ^{
|
|
103
|
+
known = [NSSet setWithArray:@[
|
|
104
|
+
@"QR_CODE", @"AZTEC", @"DATA_MATRIX", @"PDF417", @"CODABAR",
|
|
105
|
+
@"CODE_39", @"CODE_93", @"CODE_128", @"EAN_8", @"EAN_13",
|
|
106
|
+
@"UPC_A", @"UPC_E", @"ITF14", @"MULTI_FUNCTIONAL",
|
|
107
|
+
]];
|
|
108
|
+
});
|
|
109
|
+
return [known containsObject:upper] ? upper : @"UNKNOWN";
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
return @"UNKNOWN";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Map enum bit -> our string. Compare against the global enum constants.
|
|
116
|
+
switch (code) {
|
|
117
|
+
case QR_CODE: return @"QR_CODE";
|
|
118
|
+
case AZTEC: return @"AZTEC";
|
|
119
|
+
case DATA_MATRIX: return @"DATA_MATRIX";
|
|
120
|
+
case PDF_417: return @"PDF417";
|
|
121
|
+
case CODABAR: return @"CODABAR";
|
|
122
|
+
case CODE_39: return @"CODE_39";
|
|
123
|
+
case CODE_93: return @"CODE_93";
|
|
124
|
+
case CODE_128: return @"CODE_128";
|
|
125
|
+
case EAN_8: return @"EAN_8";
|
|
126
|
+
case EAN_13: return @"EAN_13";
|
|
127
|
+
case UPC_A: return @"UPC_A";
|
|
128
|
+
case UPC_E: return @"UPC_E";
|
|
129
|
+
case ITF: return @"ITF14";
|
|
130
|
+
default: return @"UNKNOWN";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#pragma mark - sceneType -> our BarcodeContentType (best effort)
|
|
135
|
+
|
|
136
|
+
// HUAWEI iOS does not publish a sceneType enum in the headers; precision is
|
|
137
|
+
// limited. We map a few well-known scene tokens/codes and otherwise return nil
|
|
138
|
+
// so `contentType` is omitted (the JS contract treats unknown as undefined).
|
|
139
|
+
+ (nullable NSString *)contentTypeFromSceneType:(id)sceneType
|
|
140
|
+
{
|
|
141
|
+
NSString *token = nil;
|
|
142
|
+
if ([sceneType isKindOfClass:[NSString class]]) {
|
|
143
|
+
token = [[(NSString *)sceneType stringByTrimmingCharactersInSet:
|
|
144
|
+
[NSCharacterSet whitespaceAndNewlineCharacterSet]] uppercaseString];
|
|
145
|
+
} else if ([sceneType isKindOfClass:[NSNumber class]]) {
|
|
146
|
+
// No documented numeric scene mapping -> leave unknown.
|
|
147
|
+
return nil;
|
|
148
|
+
} else {
|
|
149
|
+
return nil;
|
|
150
|
+
}
|
|
151
|
+
if (token.length == 0) {
|
|
152
|
+
return nil;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static NSDictionary<NSString *, NSString *> *map = nil;
|
|
156
|
+
static dispatch_once_t onceToken;
|
|
157
|
+
dispatch_once(&onceToken, ^{
|
|
158
|
+
map = @{
|
|
159
|
+
@"TEXT": @"TEXT",
|
|
160
|
+
@"URL": @"URL",
|
|
161
|
+
@"URI": @"URL",
|
|
162
|
+
@"EMAIL": @"EMAIL",
|
|
163
|
+
@"PHONE": @"PHONE",
|
|
164
|
+
@"TEL": @"PHONE",
|
|
165
|
+
@"SMS": @"SMS",
|
|
166
|
+
@"WIFI": @"WIFI",
|
|
167
|
+
@"CONTACT": @"CONTACT",
|
|
168
|
+
@"CONTACT_INFO": @"CONTACT",
|
|
169
|
+
@"EVENT": @"EVENT",
|
|
170
|
+
@"LOCATION": @"LOCATION",
|
|
171
|
+
@"GEO": @"LOCATION",
|
|
172
|
+
@"DRIVER": @"DRIVER",
|
|
173
|
+
@"ISBN": @"ISBN",
|
|
174
|
+
@"ARTICLE": @"ARTICLE",
|
|
175
|
+
@"PRODUCT": @"ARTICLE",
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
return map[token]; // nil when unrecognized -> contentType omitted
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#pragma mark - ResultPoint -> cornerPoints
|
|
182
|
+
|
|
183
|
+
+ (nullable NSNumber *)numberFromPointDict:(NSDictionary *)dict keys:(NSArray<NSString *> *)keys
|
|
184
|
+
{
|
|
185
|
+
for (NSString *key in keys) {
|
|
186
|
+
id v = dict[key];
|
|
187
|
+
if ([v isKindOfClass:[NSNumber class]]) {
|
|
188
|
+
return (NSNumber *)v;
|
|
189
|
+
}
|
|
190
|
+
if ([v isKindOfClass:[NSString class]]) {
|
|
191
|
+
return @([(NSString *)v doubleValue]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return nil;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// HUAWEI delivers `ResultPoint` as an array of dicts with `posX`/`posY` numbers.
|
|
198
|
+
// We tolerate a couple of key spellings just in case.
|
|
199
|
+
+ (nullable NSArray<NSDictionary *> *)cornerPointsFromResultPoint:(id)resultPoint
|
|
200
|
+
{
|
|
201
|
+
if (![resultPoint isKindOfClass:[NSArray class]]) {
|
|
202
|
+
return nil;
|
|
203
|
+
}
|
|
204
|
+
NSMutableArray<NSDictionary *> *points = [NSMutableArray array];
|
|
205
|
+
for (id element in (NSArray *)resultPoint) {
|
|
206
|
+
if (![element isKindOfClass:[NSDictionary class]]) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
NSDictionary *p = (NSDictionary *)element;
|
|
210
|
+
NSNumber *x = [self numberFromPointDict:p keys:@[ @"posX", @"x", @"X" ]];
|
|
211
|
+
NSNumber *y = [self numberFromPointDict:p keys:@[ @"posY", @"y", @"Y" ]];
|
|
212
|
+
if (x != nil && y != nil) {
|
|
213
|
+
[points addObject:@{ @"x": x, @"y": y }];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return points.count > 0 ? [points copy] : nil;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#pragma mark - Dict -> ScanResult
|
|
220
|
+
|
|
221
|
+
+ (nullable NSString *)valueFromHuaweiDict:(NSDictionary *)dict
|
|
222
|
+
{
|
|
223
|
+
// Primary key is `text`; tolerate a couple of alternates defensively.
|
|
224
|
+
for (NSString *key in @[ @"text", @"originalValue", @"showText", @"value" ]) {
|
|
225
|
+
id v = dict[key];
|
|
226
|
+
if ([v isKindOfClass:[NSString class]] && ((NSString *)v).length > 0) {
|
|
227
|
+
return (NSString *)v;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return nil;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
+ (nullable NSDictionary *)scanResultFromHuaweiDict:(NSDictionary *)dict
|
|
234
|
+
{
|
|
235
|
+
if (![dict isKindOfClass:[NSDictionary class]]) {
|
|
236
|
+
return nil;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
NSString *value = [self valueFromHuaweiDict:dict];
|
|
240
|
+
if (value == nil) {
|
|
241
|
+
return nil; // no usable decoded text -> drop
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
NSMutableDictionary *out = [NSMutableDictionary dictionary];
|
|
245
|
+
out[@"value"] = value;
|
|
246
|
+
out[@"format"] = [self formatStringFromValue:dict[@"formatValue"]];
|
|
247
|
+
|
|
248
|
+
NSString *contentType = [self contentTypeFromSceneType:dict[@"sceneType"]];
|
|
249
|
+
if (contentType != nil) {
|
|
250
|
+
out[@"contentType"] = contentType;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
NSArray<NSDictionary *> *corners = [self cornerPointsFromResultPoint:dict[@"ResultPoint"]];
|
|
254
|
+
if (corners != nil) {
|
|
255
|
+
out[@"cornerPoints"] = corners;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [out copy];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
+ (NSArray<NSDictionary *> *)scanResultsFromHuaweiArray:(NSArray *)array
|
|
262
|
+
{
|
|
263
|
+
NSMutableArray<NSDictionary *> *results = [NSMutableArray array];
|
|
264
|
+
if ([array isKindOfClass:[NSArray class]]) {
|
|
265
|
+
for (id element in array) {
|
|
266
|
+
NSDictionary *mapped = [self scanResultFromHuaweiDict:element];
|
|
267
|
+
if (mapped != nil) {
|
|
268
|
+
[results addObject:mapped];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [results copy];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#pragma mark - JSON
|
|
276
|
+
|
|
277
|
+
+ (NSString *)jsonStringFromScanResults:(NSArray<NSDictionary *> *)results
|
|
278
|
+
{
|
|
279
|
+
NSArray *safe = [results isKindOfClass:[NSArray class]] ? results : @[];
|
|
280
|
+
if (![NSJSONSerialization isValidJSONObject:safe]) {
|
|
281
|
+
return @"[]";
|
|
282
|
+
}
|
|
283
|
+
NSError *error = nil;
|
|
284
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject:safe options:0 error:&error];
|
|
285
|
+
if (data == nil || error != nil) {
|
|
286
|
+
return @"[]";
|
|
287
|
+
}
|
|
288
|
+
NSString *json = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
289
|
+
return json ?: @"[]";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
+ (NSString *)jsonStringFromHuaweiArray:(NSArray *)array
|
|
293
|
+
{
|
|
294
|
+
return [self jsonStringFromScanResults:[self scanResultsFromHuaweiArray:array]];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HmsScanView.h
|
|
3
|
+
// @unif/react-native-hms-scan
|
|
4
|
+
//
|
|
5
|
+
// Fabric component registered under the name "HmsScanView". Hosts HUAWEI Scan
|
|
6
|
+
// Kit's HmsCustomScanViewController as an embedded child view controller and
|
|
7
|
+
// bridges its delegate callbacks to the codegen events
|
|
8
|
+
// (onScanResult / onScanError / onTorchStatus).
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
#import <React/RCTViewComponentView.h>
|
|
12
|
+
#import <UIKit/UIKit.h>
|
|
13
|
+
|
|
14
|
+
#ifndef HmsScanViewNativeComponent_h
|
|
15
|
+
#define HmsScanViewNativeComponent_h
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
18
|
+
|
|
19
|
+
@interface HmsScanView : RCTViewComponentView
|
|
20
|
+
@end
|
|
21
|
+
|
|
22
|
+
NS_ASSUME_NONNULL_END
|
|
23
|
+
|
|
24
|
+
#endif /* HmsScanViewNativeComponent_h */
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HmsScanView.mm
|
|
3
|
+
// @unif/react-native-hms-scan
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
#import "HmsScanView.h"
|
|
7
|
+
#import "HmsScanResultMapper.h"
|
|
8
|
+
|
|
9
|
+
#import <React/RCTConversions.h>
|
|
10
|
+
|
|
11
|
+
#import <react/renderer/components/ReactNativeHmsScanSpec/ComponentDescriptors.h>
|
|
12
|
+
#import <react/renderer/components/ReactNativeHmsScanSpec/EventEmitters.h>
|
|
13
|
+
#import <react/renderer/components/ReactNativeHmsScanSpec/Props.h>
|
|
14
|
+
#import <react/renderer/components/ReactNativeHmsScanSpec/RCTComponentViewHelpers.h>
|
|
15
|
+
|
|
16
|
+
#import "RCTFabricComponentsPlugins.h"
|
|
17
|
+
|
|
18
|
+
#import <AVFoundation/AVFoundation.h>
|
|
19
|
+
#import <ScanKitFrameWork/ScanKitFrameWork.h>
|
|
20
|
+
|
|
21
|
+
using namespace facebook::react;
|
|
22
|
+
|
|
23
|
+
@interface HmsScanView () <CustomizedScanDelegate>
|
|
24
|
+
@end
|
|
25
|
+
|
|
26
|
+
@implementation HmsScanView {
|
|
27
|
+
HmsCustomScanViewController *_scanVC;
|
|
28
|
+
// Mirror of the props we apply imperatively, so updateProps can diff against
|
|
29
|
+
// the *actually applied* state (RCTViewComponentView resets _props each call).
|
|
30
|
+
NSString *_appliedFormatsCsv;
|
|
31
|
+
BOOL _appliedContinuous;
|
|
32
|
+
BOOL _appliedPaused;
|
|
33
|
+
BOOL _appliedTorch;
|
|
34
|
+
BOOL _hasAppliedOnce;
|
|
35
|
+
BOOL _childAttached;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#pragma mark - Codegen wiring
|
|
39
|
+
|
|
40
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
41
|
+
{
|
|
42
|
+
return concreteComponentDescriptorProvider<HmsScanViewComponentDescriptor>();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
46
|
+
{
|
|
47
|
+
if (self = [super initWithFrame:frame]) {
|
|
48
|
+
static const auto defaultProps = std::make_shared<const HmsScanViewProps>();
|
|
49
|
+
_props = defaultProps;
|
|
50
|
+
|
|
51
|
+
_appliedContinuous = YES; // matches WithDefault<boolean, true>
|
|
52
|
+
_appliedPaused = NO;
|
|
53
|
+
_appliedTorch = NO;
|
|
54
|
+
_appliedFormatsCsv = @""; // matches WithDefault<string, ''> (= all formats)
|
|
55
|
+
_hasAppliedOnce = NO;
|
|
56
|
+
_childAttached = NO;
|
|
57
|
+
|
|
58
|
+
[self buildScanViewControllerWithCsv:_appliedFormatsCsv];
|
|
59
|
+
}
|
|
60
|
+
return self;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#pragma mark - Scan view controller lifecycle
|
|
64
|
+
|
|
65
|
+
- (void)buildScanViewControllerWithCsv:(NSString *)csv
|
|
66
|
+
{
|
|
67
|
+
// (Re)create the VC. HmsCustomScanViewController takes its format set at init
|
|
68
|
+
// via HmsScanOptions and exposes no setter to change it later, so a formatsCsv
|
|
69
|
+
// change requires rebuilding the controller.
|
|
70
|
+
[self teardownScanViewController];
|
|
71
|
+
|
|
72
|
+
unsigned int formatType = [HmsScanResultMapper scanFormatTypeFromCsv:csv];
|
|
73
|
+
HmsScanOptions *options =
|
|
74
|
+
[[HmsScanOptions alloc] initWithScanFormatType:formatType Photo:NO];
|
|
75
|
+
|
|
76
|
+
HmsCustomScanViewController *vc =
|
|
77
|
+
[[HmsCustomScanViewController alloc] initCustomizedScanWithFormatType:options];
|
|
78
|
+
vc.backButtonHidden = YES; // we render our own chrome from JS
|
|
79
|
+
vc.customizedScanDelegate = self;
|
|
80
|
+
vc.continuouslyScan = _appliedContinuous;
|
|
81
|
+
|
|
82
|
+
_scanVC = vc;
|
|
83
|
+
_appliedFormatsCsv = csv ?: @"";
|
|
84
|
+
|
|
85
|
+
// Embed the VC's view as our content view, filling self.
|
|
86
|
+
UIView *scanView = vc.view;
|
|
87
|
+
scanView.frame = self.bounds;
|
|
88
|
+
scanView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
|
89
|
+
self.contentView = scanView;
|
|
90
|
+
|
|
91
|
+
// If we're already on screen, (re)attach to the nearest view controller.
|
|
92
|
+
if (self.window != nil) {
|
|
93
|
+
[self attachChildViewController];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Re-apply paused state after a rebuild (continuouslyScan was set above).
|
|
97
|
+
if (_appliedPaused) {
|
|
98
|
+
[_scanVC pauseContinuouslyScan];
|
|
99
|
+
} else {
|
|
100
|
+
[_scanVC resumeContinuouslyScan];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)teardownScanViewController
|
|
105
|
+
{
|
|
106
|
+
if (_scanVC == nil) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
[self detachChildViewController];
|
|
110
|
+
_scanVC.customizedScanDelegate = nil;
|
|
111
|
+
if (self.contentView == _scanVC.view) {
|
|
112
|
+
self.contentView = nil;
|
|
113
|
+
}
|
|
114
|
+
_scanVC = nil;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
- (void)attachChildViewController
|
|
118
|
+
{
|
|
119
|
+
if (_scanVC == nil || _childAttached) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
UIViewController *parent = [self reactViewController];
|
|
123
|
+
if (parent == nil) {
|
|
124
|
+
return; // try again on next didMoveToWindow
|
|
125
|
+
}
|
|
126
|
+
[parent addChildViewController:_scanVC];
|
|
127
|
+
// The view is already in our hierarchy (set as contentView); just notify.
|
|
128
|
+
[_scanVC didMoveToParentViewController:parent];
|
|
129
|
+
_childAttached = YES;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
- (void)detachChildViewController
|
|
133
|
+
{
|
|
134
|
+
if (_scanVC == nil || !_childAttached) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
[_scanVC willMoveToParentViewController:nil];
|
|
138
|
+
[_scanVC removeFromParentViewController];
|
|
139
|
+
_childAttached = NO;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
- (void)didMoveToWindow
|
|
143
|
+
{
|
|
144
|
+
[super didMoveToWindow];
|
|
145
|
+
if (self.window != nil) {
|
|
146
|
+
[self attachChildViewController];
|
|
147
|
+
} else {
|
|
148
|
+
[self detachChildViewController];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#pragma mark - Props
|
|
153
|
+
|
|
154
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
155
|
+
{
|
|
156
|
+
const auto &newViewProps = *std::static_pointer_cast<HmsScanViewProps const>(props);
|
|
157
|
+
|
|
158
|
+
NSString *newCsv = [NSString stringWithUTF8String:newViewProps.formatsCsv.c_str()];
|
|
159
|
+
if (newCsv == nil) {
|
|
160
|
+
newCsv = @"";
|
|
161
|
+
}
|
|
162
|
+
BOOL newContinuous = newViewProps.continuous;
|
|
163
|
+
BOOL newPaused = newViewProps.paused;
|
|
164
|
+
BOOL newTorch = newViewProps.torch;
|
|
165
|
+
|
|
166
|
+
BOOL first = !_hasAppliedOnce;
|
|
167
|
+
|
|
168
|
+
// formatsCsv change -> rebuild the controller (carry over continuous/paused).
|
|
169
|
+
if (first || ![newCsv isEqualToString:_appliedFormatsCsv]) {
|
|
170
|
+
_appliedContinuous = newContinuous;
|
|
171
|
+
_appliedPaused = newPaused;
|
|
172
|
+
[self buildScanViewControllerWithCsv:newCsv];
|
|
173
|
+
} else {
|
|
174
|
+
// continuous change -> toggle continuouslyScan in place.
|
|
175
|
+
if (newContinuous != _appliedContinuous) {
|
|
176
|
+
_appliedContinuous = newContinuous;
|
|
177
|
+
_scanVC.continuouslyScan = newContinuous;
|
|
178
|
+
}
|
|
179
|
+
// paused change -> pause/resume in place.
|
|
180
|
+
if (newPaused != _appliedPaused) {
|
|
181
|
+
_appliedPaused = newPaused;
|
|
182
|
+
if (newPaused) {
|
|
183
|
+
[_scanVC pauseContinuouslyScan];
|
|
184
|
+
} else {
|
|
185
|
+
[_scanVC resumeContinuouslyScan];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// torch change (best-effort, see applyTorch:).
|
|
191
|
+
if (first || newTorch != _appliedTorch) {
|
|
192
|
+
_appliedTorch = newTorch;
|
|
193
|
+
[self applyTorch:newTorch];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
_hasAppliedOnce = YES;
|
|
197
|
+
|
|
198
|
+
[super updateProps:props oldProps:oldProps];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#pragma mark - Torch (best-effort)
|
|
202
|
+
|
|
203
|
+
// HUAWEI Scan Kit exposes NO public torch API on iOS — HmsCustomScanViewController
|
|
204
|
+
// renders its own torch button and auto-detects low light. We therefore drive the
|
|
205
|
+
// torch directly through AVFoundation as a best-effort convenience. This is NOT
|
|
206
|
+
// guaranteed to stay in sync with HUAWEI's built-in torch button, and may be a
|
|
207
|
+
// no-op if HUAWEI holds an exclusive lock on the capture device's configuration.
|
|
208
|
+
- (void)applyTorch:(BOOL)on
|
|
209
|
+
{
|
|
210
|
+
BOOL available = NO;
|
|
211
|
+
BOOL torchOn = [self setTorchHardwareOn:on available:&available];
|
|
212
|
+
[self emitTorchStatusAvailable:available on:torchOn];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Mutates the capture device's torch. Returns the resulting on-state and writes
|
|
216
|
+
// hardware availability into `available`. Does NOT emit any event (so it can be
|
|
217
|
+
// reused on teardown/recycle without firing onTorchStatus on a stale emitter).
|
|
218
|
+
- (BOOL)setTorchHardwareOn:(BOOL)on available:(BOOL *)available
|
|
219
|
+
{
|
|
220
|
+
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
|
221
|
+
BOOL isAvailable = (device != nil && device.hasTorch && device.isTorchAvailable);
|
|
222
|
+
if (available != NULL) {
|
|
223
|
+
*available = isAvailable;
|
|
224
|
+
}
|
|
225
|
+
if (!isAvailable) {
|
|
226
|
+
return NO;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
BOOL torchOn = NO;
|
|
230
|
+
NSError *error = nil;
|
|
231
|
+
if ([device lockForConfiguration:&error]) {
|
|
232
|
+
if (on && [device isTorchModeSupported:AVCaptureTorchModeOn]) {
|
|
233
|
+
device.torchMode = AVCaptureTorchModeOn;
|
|
234
|
+
torchOn = YES;
|
|
235
|
+
} else {
|
|
236
|
+
device.torchMode = AVCaptureTorchModeOff;
|
|
237
|
+
torchOn = NO;
|
|
238
|
+
}
|
|
239
|
+
[device unlockForConfiguration];
|
|
240
|
+
}
|
|
241
|
+
// On lock failure we silently ignore (best-effort) and report torchOn = NO.
|
|
242
|
+
return torchOn;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#pragma mark - CustomizedScanDelegate
|
|
246
|
+
|
|
247
|
+
// Called by HUAWEI on every decode (repeatedly in continuous mode). `resultDic`
|
|
248
|
+
// is a single result dictionary; we wrap it into the contract's top-level array.
|
|
249
|
+
- (void)customizedScanDelegateForResult:(NSDictionary *)resultDic
|
|
250
|
+
{
|
|
251
|
+
NSDictionary *mapped = [HmsScanResultMapper scanResultFromHuaweiDict:resultDic];
|
|
252
|
+
if (mapped == nil) {
|
|
253
|
+
// Decoded payload had no usable value; surface as a soft error event.
|
|
254
|
+
[self emitScanErrorWithCode:@"E_NO_RESULT" message:@"扫码结果为空或无法解析"];
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
NSString *json = [HmsScanResultMapper jsonStringFromScanResults:@[ mapped ]];
|
|
258
|
+
[self emitScanResultJson:json];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#pragma mark - Event emitters
|
|
262
|
+
|
|
263
|
+
- (void)emitScanResultJson:(NSString *)json
|
|
264
|
+
{
|
|
265
|
+
if (!_eventEmitter) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
std::string resultsJson = json ? std::string([json UTF8String]) : std::string("[]");
|
|
269
|
+
std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
|
|
270
|
+
->onScanResult(HmsScanViewEventEmitter::OnScanResult{
|
|
271
|
+
.resultsJson = resultsJson,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
- (void)emitScanErrorWithCode:(NSString *)code message:(NSString *)message
|
|
276
|
+
{
|
|
277
|
+
if (!_eventEmitter) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
|
|
281
|
+
->onScanError(HmsScanViewEventEmitter::OnScanError{
|
|
282
|
+
.code = code ? std::string([code UTF8String]) : std::string(""),
|
|
283
|
+
.message = message ? std::string([message UTF8String]) : std::string(""),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
- (void)emitTorchStatusAvailable:(BOOL)available on:(BOOL)on
|
|
288
|
+
{
|
|
289
|
+
if (!_eventEmitter) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
std::static_pointer_cast<const HmsScanViewEventEmitter>(_eventEmitter)
|
|
293
|
+
->onTorchStatus(HmsScanViewEventEmitter::OnTorchStatus{
|
|
294
|
+
.available = available ? true : false,
|
|
295
|
+
.on = on ? true : false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#pragma mark - Cleanup
|
|
300
|
+
|
|
301
|
+
- (void)prepareForRecycle
|
|
302
|
+
{
|
|
303
|
+
// Reset imperative state so a recycled view re-applies props cleanly.
|
|
304
|
+
// Turn the torch off WITHOUT emitting (the emitter is being torn down).
|
|
305
|
+
[self setTorchHardwareOn:NO available:NULL];
|
|
306
|
+
[self teardownScanViewController];
|
|
307
|
+
_hasAppliedOnce = NO;
|
|
308
|
+
_appliedFormatsCsv = @"";
|
|
309
|
+
_appliedContinuous = YES;
|
|
310
|
+
_appliedPaused = NO;
|
|
311
|
+
_appliedTorch = NO;
|
|
312
|
+
[super prepareForRecycle];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
- (void)dealloc
|
|
316
|
+
{
|
|
317
|
+
if (_scanVC != nil) {
|
|
318
|
+
_scanVC.customizedScanDelegate = nil;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import NativeHmsScanView from './HmsScanViewNativeComponent';
|
|
5
|
+
import { formatsToCsv, parseResultsJson } from "./format.js";
|
|
6
|
+
|
|
7
|
+
/** 暗光提示 / 手电状态。 */
|
|
8
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
/**
|
|
10
|
+
* 底层 headless 扫码相机组件:只渲染相机预览并发出扫码结果事件。
|
|
11
|
+
* 取景框 / 扫描线 / 手电按钮等 UI 由上层(如 `<Scanner>`)用普通 RN 视图叠加绘制。
|
|
12
|
+
*
|
|
13
|
+
* Android = 华为 RemoteView,iOS = HmsCustomScanViewController(Scan SDK-Plus / ScanKitFrameWork)。
|
|
14
|
+
*/
|
|
15
|
+
export const HmsScanView = /*#__PURE__*/forwardRef(function HmsScanView({
|
|
16
|
+
formats,
|
|
17
|
+
continuous = true,
|
|
18
|
+
paused = false,
|
|
19
|
+
torch = false,
|
|
20
|
+
onScanResult,
|
|
21
|
+
onScanError,
|
|
22
|
+
onTorchStatus,
|
|
23
|
+
...viewProps
|
|
24
|
+
}, ref) {
|
|
25
|
+
return /*#__PURE__*/_jsx(NativeHmsScanView, {
|
|
26
|
+
...viewProps,
|
|
27
|
+
ref: ref,
|
|
28
|
+
formatsCsv: formatsToCsv(formats),
|
|
29
|
+
continuous: continuous,
|
|
30
|
+
paused: paused,
|
|
31
|
+
torch: torch,
|
|
32
|
+
onScanResult: onScanResult ? e => onScanResult(parseResultsJson(e.nativeEvent.resultsJson)) : undefined,
|
|
33
|
+
onScanError: onScanError ? e => onScanError({
|
|
34
|
+
code: e.nativeEvent.code,
|
|
35
|
+
message: e.nativeEvent.message
|
|
36
|
+
}) : undefined,
|
|
37
|
+
onTorchStatus: onTorchStatus ? e => onTorchStatus({
|
|
38
|
+
available: e.nativeEvent.available,
|
|
39
|
+
on: e.nativeEvent.on
|
|
40
|
+
}) : undefined
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=HmsScanView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["forwardRef","NativeHmsScanView","formatsToCsv","parseResultsJson","jsx","_jsx","HmsScanView","formats","continuous","paused","torch","onScanResult","onScanError","onTorchStatus","viewProps","ref","formatsCsv","e","nativeEvent","resultsJson","undefined","code","message","available","on"],"sourceRoot":"../../src","sources":["HmsScanView.tsx"],"mappings":";;AAAA,SAASA,UAAU,QAA2B,OAAO;AAErD,OAAOC,iBAAiB,MAAM,8BAA8B;AAC5D,SAASC,YAAY,EAAEC,gBAAgB,QAAQ,aAAU;;AAGzD;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAyBA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,WAAW,gBAAGN,UAAU,CAGnC,SAASM,WAAWA,CACpB;EACEC,OAAO;EACPC,UAAU,GAAG,IAAI;EACjBC,MAAM,GAAG,KAAK;EACdC,KAAK,GAAG,KAAK;EACbC,YAAY;EACZC,WAAW;EACXC,aAAa;EACb,GAAGC;AACL,CAAC,EACDC,GAAG,EACH;EACA,oBACEV,IAAA,CAACJ,iBAAiB;IAAA,GACZa,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTC,UAAU,EAAEd,YAAY,CAACK,OAAO,CAAE;IAClCC,UAAU,EAAEA,UAAW;IACvBC,MAAM,EAAEA,MAAO;IACfC,KAAK,EAAEA,KAAM;IACbC,YAAY,EACVA,YAAY,GACPM,CAAC,IAAKN,YAAY,CAACR,gBAAgB,CAACc,CAAC,CAACC,WAAW,CAACC,WAAW,CAAC,CAAC,GAChEC,SACL;IACDR,WAAW,EACTA,WAAW,GACNK,CAAC,IACAL,WAAW,CAAC;MACVS,IAAI,EAAEJ,CAAC,CAACC,WAAW,CAACG,IAAI;MACxBC,OAAO,EAAEL,CAAC,CAACC,WAAW,CAACI;IACzB,CAAC,CAAC,GACJF,SACL;IACDP,aAAa,EACXA,aAAa,GACRI,CAAC,IACAJ,aAAa,CAAC;MACZU,SAAS,EAAEN,CAAC,CAACC,WAAW,CAACK,SAAS;MAClCC,EAAE,EAAEP,CAAC,CAACC,WAAW,CAACM;IACpB,CAAC,CAAC,GACJJ;EACL,CACF,CAAC;AAEN,CAAC,CAAC","ignoreList":[]}
|