react-native-advanced-share-intent 1.0.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 +345 -0
- package/android/build.gradle +37 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/advancedshareintent/AdvancedShareIntentModule.kt +266 -0
- package/android/src/main/java/com/advancedshareintent/AdvancedShareIntentPackage.kt +16 -0
- package/index.d.ts +2 -0
- package/index.js +92 -0
- package/index.mjs +9 -0
- package/ios/AdvancedShareIntent.h +5 -0
- package/ios/AdvancedShareIntent.m +186 -0
- package/ios/ShareExtension/AdvancedShareIntentShareExtension.swift +363 -0
- package/package.json +70 -0
- package/react-native-advanced-share-intent.podspec +18 -0
- package/react-native.config.js +9 -0
- package/src/index.d.ts +44 -0
- package/src/index.ts +134 -0
package/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const {
|
|
2
|
+
NativeEventEmitter,
|
|
3
|
+
NativeModules,
|
|
4
|
+
Platform,
|
|
5
|
+
} = require('react-native');
|
|
6
|
+
|
|
7
|
+
const LINKING_ERROR =
|
|
8
|
+
'react-native-advanced-share-intent is not linked. Rebuild the native app after installing the package.';
|
|
9
|
+
|
|
10
|
+
const NativeAdvancedShareIntent = NativeModules.AdvancedShareIntent;
|
|
11
|
+
const EVENT_NAME = 'AdvancedShareIntentReceived';
|
|
12
|
+
|
|
13
|
+
const emitter = NativeAdvancedShareIntent
|
|
14
|
+
? new NativeEventEmitter(NativeAdvancedShareIntent)
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
function getNativeModule() {
|
|
18
|
+
if (!NativeAdvancedShareIntent) {
|
|
19
|
+
throw new Error(LINKING_ERROR);
|
|
20
|
+
}
|
|
21
|
+
return NativeAdvancedShareIntent;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizePayload(payload) {
|
|
25
|
+
if (!payload) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...payload,
|
|
31
|
+
files: Array.isArray(payload.files) ? payload.files : [],
|
|
32
|
+
receivedAt: payload.receivedAt ?? Date.now(),
|
|
33
|
+
isInitial: Boolean(payload.isInitial),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ShareIntent = {
|
|
38
|
+
async getInitialShare() {
|
|
39
|
+
return normalizePayload(await getNativeModule().getInitialShare());
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
addShareListener(listener) {
|
|
43
|
+
if (!emitter) {
|
|
44
|
+
throw new Error(LINKING_ERROR);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return emitter.addListener(EVENT_NAME, payload => {
|
|
48
|
+
const normalizedPayload = normalizePayload(payload);
|
|
49
|
+
if (normalizedPayload) {
|
|
50
|
+
listener(normalizedPayload);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async clearSharedData() {
|
|
56
|
+
await getNativeModule().clearSharedData();
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async setAppGroupIdentifier(identifier) {
|
|
60
|
+
if (Platform.OS !== 'ios') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nativeModule = getNativeModule();
|
|
65
|
+
if (!nativeModule.setAppGroupIdentifier) {
|
|
66
|
+
throw new Error('setAppGroupIdentifier is not available on this platform.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await nativeModule.setAppGroupIdentifier(identifier);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async setContainingAppScheme(scheme) {
|
|
73
|
+
if (Platform.OS !== 'ios') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nativeModule = getNativeModule();
|
|
78
|
+
if (!nativeModule.setContainingAppScheme) {
|
|
79
|
+
throw new Error('setContainingAppScheme is not available on this platform.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await nativeModule.setContainingAppScheme(scheme);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
module.exports = ShareIntent;
|
|
87
|
+
module.exports.default = ShareIntent;
|
|
88
|
+
module.exports.getInitialShare = ShareIntent.getInitialShare;
|
|
89
|
+
module.exports.addShareListener = ShareIntent.addShareListener;
|
|
90
|
+
module.exports.clearSharedData = ShareIntent.clearSharedData;
|
|
91
|
+
module.exports.setAppGroupIdentifier = ShareIntent.setAppGroupIdentifier;
|
|
92
|
+
module.exports.setContainingAppScheme = ShareIntent.setContainingAppScheme;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import ShareIntent from './index.js';
|
|
2
|
+
|
|
3
|
+
export const getInitialShare = ShareIntent.getInitialShare;
|
|
4
|
+
export const addShareListener = ShareIntent.addShareListener;
|
|
5
|
+
export const clearSharedData = ShareIntent.clearSharedData;
|
|
6
|
+
export const setAppGroupIdentifier = ShareIntent.setAppGroupIdentifier;
|
|
7
|
+
export const setContainingAppScheme = ShareIntent.setContainingAppScheme;
|
|
8
|
+
|
|
9
|
+
export default ShareIntent;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#import "AdvancedShareIntent.h"
|
|
2
|
+
|
|
3
|
+
#import <MobileCoreServices/MobileCoreServices.h>
|
|
4
|
+
#import <React/RCTConvert.h>
|
|
5
|
+
#import <UIKit/UIKit.h>
|
|
6
|
+
|
|
7
|
+
static NSString *const AdvancedShareIntentEventName = @"AdvancedShareIntentReceived";
|
|
8
|
+
static NSString *const AdvancedShareIntentDefaultsKey = @"AdvancedShareIntentPayload";
|
|
9
|
+
static NSString *const AdvancedShareIntentAppGroupKey = @"AdvancedShareIntentAppGroupIdentifier";
|
|
10
|
+
static NSString *const AdvancedShareIntentContainingAppSchemeKey = @"AdvancedShareIntentContainingAppScheme";
|
|
11
|
+
|
|
12
|
+
@interface AdvancedShareIntent ()
|
|
13
|
+
@property (nonatomic, assign) BOOL hasListeners;
|
|
14
|
+
@property (nonatomic, copy, nullable) NSDictionary *initialPayload;
|
|
15
|
+
@property (nonatomic, copy, nullable) NSString *appGroupIdentifier;
|
|
16
|
+
@property (nonatomic, copy, nullable) NSString *containingAppScheme;
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
@implementation AdvancedShareIntent
|
|
20
|
+
|
|
21
|
+
RCT_EXPORT_MODULE(AdvancedShareIntent)
|
|
22
|
+
|
|
23
|
+
+ (BOOL)requiresMainQueueSetup
|
|
24
|
+
{
|
|
25
|
+
return YES;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
- (instancetype)init
|
|
29
|
+
{
|
|
30
|
+
if ((self = [super init])) {
|
|
31
|
+
_appGroupIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:AdvancedShareIntentAppGroupKey];
|
|
32
|
+
_containingAppScheme = [[NSUserDefaults standardUserDefaults] stringForKey:AdvancedShareIntentContainingAppSchemeKey];
|
|
33
|
+
_initialPayload = [self readPayloadWithInitial:YES];
|
|
34
|
+
|
|
35
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
36
|
+
selector:@selector(applicationDidBecomeActive)
|
|
37
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
38
|
+
object:nil];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return self;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)dealloc
|
|
45
|
+
{
|
|
46
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (NSArray<NSString *> *)supportedEvents
|
|
50
|
+
{
|
|
51
|
+
return @[AdvancedShareIntentEventName];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
- (void)startObserving
|
|
55
|
+
{
|
|
56
|
+
self.hasListeners = YES;
|
|
57
|
+
NSDictionary *payload = [self readPayloadWithInitial:NO];
|
|
58
|
+
if (payload != nil) {
|
|
59
|
+
[self sendEventWithName:AdvancedShareIntentEventName body:payload];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)stopObserving
|
|
64
|
+
{
|
|
65
|
+
self.hasListeners = NO;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
RCT_REMAP_METHOD(getInitialShare,
|
|
69
|
+
getInitialShareWithResolver:(RCTPromiseResolveBlock)resolve
|
|
70
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
71
|
+
{
|
|
72
|
+
NSDictionary *payload = self.initialPayload ?: [self readPayloadWithInitial:YES];
|
|
73
|
+
self.initialPayload = payload;
|
|
74
|
+
resolve(payload ?: nil);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
RCT_REMAP_METHOD(clearSharedData,
|
|
78
|
+
clearSharedDataWithResolver:(RCTPromiseResolveBlock)resolve
|
|
79
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
80
|
+
{
|
|
81
|
+
self.initialPayload = nil;
|
|
82
|
+
NSUserDefaults *defaults = [self sharedDefaults];
|
|
83
|
+
[defaults removeObjectForKey:AdvancedShareIntentDefaultsKey];
|
|
84
|
+
[defaults synchronize];
|
|
85
|
+
[self removeSharedFiles];
|
|
86
|
+
resolve(nil);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
RCT_REMAP_METHOD(setAppGroupIdentifier,
|
|
90
|
+
setAppGroupIdentifier:(NSString *)identifier
|
|
91
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
92
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
93
|
+
{
|
|
94
|
+
self.appGroupIdentifier = identifier;
|
|
95
|
+
[[NSUserDefaults standardUserDefaults] setObject:identifier forKey:AdvancedShareIntentAppGroupKey];
|
|
96
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
97
|
+
self.initialPayload = [self readPayloadWithInitial:YES];
|
|
98
|
+
resolve(nil);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
RCT_REMAP_METHOD(setContainingAppScheme,
|
|
102
|
+
setContainingAppScheme:(NSString *)scheme
|
|
103
|
+
schemeResolver:(RCTPromiseResolveBlock)resolve
|
|
104
|
+
schemeRejecter:(RCTPromiseRejectBlock)reject)
|
|
105
|
+
{
|
|
106
|
+
self.containingAppScheme = scheme;
|
|
107
|
+
[[NSUserDefaults standardUserDefaults] setObject:scheme forKey:AdvancedShareIntentContainingAppSchemeKey];
|
|
108
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
109
|
+
resolve(nil);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
- (void)applicationDidBecomeActive
|
|
113
|
+
{
|
|
114
|
+
if (!self.hasListeners) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
NSDictionary *payload = [self readPayloadWithInitial:NO];
|
|
119
|
+
if (payload != nil) {
|
|
120
|
+
[self sendEventWithName:AdvancedShareIntentEventName body:payload];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
- (NSUserDefaults *)sharedDefaults
|
|
125
|
+
{
|
|
126
|
+
if (self.appGroupIdentifier.length > 0) {
|
|
127
|
+
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:self.appGroupIdentifier];
|
|
128
|
+
if (defaults != nil) {
|
|
129
|
+
return defaults;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [NSUserDefaults standardUserDefaults];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
- (NSDictionary *)readPayloadWithInitial:(BOOL)isInitial
|
|
137
|
+
{
|
|
138
|
+
NSUserDefaults *defaults = [self sharedDefaults];
|
|
139
|
+
NSDictionary *storedPayload = [defaults dictionaryForKey:AdvancedShareIntentDefaultsKey];
|
|
140
|
+
if (storedPayload == nil) {
|
|
141
|
+
return nil;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
NSMutableDictionary *payload = [storedPayload mutableCopy];
|
|
145
|
+
payload[@"isInitial"] = @(isInitial);
|
|
146
|
+
if (payload[@"files"] == nil) {
|
|
147
|
+
payload[@"files"] = @[];
|
|
148
|
+
}
|
|
149
|
+
if (payload[@"receivedAt"] == nil) {
|
|
150
|
+
payload[@"receivedAt"] = @([[NSDate date] timeIntervalSince1970] * 1000);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return payload;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
- (NSString *)classifyMimeType:(NSString *)mimeType
|
|
157
|
+
{
|
|
158
|
+
if ([mimeType hasPrefix:@"image/"] || [mimeType hasPrefix:@"public.image"]) {
|
|
159
|
+
return @"image";
|
|
160
|
+
}
|
|
161
|
+
if ([mimeType hasPrefix:@"video/"] || [mimeType hasPrefix:@"public.movie"]) {
|
|
162
|
+
return @"video";
|
|
163
|
+
}
|
|
164
|
+
if ([mimeType hasPrefix:@"text/"]) {
|
|
165
|
+
return @"text";
|
|
166
|
+
}
|
|
167
|
+
if ([mimeType isEqualToString:@"photos/asset"]) {
|
|
168
|
+
return @"image";
|
|
169
|
+
}
|
|
170
|
+
return @"document";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
- (void)removeSharedFiles
|
|
174
|
+
{
|
|
175
|
+
if (self.appGroupIdentifier.length == 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:self.appGroupIdentifier];
|
|
180
|
+
NSURL *directoryURL = [containerURL URLByAppendingPathComponent:@"AdvancedShareIntent" isDirectory:YES];
|
|
181
|
+
if (directoryURL != nil) {
|
|
182
|
+
[[NSFileManager defaultManager] removeItemAtURL:directoryURL error:nil];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@end
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import MobileCoreServices
|
|
3
|
+
import Photos
|
|
4
|
+
import Social
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
@objc(open) class AdvancedShareIntentShareExtension: UIViewController {
|
|
8
|
+
@objc open var appGroupIdentifier: String { "" }
|
|
9
|
+
@objc open var containingAppScheme: String { "" }
|
|
10
|
+
@objc open var sharedDirectoryName: String { "AdvancedShareIntent" }
|
|
11
|
+
|
|
12
|
+
private var activityIndicator: UIActivityIndicatorView?
|
|
13
|
+
private var cancelButton: UIButton?
|
|
14
|
+
|
|
15
|
+
open override func viewDidLoad() {
|
|
16
|
+
super.viewDidLoad()
|
|
17
|
+
setupLoadingView()
|
|
18
|
+
processSharedItems()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@objc private func cancel() {
|
|
22
|
+
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private func setupLoadingView() {
|
|
26
|
+
view.subviews.forEach { $0.removeFromSuperview() }
|
|
27
|
+
view.backgroundColor = .systemBackground
|
|
28
|
+
|
|
29
|
+
let indicator = UIActivityIndicatorView(style: .large)
|
|
30
|
+
indicator.translatesAutoresizingMaskIntoConstraints = false
|
|
31
|
+
indicator.startAnimating()
|
|
32
|
+
view.addSubview(indicator)
|
|
33
|
+
activityIndicator = indicator
|
|
34
|
+
|
|
35
|
+
let button = UIButton(type: .system)
|
|
36
|
+
button.setTitle("Cancel", for: .normal)
|
|
37
|
+
button.setTitleColor(.systemRed, for: .normal)
|
|
38
|
+
button.translatesAutoresizingMaskIntoConstraints = false
|
|
39
|
+
button.addTarget(self, action: #selector(cancel), for: .touchUpInside)
|
|
40
|
+
view.addSubview(button)
|
|
41
|
+
cancelButton = button
|
|
42
|
+
|
|
43
|
+
NSLayoutConstraint.activate([
|
|
44
|
+
indicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
45
|
+
indicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
46
|
+
button.topAnchor.constraint(equalTo: indicator.bottomAnchor, constant: 40),
|
|
47
|
+
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
48
|
+
button.heightAnchor.constraint(equalToConstant: 44)
|
|
49
|
+
])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func processSharedItems() {
|
|
53
|
+
Task {
|
|
54
|
+
let payload = await ShareExtensionPayloadBuilder(
|
|
55
|
+
context: extensionContext,
|
|
56
|
+
appGroupIdentifier: appGroupIdentifier,
|
|
57
|
+
sharedDirectoryName: sharedDirectoryName
|
|
58
|
+
).build()
|
|
59
|
+
|
|
60
|
+
save(payload: payload)
|
|
61
|
+
openContainingApp()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private func save(payload: [String: Any]) {
|
|
66
|
+
guard !appGroupIdentifier.isEmpty,
|
|
67
|
+
let defaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
defaults.set(payload, forKey: "AdvancedShareIntentPayload")
|
|
72
|
+
defaults.synchronize()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func openContainingApp() {
|
|
76
|
+
guard !containingAppScheme.isEmpty,
|
|
77
|
+
let url = URL(string: "\(containingAppScheme)://share-intent?source=share_extension×tamp=\(Int(Date().timeIntervalSince1970))") else {
|
|
78
|
+
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
extensionContext?.open(url) { [weak self] _ in
|
|
83
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
|
84
|
+
self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private final class ShareExtensionPayloadBuilder {
|
|
91
|
+
private let context: NSExtensionContext?
|
|
92
|
+
private let appGroupIdentifier: String
|
|
93
|
+
private let sharedDirectoryName: String
|
|
94
|
+
private let lock = NSLock()
|
|
95
|
+
private var indexedFiles: [Int: [String: Any]] = [:]
|
|
96
|
+
private var indexedText: [Int: String] = [:]
|
|
97
|
+
|
|
98
|
+
init(context: NSExtensionContext?, appGroupIdentifier: String, sharedDirectoryName: String) {
|
|
99
|
+
self.context = context
|
|
100
|
+
self.appGroupIdentifier = appGroupIdentifier
|
|
101
|
+
self.sharedDirectoryName = sharedDirectoryName
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func build() async -> [String: Any] {
|
|
105
|
+
let providers = (context?.inputItems as? [NSExtensionItem])?
|
|
106
|
+
.flatMap { $0.attachments ?? [] } ?? []
|
|
107
|
+
|
|
108
|
+
for (index, provider) in providers.enumerated() {
|
|
109
|
+
await process(provider: provider, index: index)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let files = orderedFiles()
|
|
113
|
+
let text = orderedText().joined(separator: "\n")
|
|
114
|
+
let firstMimeType = files.first?["mimeType"] as? String
|
|
115
|
+
|
|
116
|
+
var payload: [String: Any] = [
|
|
117
|
+
"files": files,
|
|
118
|
+
"isInitial": true,
|
|
119
|
+
"receivedAt": Date().timeIntervalSince1970 * 1000
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
if !text.isEmpty {
|
|
123
|
+
payload["text"] = text
|
|
124
|
+
payload["webUrl"] = text.firstWebURL
|
|
125
|
+
}
|
|
126
|
+
if let firstMimeType {
|
|
127
|
+
payload["mimeType"] = firstMimeType
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return payload
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func process(provider: NSItemProvider, index: Int) async {
|
|
134
|
+
if let text = await loadText(provider: provider) {
|
|
135
|
+
store(text: text, index: index)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if let assetFile = await loadPhotosAsset(provider: provider, index: index) {
|
|
140
|
+
store(file: assetFile, index: index)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if let file = await loadFile(provider: provider, index: index) {
|
|
145
|
+
store(file: file, index: index)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private func loadText(provider: NSItemProvider) async -> String? {
|
|
150
|
+
let identifiers = [kUTTypeURL as String, kUTTypePlainText as String, kUTTypeText as String]
|
|
151
|
+
guard let identifier = identifiers.first(where: { provider.hasItemConformingToTypeIdentifier($0) }) else {
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return await withCheckedContinuation { continuation in
|
|
156
|
+
provider.loadItem(forTypeIdentifier: identifier, options: nil) { item, _ in
|
|
157
|
+
if let url = item as? URL {
|
|
158
|
+
continuation.resume(returning: url.absoluteString)
|
|
159
|
+
} else if let string = item as? String {
|
|
160
|
+
continuation.resume(returning: string)
|
|
161
|
+
} else if let data = item as? Data, let string = String(data: data, encoding: .utf8) {
|
|
162
|
+
continuation.resume(returning: string)
|
|
163
|
+
} else {
|
|
164
|
+
continuation.resume(returning: nil)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func loadPhotosAsset(provider: NSItemProvider, index: Int) async -> [String: Any]? {
|
|
171
|
+
let identifiers = [
|
|
172
|
+
"com.apple.photos.asset",
|
|
173
|
+
"com.apple.photos.asset-id",
|
|
174
|
+
"com.apple.photos.asset-url",
|
|
175
|
+
"com.apple.private.auto-fill-photos-asset",
|
|
176
|
+
"com.apple.private.photos-cloud-sharing",
|
|
177
|
+
"com.apple.photos.library-asset",
|
|
178
|
+
"com.apple.photos.asset-identifier"
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for identifier in identifiers where provider.hasItemConformingToTypeIdentifier(identifier) {
|
|
182
|
+
if let assetId = await loadAssetIdentifier(provider: provider, identifier: identifier) {
|
|
183
|
+
return photosAssetPayload(assetId: assetId)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func loadAssetIdentifier(provider: NSItemProvider, identifier: String) async -> String? {
|
|
191
|
+
await withCheckedContinuation { continuation in
|
|
192
|
+
provider.loadItem(forTypeIdentifier: identifier, options: nil) { item, _ in
|
|
193
|
+
if let assetId = item as? String {
|
|
194
|
+
continuation.resume(returning: assetId)
|
|
195
|
+
} else if let url = item as? URL, url.scheme == "ph" {
|
|
196
|
+
continuation.resume(returning: url.absoluteString.replacingOccurrences(of: "ph://", with: ""))
|
|
197
|
+
} else if let data = item as? Data,
|
|
198
|
+
let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
|
|
199
|
+
let assetId = plist["assetID"] as? String {
|
|
200
|
+
continuation.resume(returning: assetId)
|
|
201
|
+
} else {
|
|
202
|
+
continuation.resume(returning: nil)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func photosAssetPayload(assetId: String) -> [String: Any] {
|
|
209
|
+
let assets = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
|
|
210
|
+
let asset = assets.firstObject
|
|
211
|
+
let mediaType = asset?.mediaType == .video ? "video" : "image"
|
|
212
|
+
let mimeType = mediaType == "video" ? "photos/video-asset" : "photos/image-asset"
|
|
213
|
+
let dateTaken = asset?.creationDate?.timeIntervalSince1970 ?? 0
|
|
214
|
+
|
|
215
|
+
return [
|
|
216
|
+
"uri": "ph://\(assetId)",
|
|
217
|
+
"fileName": "Photo Asset",
|
|
218
|
+
"name": "Photo Asset",
|
|
219
|
+
"mimeType": mimeType,
|
|
220
|
+
"type": mediaType,
|
|
221
|
+
"dateTaken": dateTaken * 1000,
|
|
222
|
+
"localIdentifier": assetId,
|
|
223
|
+
"originalUri": "ph://\(assetId)"
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private func loadFile(provider: NSItemProvider, index: Int) async -> [String: Any]? {
|
|
228
|
+
guard let identifier = preferredFileIdentifier(provider: provider) else {
|
|
229
|
+
return nil
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return await withCheckedContinuation { continuation in
|
|
233
|
+
provider.loadFileRepresentation(forTypeIdentifier: identifier) { [weak self] url, _ in
|
|
234
|
+
guard let self, let url else {
|
|
235
|
+
continuation.resume(returning: nil)
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let destination = self.sharedContainerDirectory()
|
|
240
|
+
.appendingPathComponent(UUID().uuidString)
|
|
241
|
+
.appendingPathExtension(url.pathExtension)
|
|
242
|
+
|
|
243
|
+
do {
|
|
244
|
+
if FileManager.default.fileExists(atPath: destination.path) {
|
|
245
|
+
try FileManager.default.removeItem(at: destination)
|
|
246
|
+
}
|
|
247
|
+
try FileManager.default.copyItem(at: url, to: destination)
|
|
248
|
+
continuation.resume(returning: self.filePayload(for: destination, provider: provider))
|
|
249
|
+
} catch {
|
|
250
|
+
continuation.resume(returning: nil)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func preferredFileIdentifier(provider: NSItemProvider) -> String? {
|
|
257
|
+
let preferred = [
|
|
258
|
+
kUTTypeImage as String,
|
|
259
|
+
kUTTypeMovie as String,
|
|
260
|
+
kUTTypePDF as String,
|
|
261
|
+
kUTTypeData as String
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
return preferred.first(where: { provider.hasItemConformingToTypeIdentifier($0) })
|
|
265
|
+
?? provider.registeredTypeIdentifiers.first
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private func filePayload(for url: URL, provider: NSItemProvider) -> [String: Any] {
|
|
269
|
+
let mimeType = provider.registeredTypeIdentifiers.first ?? "application/octet-stream"
|
|
270
|
+
let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
|
|
271
|
+
let createdDate = (attributes?[.creationDate] as? Date)?.timeIntervalSince1970 ?? Date().timeIntervalSince1970
|
|
272
|
+
|
|
273
|
+
var payload: [String: Any] = [
|
|
274
|
+
"uri": url.absoluteString,
|
|
275
|
+
"fileName": url.lastPathComponent,
|
|
276
|
+
"name": url.lastPathComponent,
|
|
277
|
+
"mimeType": mimeType,
|
|
278
|
+
"type": classify(mimeType: mimeType),
|
|
279
|
+
"dateTaken": createdDate * 1000,
|
|
280
|
+
"originalUri": url.absoluteString
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
if let size = attributes?[.size] as? NSNumber {
|
|
284
|
+
payload["size"] = size
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return payload
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private func orderedFiles() -> [[String: Any]] {
|
|
291
|
+
lock.lock()
|
|
292
|
+
let files = indexedFiles.keys.sorted().compactMap { indexedFiles[$0] }
|
|
293
|
+
lock.unlock()
|
|
294
|
+
|
|
295
|
+
let hasImages = files.contains { ($0["type"] as? String) == "image" }
|
|
296
|
+
let hasVideos = files.contains { ($0["type"] as? String) == "video" }
|
|
297
|
+
guard hasImages && hasVideos else {
|
|
298
|
+
return files
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let images = files.filter { ($0["type"] as? String) == "image" }
|
|
302
|
+
guard let first = images.first?["dateTaken"] as? Double,
|
|
303
|
+
let last = images.last?["dateTaken"] as? Double else {
|
|
304
|
+
return files
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
let ascending = first < last
|
|
308
|
+
return files.sorted {
|
|
309
|
+
let left = $0["dateTaken"] as? Double ?? 0
|
|
310
|
+
let right = $1["dateTaken"] as? Double ?? 0
|
|
311
|
+
return ascending ? left < right : left > right
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private func orderedText() -> [String] {
|
|
316
|
+
lock.lock()
|
|
317
|
+
let text = indexedText.keys.sorted().compactMap { indexedText[$0] }
|
|
318
|
+
lock.unlock()
|
|
319
|
+
return text
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private func store(file: [String: Any], index: Int) {
|
|
323
|
+
lock.lock()
|
|
324
|
+
indexedFiles[index] = file
|
|
325
|
+
lock.unlock()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private func store(text: String, index: Int) {
|
|
329
|
+
lock.lock()
|
|
330
|
+
indexedText[index] = text
|
|
331
|
+
lock.unlock()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private func sharedContainerDirectory() -> URL {
|
|
335
|
+
if !appGroupIdentifier.isEmpty,
|
|
336
|
+
let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
|
|
337
|
+
let directory = container.appendingPathComponent(sharedDirectoryName, isDirectory: true)
|
|
338
|
+
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
339
|
+
return directory
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return FileManager.default.temporaryDirectory
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private func classify(mimeType: String) -> String {
|
|
346
|
+
if mimeType.hasPrefix("public.image") || mimeType.hasPrefix("image/") {
|
|
347
|
+
return "image"
|
|
348
|
+
}
|
|
349
|
+
if mimeType.hasPrefix("public.movie") || mimeType.hasPrefix("video/") {
|
|
350
|
+
return "video"
|
|
351
|
+
}
|
|
352
|
+
if mimeType.hasPrefix("public.text") || mimeType.hasPrefix("text/") {
|
|
353
|
+
return "text"
|
|
354
|
+
}
|
|
355
|
+
return "document"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private extension String {
|
|
360
|
+
var firstWebURL: String? {
|
|
361
|
+
range(of: #"https?://\S+"#, options: .regularExpression).map { String(self[$0]) }
|
|
362
|
+
}
|
|
363
|
+
}
|