@react-native-documents/picker 9.3.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/LICENSE.md +21 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +80 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/reactnativedocumentpicker/CopyDestination.kt +12 -0
- package/android/src/main/java/com/reactnativedocumentpicker/DocumentMetadataBuilder.kt +79 -0
- package/android/src/main/java/com/reactnativedocumentpicker/FileOperations.kt +203 -0
- package/android/src/main/java/com/reactnativedocumentpicker/IntentFactory.kt +36 -0
- package/android/src/main/java/com/reactnativedocumentpicker/IsKnownTypeImpl.kt +40 -0
- package/android/src/main/java/com/reactnativedocumentpicker/MetadataGetter.kt +150 -0
- package/android/src/main/java/com/reactnativedocumentpicker/PickOptions.kt +63 -0
- package/android/src/main/java/com/reactnativedocumentpicker/PromiseWrapper.java +105 -0
- package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerModule.kt +352 -0
- package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerPackage.java +49 -0
- package/android/src/paper/java/com/reactnativedocumentpicker/NativeDocumentPickerSpec.java +69 -0
- package/ios/RCTConvert+RNDocumentPicker.h +8 -0
- package/ios/RCTConvert+RNDocumentPicker.mm +16 -0
- package/ios/RNDocumentPicker.h +19 -0
- package/ios/RNDocumentPicker.mm +128 -0
- package/ios/swift/DocPicker.swift +84 -0
- package/ios/swift/DocSaver.swift +41 -0
- package/ios/swift/DocumentMetadataBuilder.swift +69 -0
- package/ios/swift/FileOperations.swift +68 -0
- package/ios/swift/IsKnownTypeImpl.swift +42 -0
- package/ios/swift/LocalCopyResponse.swift +27 -0
- package/ios/swift/PickerBase.swift +78 -0
- package/ios/swift/PickerOptions.swift +44 -0
- package/ios/swift/PromiseSupport.swift +2 -0
- package/ios/swift/PromiseWrapper.swift +92 -0
- package/ios/swift/SaverOptions.swift +30 -0
- package/jest/build/jest/setup.js +70 -0
- package/jest/build/src/errors.js +47 -0
- package/jest/build/src/fileTypes.js +53 -0
- package/jest/build/src/index.js +22 -0
- package/jest/build/src/isKnownType.js +16 -0
- package/jest/build/src/keepLocalCopy.js +17 -0
- package/jest/build/src/pick.js +50 -0
- package/jest/build/src/pickDirectory.js +31 -0
- package/jest/build/src/release.js +22 -0
- package/jest/build/src/saveDocuments.js +40 -0
- package/jest/build/src/spec/NativeDocumentPicker.js +5 -0
- package/jest/build/src/types.js +4 -0
- package/jest/build/src/validateTypes.js +23 -0
- package/jest/build/tsconfig.tsbuildinfo +1 -0
- package/lib/commonjs/errors.js +53 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/fileTypes.js +84 -0
- package/lib/commonjs/fileTypes.js.map +1 -0
- package/lib/commonjs/index.js +74 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/isKnownType.js +27 -0
- package/lib/commonjs/isKnownType.js.map +1 -0
- package/lib/commonjs/keepLocalCopy.js +34 -0
- package/lib/commonjs/keepLocalCopy.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/pick.js +93 -0
- package/lib/commonjs/pick.js.map +1 -0
- package/lib/commonjs/pickDirectory.js +71 -0
- package/lib/commonjs/pickDirectory.js.map +1 -0
- package/lib/commonjs/release.js +31 -0
- package/lib/commonjs/release.js.map +1 -0
- package/lib/commonjs/saveDocuments.js +55 -0
- package/lib/commonjs/saveDocuments.js.map +1 -0
- package/lib/commonjs/spec/NativeDocumentPicker.js +16 -0
- package/lib/commonjs/spec/NativeDocumentPicker.js.map +1 -0
- package/lib/commonjs/types.js +37 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/validateTypes.js +29 -0
- package/lib/commonjs/validateTypes.js.map +1 -0
- package/lib/module/errors.js +48 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/fileTypes.js +81 -0
- package/lib/module/fileTypes.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/isKnownType.js +24 -0
- package/lib/module/isKnownType.js.map +1 -0
- package/lib/module/keepLocalCopy.js +31 -0
- package/lib/module/keepLocalCopy.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pick.js +90 -0
- package/lib/module/pick.js.map +1 -0
- package/lib/module/pickDirectory.js +68 -0
- package/lib/module/pickDirectory.js.map +1 -0
- package/lib/module/release.js +26 -0
- package/lib/module/release.js.map +1 -0
- package/lib/module/saveDocuments.js +52 -0
- package/lib/module/saveDocuments.js.map +1 -0
- package/lib/module/spec/NativeDocumentPicker.js +13 -0
- package/lib/module/spec/NativeDocumentPicker.js.map +1 -0
- package/lib/module/types.js +33 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validateTypes.js +24 -0
- package/lib/module/validateTypes.js.map +1 -0
- package/lib/typescript/errors.d.ts +40 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/fileTypes.d.ts +94 -0
- package/lib/typescript/fileTypes.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/isKnownType.d.ts +41 -0
- package/lib/typescript/isKnownType.d.ts.map +1 -0
- package/lib/typescript/keepLocalCopy.d.ts +46 -0
- package/lib/typescript/keepLocalCopy.d.ts.map +1 -0
- package/lib/typescript/pick.d.ts +84 -0
- package/lib/typescript/pick.d.ts.map +1 -0
- package/lib/typescript/pickDirectory.d.ts +62 -0
- package/lib/typescript/pickDirectory.d.ts.map +1 -0
- package/lib/typescript/release.d.ts +24 -0
- package/lib/typescript/release.d.ts.map +1 -0
- package/lib/typescript/saveDocuments.d.ts +55 -0
- package/lib/typescript/saveDocuments.d.ts.map +1 -0
- package/lib/typescript/spec/NativeDocumentPicker.d.ts +29 -0
- package/lib/typescript/spec/NativeDocumentPicker.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +95 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/validateTypes.d.ts +3 -0
- package/lib/typescript/validateTypes.d.ts.map +1 -0
- package/package.json +92 -0
- package/react-native-document-picker.podspec +30 -0
- package/src/errors.ts +49 -0
- package/src/fileTypes.ts +92 -0
- package/src/index.ts +47 -0
- package/src/isKnownType.ts +48 -0
- package/src/keepLocalCopy.ts +51 -0
- package/src/pick.ts +151 -0
- package/src/pickDirectory.ts +93 -0
- package/src/release.ts +36 -0
- package/src/saveDocuments.ts +99 -0
- package/src/spec/NativeDocumentPicker.ts +31 -0
- package/src/types.ts +119 -0
- package/src/validateTypes.ts +26 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
#import "RNDocumentPicker.h"
|
|
5
|
+
|
|
6
|
+
#import "RCTConvert+RNDocumentPicker.h"
|
|
7
|
+
// this header file is generated by Xcode: https://developer.apple.com/documentation/swift/importing-swift-into-objective-c
|
|
8
|
+
// if it cannot be found, try cleaning the build folder and Xcode derived data folder
|
|
9
|
+
#import "react_native_document_picker-Swift.h"
|
|
10
|
+
// for UIModalPresentationStyle conversion
|
|
11
|
+
// remove after https://github.com/facebook/react-native/commit/2d547a3252b328251e49dabfeec85f8d46c85411 is released
|
|
12
|
+
#import <React/RCTModalHostViewManager.h>
|
|
13
|
+
|
|
14
|
+
@interface RNDocumentPicker ()
|
|
15
|
+
@end
|
|
16
|
+
|
|
17
|
+
@implementation RNDocumentPicker {
|
|
18
|
+
DocPicker *docPicker;
|
|
19
|
+
DocSaver *docSaver;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
- (instancetype)init {
|
|
23
|
+
if ((self = [super init])) {
|
|
24
|
+
docPicker = [DocPicker new];
|
|
25
|
+
docSaver = [DocSaver new];
|
|
26
|
+
}
|
|
27
|
+
return self;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
+ (BOOL)requiresMainQueueSetup {
|
|
31
|
+
return NO;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
RCT_EXPORT_MODULE()
|
|
35
|
+
|
|
36
|
+
RCT_EXPORT_METHOD(pick:
|
|
37
|
+
(NSDictionary *) options
|
|
38
|
+
resolve:
|
|
39
|
+
(RCTPromiseResolveBlock) resolve
|
|
40
|
+
reject:
|
|
41
|
+
(RCTPromiseRejectBlock) reject)
|
|
42
|
+
{
|
|
43
|
+
// https://stackoverflow.com/questions/5270519/what-is-difference-between-uimodaltransitionstyle-and-uimodalpresentationstyle
|
|
44
|
+
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
|
|
45
|
+
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
|
|
46
|
+
NSArray *allowedUTIs = [RCTConvert NSArray:options[@"type"]];
|
|
47
|
+
BOOL allowMultiple = [RCTConvert BOOL:options[@"allowMultiSelection"]];
|
|
48
|
+
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
|
|
49
|
+
NSString *mode = options[@"mode"];
|
|
50
|
+
NSString *initialDir = options[@"initialDirectoryUrl"];
|
|
51
|
+
BOOL requestLongTermAccess = [RCTConvert BOOL:options[@"requestLongTermAccess"]];
|
|
52
|
+
|
|
53
|
+
PickerOptions *pickerOptions = [[PickerOptions alloc] initWithTypes:allowedUTIs
|
|
54
|
+
mode:mode
|
|
55
|
+
initialDirectoryUrl:initialDir
|
|
56
|
+
allowMultiSelection:allowMultiple
|
|
57
|
+
shouldShowFileExtensions:showExtensions
|
|
58
|
+
transitionStyle:transitionStyle
|
|
59
|
+
presentationStyle:presentationStyle
|
|
60
|
+
requestLongTermAccess:requestLongTermAccess];
|
|
61
|
+
|
|
62
|
+
[docPicker presentWithOptions:pickerOptions resolve:resolve reject:reject];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
RCT_EXPORT_METHOD(pickDirectory:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
66
|
+
void (^resolveWithDirectoryUri)(NSArray<NSDictionary *> *) = ^void(NSArray<NSDictionary *> *pickedValues) {
|
|
67
|
+
NSDictionary* firstObject = pickedValues.firstObject;
|
|
68
|
+
resolve(firstObject);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
[self pick:options resolve:resolveWithDirectoryUri reject:reject];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
RCT_EXPORT_METHOD(keepLocalCopy:
|
|
75
|
+
(NSDictionary *) options
|
|
76
|
+
resolve:
|
|
77
|
+
(RCTPromiseResolveBlock) resolve
|
|
78
|
+
reject:
|
|
79
|
+
(RCTPromiseRejectBlock) reject) {
|
|
80
|
+
NSArray *uris = options[@"files"];
|
|
81
|
+
NSString *destination = options[@"destination"];
|
|
82
|
+
|
|
83
|
+
[[FileOperations class] keepLocalCopyAtUniqueDestinationFrom:uris destinationPreset:destination resolve:resolve];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, isKnownType:(NSString *)kind value:(NSString *)value) {
|
|
87
|
+
NSDictionary* result = [[IsKnownTypeImpl class] checkType:kind value:value];
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
RCT_EXPORT_METHOD(writeDocuments:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
92
|
+
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
|
|
93
|
+
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
|
|
94
|
+
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
|
|
95
|
+
BOOL asCopy = [RCTConvert BOOL:options[@"copy"]];
|
|
96
|
+
|
|
97
|
+
NSString *initialDir = options[@"initialDirectoryUri"];
|
|
98
|
+
NSArray<NSString*> *documentUrl = options[@"sourceUris"];
|
|
99
|
+
|
|
100
|
+
SaverOptions* saverOptions = [[SaverOptions alloc] initWithSourceUrlStrings:documentUrl asCopy:asCopy initialDirectoryUrl:initialDir shouldShowFileExtensions:showExtensions transitionStyle:transitionStyle presentationStyle:presentationStyle];
|
|
101
|
+
|
|
102
|
+
[docSaver presentWithOptions:saverOptions resolve:resolve reject:reject];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
106
|
+
[docPicker stopAccessingOpenedUrls:uris];
|
|
107
|
+
resolve([NSNull null]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
RCT_EXPORT_METHOD(releaseLongTermAccess:(NSArray *)uris resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
111
|
+
resolve([NSNull null]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Thanks to this guard, we won't compile this code when we build for the old architecture.
|
|
115
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
116
|
+
|
|
117
|
+
- (void)saveDocument:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
118
|
+
// not needed on iOS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
122
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params {
|
|
123
|
+
return std::make_shared<facebook::react::NativeDocumentPickerSpecJSI>(params);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#endif
|
|
127
|
+
|
|
128
|
+
@end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
import MobileCoreServices
|
|
6
|
+
|
|
7
|
+
@objc public class DocPicker: PickerWithMetadataImpl {
|
|
8
|
+
|
|
9
|
+
var currentOptions: PickerOptions? = nil
|
|
10
|
+
|
|
11
|
+
@objc public func present(options: PickerOptions, resolve: @escaping RNDPPromiseResolveBlock, reject: @escaping RNDPPromiseRejectBlock) {
|
|
12
|
+
// TODO fix callsite param
|
|
13
|
+
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "pick")) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
currentOptions = options;
|
|
17
|
+
DispatchQueue.main.async {
|
|
18
|
+
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: options.allowedTypes, asCopy: options.modeAsCopy())
|
|
19
|
+
|
|
20
|
+
documentPicker.modalPresentationStyle = options.presentationStyle
|
|
21
|
+
documentPicker.allowsMultipleSelection = options.allowMultiSelection
|
|
22
|
+
documentPicker.modalTransitionStyle = options.transitionStyle
|
|
23
|
+
// documentPicker.directoryURL = options.initialDirectoryUrl
|
|
24
|
+
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions
|
|
25
|
+
|
|
26
|
+
self.presentInternal(documentPicker: documentPicker)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
|
|
31
|
+
if (currentOptions?.isOpenMode() == true) {
|
|
32
|
+
return try self.getOpenedDocumentInfo(url: url, requestLongTermAccess: currentOptions?.requestLongTermAccess ?? false)
|
|
33
|
+
} else {
|
|
34
|
+
return try self.getAnyModeMetadata(url: url)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private func getAnyModeMetadata(url: URL) throws -> DocumentMetadataBuilder {
|
|
39
|
+
let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .nameKey, .isDirectoryKey, .contentTypeKey])
|
|
40
|
+
|
|
41
|
+
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enum KeepLocalCopyError: Error {
|
|
45
|
+
case sourceAccessError
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func getOpenedDocumentInfo(url: URL, requestLongTermAccess: Bool) throws -> DocumentMetadataBuilder {
|
|
49
|
+
guard url.startAccessingSecurityScopedResource() else {
|
|
50
|
+
throw KeepLocalCopyError.sourceAccessError
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// url.stopAccessingSecurityScopedResource() must be called later
|
|
54
|
+
openedUrls.append(url)
|
|
55
|
+
|
|
56
|
+
// Use file coordination for reading and writing any of the URL’s content.
|
|
57
|
+
var error: NSError? = nil
|
|
58
|
+
var success = false
|
|
59
|
+
var metadataBuilder: DocumentMetadataBuilder = DocumentMetadataBuilder(forUri: url)
|
|
60
|
+
|
|
61
|
+
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
|
|
62
|
+
do {
|
|
63
|
+
metadataBuilder = try self.getAnyModeMetadata(url: url)
|
|
64
|
+
success = true
|
|
65
|
+
} catch {
|
|
66
|
+
metadataBuilder.setMetadataReadingError(error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (requestLongTermAccess == true) {
|
|
70
|
+
do {
|
|
71
|
+
let bookmarkData = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil)
|
|
72
|
+
metadataBuilder.setBookmark(bookmarkData)
|
|
73
|
+
} catch {
|
|
74
|
+
metadataBuilder.setBookmarkError(error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if let err = error, success == false {
|
|
79
|
+
throw err
|
|
80
|
+
}
|
|
81
|
+
return metadataBuilder
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DocSaver.swift
|
|
3
|
+
// react-native-document-picker
|
|
4
|
+
//
|
|
5
|
+
// Created by Vojtech Novak on 25.05.2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
// LICENSE: see License.md in the package root
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
import UniformTypeIdentifiers
|
|
12
|
+
import MobileCoreServices
|
|
13
|
+
|
|
14
|
+
@objc public class DocSaver: PickerWithMetadataImpl {
|
|
15
|
+
|
|
16
|
+
@objc public func present(options: SaverOptions, resolve: @escaping (Any?) -> Void, reject: @escaping (String?, String?, Error?) -> Void) {
|
|
17
|
+
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "saveDocuments")) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
DispatchQueue.main.async {
|
|
21
|
+
let documentPicker = UIDocumentPickerViewController(forExporting: options.sourceUrls, asCopy: options.asCopy)
|
|
22
|
+
|
|
23
|
+
documentPicker.modalPresentationStyle = options.presentationStyle
|
|
24
|
+
documentPicker.modalTransitionStyle = options.transitionStyle
|
|
25
|
+
// documentPicker.directoryURL = options.initialDirectoryUrl
|
|
26
|
+
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions
|
|
27
|
+
|
|
28
|
+
self.presentInternal(documentPicker: documentPicker)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
|
|
33
|
+
let name = url.lastPathComponent.removingPercentEncoding
|
|
34
|
+
|
|
35
|
+
var resourceValues = URLResourceValues()
|
|
36
|
+
resourceValues.name = name
|
|
37
|
+
|
|
38
|
+
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
|
|
6
|
+
public class DocumentMetadataBuilder {
|
|
7
|
+
private let uri: URL
|
|
8
|
+
private let resourceValues: URLResourceValues?
|
|
9
|
+
private var bookmarkData: Data?
|
|
10
|
+
private var metadataError: Error?
|
|
11
|
+
private var bookmarkError: Error?
|
|
12
|
+
|
|
13
|
+
init(forUri uri: URL) {
|
|
14
|
+
self.uri = uri
|
|
15
|
+
self.resourceValues = nil
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
init(forUri uri: URL, resourceValues: URLResourceValues) {
|
|
19
|
+
self.uri = uri
|
|
20
|
+
self.resourceValues = resourceValues
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
convenience init(forUri uri: URL, error: Error) {
|
|
24
|
+
self.init(forUri: uri)
|
|
25
|
+
self.metadataError = error
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func setBookmark(_ bookmark: Data) {
|
|
29
|
+
self.bookmarkData = bookmark
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func setBookmarkError(_ bookmarkError: Error) {
|
|
33
|
+
self.bookmarkError = bookmarkError
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func setMetadataReadingError(_ error: Error) {
|
|
37
|
+
self.metadataError = error
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func build() -> [String: Any?] {
|
|
41
|
+
var dictionary: [String: Any?] = [:]
|
|
42
|
+
if (resourceValues?.isDirectory ?? false == false) {
|
|
43
|
+
let utTypeFromFile: UTType? = resourceValues?.contentType
|
|
44
|
+
let utType: UTType? = utTypeFromFile ?? UTType(filenameExtension: uri.pathExtension)
|
|
45
|
+
|
|
46
|
+
dictionary = [
|
|
47
|
+
"name": resourceValues?.name,
|
|
48
|
+
"size": resourceValues?.fileSize,
|
|
49
|
+
"type": utType?.preferredMIMEType,
|
|
50
|
+
"nativeType": utType?.identifier,
|
|
51
|
+
"error": metadataError?.localizedDescription,
|
|
52
|
+
"isVirtual": false,
|
|
53
|
+
"convertibleToMimeTypes": nil
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
dictionary["uri"] = uri.absoluteString
|
|
58
|
+
|
|
59
|
+
if let bookmark = bookmarkData {
|
|
60
|
+
dictionary["bookmarkStatus"] = "success"
|
|
61
|
+
dictionary["bookmark"] = bookmark.base64EncodedString()
|
|
62
|
+
} else if let bookmarkError = bookmarkError {
|
|
63
|
+
dictionary["bookmarkStatus"] = "error"
|
|
64
|
+
dictionary["bookmarkError"] = bookmarkError.localizedDescription
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return dictionary
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
@objc public class FileOperations: NSObject {
|
|
6
|
+
|
|
7
|
+
@objc public static func keepLocalCopyAtUniqueDestination(from: Array<Dictionary<String, String>>, destinationPreset: String, resolve: @escaping RNDPPromiseResolveBlock) {
|
|
8
|
+
Task {
|
|
9
|
+
let results = await moveFiles(from: from, destinationPreset: destinationPreset)
|
|
10
|
+
resolve(results)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static func moveFiles(from: Array<Dictionary<String, String>>, destinationPreset: String) async -> [[String: String?]] {
|
|
15
|
+
let destinationRootDir = getDirectoryForFileDestination(destinationPreset)
|
|
16
|
+
let uniqueSubDirName = UUID().uuidString
|
|
17
|
+
let destinationDir: URL = destinationRootDir.appendingPathComponent("\(uniqueSubDirName)/", isDirectory: true)
|
|
18
|
+
// TODO do we need all of this Task dance?
|
|
19
|
+
|
|
20
|
+
return await withTaskGroup(of: LocalCopyResponse.self) { group in
|
|
21
|
+
var results: Array<Dictionary<String, String?>> = [[String: String?]]()
|
|
22
|
+
|
|
23
|
+
for dictionary in from {
|
|
24
|
+
group.addTask {
|
|
25
|
+
do {
|
|
26
|
+
guard let uriString = dictionary["uri"], let uri = URL(string: uriString) else {
|
|
27
|
+
return LocalCopyResponse.error(sourceUri: dictionary["uri"], copyError: "Invalid URI")
|
|
28
|
+
}
|
|
29
|
+
guard let fileName = dictionary["fileName"] else {
|
|
30
|
+
return LocalCopyResponse.error(sourceUri: uri.absoluteString, copyError: "Invalid fileName")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let destinationUrl = try moveToDestination(from: uri, usingFilename: fileName, destinationDir: destinationDir)
|
|
34
|
+
return LocalCopyResponse.success(sourceUri: uri.absoluteString, localUri: destinationUrl.absoluteString)
|
|
35
|
+
} catch {
|
|
36
|
+
return LocalCopyResponse.error(sourceUri: dictionary["uri"]!, copyError: error.localizedDescription)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for await result in group {
|
|
42
|
+
results.append(result.dictionaryRepresentation)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return results
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func moveToDestination(from: URL, usingFilename fileName: String, destinationDir: URL) throws -> URL {
|
|
50
|
+
let destinationFile = destinationDir.appendingPathComponent(fileName).standardized
|
|
51
|
+
|
|
52
|
+
guard destinationFile.path.hasPrefix(destinationDir.standardized.path) else {
|
|
53
|
+
throw NSError(domain: "PathTraversalPrevention", code: 400, userInfo: [NSLocalizedDescriptionKey: "The copied file is attempting to write outside of the target directory."])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true, attributes: nil)
|
|
57
|
+
try FileManager.default.moveItem(at: from, to: destinationFile)
|
|
58
|
+
|
|
59
|
+
return destinationFile
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static func getDirectoryForFileDestination(_ copyToDirectory: String) -> URL {
|
|
63
|
+
if copyToDirectory == "documentDirectory" {
|
|
64
|
+
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
65
|
+
}
|
|
66
|
+
return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//
|
|
2
|
+
// IsKnownTypeImpl.swift
|
|
3
|
+
// react-native-document-picker
|
|
4
|
+
//
|
|
5
|
+
// Created by Vojtech Novak on 26.05.2024.
|
|
6
|
+
// LICENSE: see License.md in the package root
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import UniformTypeIdentifiers
|
|
11
|
+
|
|
12
|
+
@objc public class IsKnownTypeImpl: NSObject {
|
|
13
|
+
|
|
14
|
+
@objc public static func checkType(_ kind: String, value: String) -> NSDictionary {
|
|
15
|
+
let dict = getTypeResult(kind, value: value)
|
|
16
|
+
return NSDictionary(dictionary: dict as [AnyHashable: Any])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static func getTypeResult(_ kind: String, value: String) -> Dictionary<String, Any?> {
|
|
20
|
+
if let utType = createUTType(kind: kind, value: value), utType.isDeclared == true {
|
|
21
|
+
return ["isKnown": true,
|
|
22
|
+
"UTType": utType.identifier,
|
|
23
|
+
"preferredFilenameExtension": utType.preferredFilenameExtension,
|
|
24
|
+
"mimeType": utType.preferredMIMEType]
|
|
25
|
+
}
|
|
26
|
+
return ["isKnown": false, "UTType": nil, "preferredFilenameExtension": nil, "mimeType": nil]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func createUTType(kind: String, value: String) -> UTType? {
|
|
30
|
+
switch kind {
|
|
31
|
+
case "UTType":
|
|
32
|
+
return UTType(value)
|
|
33
|
+
case "mimeType":
|
|
34
|
+
return UTType(mimeType: value)
|
|
35
|
+
case "extension":
|
|
36
|
+
return UTType(filenameExtension: value)
|
|
37
|
+
default:
|
|
38
|
+
return nil
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
// export type LocalCopyResponse =
|
|
6
|
+
// | {
|
|
7
|
+
// status: 'success'
|
|
8
|
+
// sourceUri: string
|
|
9
|
+
// localUri: string
|
|
10
|
+
// }
|
|
11
|
+
// | { status: 'error'; sourceUri: string; copyError: string }
|
|
12
|
+
|
|
13
|
+
enum LocalCopyResponse {
|
|
14
|
+
case success(sourceUri: String, localUri: String)
|
|
15
|
+
case error(sourceUri: String?, copyError: String)
|
|
16
|
+
|
|
17
|
+
var dictionaryRepresentation: [String: String?] {
|
|
18
|
+
switch self {
|
|
19
|
+
case .success(let sourceUri, let localUri):
|
|
20
|
+
return ["sourceUri": sourceUri, "localUri": localUri, "status": "success"]
|
|
21
|
+
case .error(let sourceUri, let copyError):
|
|
22
|
+
var result = ["copyError": copyError, "status": "error"]
|
|
23
|
+
result["sourceUri"] = sourceUri ?? nil
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DocSaver.swift
|
|
3
|
+
// react-native-document-picker
|
|
4
|
+
//
|
|
5
|
+
// Created by Vojtech Novak on 25.05.2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
// LICENSE: see License.md in the package root
|
|
9
|
+
|
|
10
|
+
import Foundation
|
|
11
|
+
import UniformTypeIdentifiers
|
|
12
|
+
import MobileCoreServices
|
|
13
|
+
|
|
14
|
+
public protocol GetsMetadataProtocol {
|
|
15
|
+
func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// https://stackoverflow.com/a/51333906/2070942
|
|
19
|
+
public typealias PickerWithMetadataImpl = PickerBase & GetsMetadataProtocol
|
|
20
|
+
|
|
21
|
+
public class PickerBase: NSObject, UIDocumentPickerDelegate, UIAdaptivePresentationControllerDelegate {
|
|
22
|
+
let promiseWrapper = PromiseWrapper()
|
|
23
|
+
var openedUrls: Array<URL> = []
|
|
24
|
+
|
|
25
|
+
func presentInternal(documentPicker: UIDocumentPickerViewController) {
|
|
26
|
+
documentPicker.delegate = self
|
|
27
|
+
documentPicker.presentationController?.delegate = self;
|
|
28
|
+
|
|
29
|
+
if let viewController = RCTPresentedViewController() {
|
|
30
|
+
viewController.present(documentPicker, animated: true, completion: nil)
|
|
31
|
+
} else {
|
|
32
|
+
let error = NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil)
|
|
33
|
+
promiseWrapper.reject("RCTPresentedViewController was nil", withCode: "PRESENTER_IS_NULL", withError: error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
|
38
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
39
|
+
// this doesn't run on the main thread
|
|
40
|
+
let documentsInfo = urls.compactMap(self.createDocumentMetadata).compactMap { $0.build() }
|
|
41
|
+
self.promiseWrapper.resolve(documentsInfo)
|
|
42
|
+
}
|
|
43
|
+
// https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/DocumentPickerProgrammingGuide/AccessingDocuments/AccessingDocuments.html#//apple_ref/doc/uid/TP40014451-CH2-SW4 "Accessing Files Outside Your Sandbox"
|
|
44
|
+
// https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func createDocumentMetadata(for url: URL) -> DocumentMetadataBuilder? {
|
|
48
|
+
guard let subclassThatGetsMetadata = self as? GetsMetadataProtocol else {
|
|
49
|
+
let error = NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: nil)
|
|
50
|
+
self.promiseWrapper.reject("PickerBase", withCode: "BAD_CLASS", withError: error)
|
|
51
|
+
return nil
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
do {
|
|
55
|
+
return try subclassThatGetsMetadata.getMetadataFor(url: url)
|
|
56
|
+
} catch {
|
|
57
|
+
return DocumentMetadataBuilder(forUri: url, error: error)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
|
62
|
+
promiseWrapper.rejectAsUserCancelledOperation()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
|
66
|
+
promiseWrapper.rejectAsUserCancelledOperation()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc public func stopAccessingOpenedUrls(_ urlStrings: [String]) {
|
|
70
|
+
let incomingUrls = Set(urlStrings.compactMap { URL(string: $0) })
|
|
71
|
+
openedUrls.removeAll { url in
|
|
72
|
+
guard incomingUrls.contains(url) else { return false }
|
|
73
|
+
url.stopAccessingSecurityScopedResource()
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
import UIKit
|
|
5
|
+
import UniformTypeIdentifiers
|
|
6
|
+
|
|
7
|
+
@objc public class PickerOptions: NSObject {
|
|
8
|
+
let allowedTypes: Array<UTType>
|
|
9
|
+
let mode: String // "import" or "open"
|
|
10
|
+
let allowMultiSelection: Bool
|
|
11
|
+
let transitionStyle: UIModalTransitionStyle
|
|
12
|
+
let presentationStyle: UIModalPresentationStyle
|
|
13
|
+
let initialDirectoryUrl: URL?
|
|
14
|
+
let shouldShowFileExtensions: Bool
|
|
15
|
+
let requestLongTermAccess: Bool
|
|
16
|
+
|
|
17
|
+
@objc public init(types: Array<String>, mode: String = "import", initialDirectoryUrl: String? = nil, allowMultiSelection: Bool, shouldShowFileExtensions: Bool, transitionStyle: UIModalTransitionStyle = .coverVertical, presentationStyle: UIModalPresentationStyle = .fullScreen, requestLongTermAccess: Bool = false) {
|
|
18
|
+
// TODO check if types were valid
|
|
19
|
+
allowedTypes = types.compactMap {
|
|
20
|
+
UTType($0)
|
|
21
|
+
}
|
|
22
|
+
self.allowMultiSelection = allowMultiSelection
|
|
23
|
+
self.transitionStyle = transitionStyle
|
|
24
|
+
self.presentationStyle = presentationStyle
|
|
25
|
+
self.mode = mode
|
|
26
|
+
if let unwrappedUrl = initialDirectoryUrl, let url = URL(string: unwrappedUrl) {
|
|
27
|
+
self.initialDirectoryUrl = url
|
|
28
|
+
} else {
|
|
29
|
+
self.initialDirectoryUrl = nil
|
|
30
|
+
}
|
|
31
|
+
self.shouldShowFileExtensions = shouldShowFileExtensions
|
|
32
|
+
self.requestLongTermAccess = requestLongTermAccess
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// asCopy: if true, the picker will give you access to a local copy of the document, otherwise you will have access to the original document
|
|
36
|
+
public func modeAsCopy() -> Bool {
|
|
37
|
+
return self.mode == "import"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public func isOpenMode() -> Bool {
|
|
41
|
+
return self.mode == "open"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
class PromiseWrapper {
|
|
6
|
+
private var promiseResolve: RNDPPromiseResolveBlock?
|
|
7
|
+
private var promiseReject: RNDPPromiseRejectBlock?
|
|
8
|
+
private var nameOfCallInProgress: String?
|
|
9
|
+
|
|
10
|
+
private let E_DOCUMENT_PICKER_CANCELED = "E_DOCUMENT_PICKER_CANCELED"
|
|
11
|
+
private let ASYNC_OP_IN_PROGRESS = "ASYNC_OP_IN_PROGRESS"
|
|
12
|
+
|
|
13
|
+
func setPromiseRejectingPrevious(_ resolve: @escaping RNDPPromiseResolveBlock,
|
|
14
|
+
rejecter reject: @escaping RNDPPromiseRejectBlock,
|
|
15
|
+
fromCallSite callsite: String) {
|
|
16
|
+
if let previousReject = promiseReject {
|
|
17
|
+
rejectPreviousPromiseBecauseNewOneIsInProgress(previousReject, requestedOperation: callsite)
|
|
18
|
+
}
|
|
19
|
+
promiseResolve = resolve
|
|
20
|
+
promiseReject = reject
|
|
21
|
+
nameOfCallInProgress = callsite
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func trySetPromiseRejectingIncoming(_ resolve: @escaping RNDPPromiseResolveBlock,
|
|
25
|
+
rejecter reject: @escaping RNDPPromiseRejectBlock,
|
|
26
|
+
fromCallSite callsite: String) -> Bool {
|
|
27
|
+
if promiseReject != nil {
|
|
28
|
+
rejectNewPromiseBecauseOldOneIsInProgress(reject, requestedOperation: callsite)
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
promiseResolve = resolve
|
|
32
|
+
promiseReject = reject
|
|
33
|
+
nameOfCallInProgress = callsite
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func resolve(_ result: Any?) {
|
|
38
|
+
guard let resolver = promiseResolve else {
|
|
39
|
+
print("cannot resolve promise because it's null")
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
resetMembers()
|
|
43
|
+
resolver(result)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func reject(_ message: String, withError error: NSError) {
|
|
47
|
+
let errorCode = String(error.code)
|
|
48
|
+
reject(message, withCode: errorCode, withError: error)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func reject(_ message: String, withCode errorCode: String, withError error: NSError) {
|
|
52
|
+
guard let rejecter = promiseReject else {
|
|
53
|
+
print("cannot reject promise because it's null")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
let errorMessage = "RNDPPromiseWrapper: \(message), \(error.description)"
|
|
57
|
+
resetMembers()
|
|
58
|
+
rejecter(errorCode, errorMessage, error)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func rejectAsUserCancelledOperation() {
|
|
62
|
+
let error = NSError(domain: NSCocoaErrorDomain,
|
|
63
|
+
code: NSUserCancelledError,
|
|
64
|
+
userInfo: nil)
|
|
65
|
+
reject("user canceled the document picker",
|
|
66
|
+
withCode: E_DOCUMENT_PICKER_CANCELED,
|
|
67
|
+
withError: error)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private func resetMembers() {
|
|
71
|
+
promiseResolve = nil
|
|
72
|
+
promiseReject = nil
|
|
73
|
+
nameOfCallInProgress = nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// TODO error messages
|
|
77
|
+
private func rejectPreviousPromiseBecauseNewOneIsInProgress(_ reject: RNDPPromiseRejectBlock,
|
|
78
|
+
requestedOperation callSiteName: String) {
|
|
79
|
+
let msg = "Warning: previous promise did not settle and was overwritten. " +
|
|
80
|
+
"You've called \"\(callSiteName)\" while \"\(nameOfCallInProgress ?? "")\" " +
|
|
81
|
+
"was already in progress and has not completed yet."
|
|
82
|
+
reject(ASYNC_OP_IN_PROGRESS, msg, nil)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private func rejectNewPromiseBecauseOldOneIsInProgress(_ reject: RNDPPromiseRejectBlock,
|
|
86
|
+
requestedOperation callSiteName: String) {
|
|
87
|
+
let msg = "Warning: previous promise did not settle and you attempted to overwrite it. " +
|
|
88
|
+
"You've called \"\(callSiteName)\" while \"\(nameOfCallInProgress ?? "")\" " +
|
|
89
|
+
"was already in progress and has not completed yet."
|
|
90
|
+
reject(ASYNC_OP_IN_PROGRESS, msg, nil)
|
|
91
|
+
}
|
|
92
|
+
}
|