@anythingai/launcher 0.1.8 → 0.1.9
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.
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'AnythingLauncher'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '15.1',
|
|
15
|
+
:tvos => '15.1'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.4'
|
|
18
|
+
s.source = { git: 'https://github.com/Create-Inc/anything-mobile-monorepo' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
# Swift/Objective-C compatibility
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// AnythingLauncherModule.swift
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import React
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
private let kBundleOverrideKey = "anything.bundleOverrideURL" // same key
|
|
7
|
+
private let kIsWebOverrideKey = "anything.bundleOverrideIsWeb"
|
|
8
|
+
|
|
9
|
+
public class AnythingLauncherModule: Module {
|
|
10
|
+
private var observersInstalled = false
|
|
11
|
+
|
|
12
|
+
public func definition() -> ModuleDefinition {
|
|
13
|
+
Name("AnythingLauncherModule")
|
|
14
|
+
|
|
15
|
+
OnCreate { self.installObserversIfNeeded() }
|
|
16
|
+
|
|
17
|
+
Function("open") { (url: String, showCloseButton: Bool, showProgress: Bool, isWeb: Bool) in
|
|
18
|
+
guard URL(string: url) != nil else {
|
|
19
|
+
throw GenericException("Invalid URL: \(url)")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
UserDefaults.standard.set(url, forKey: kBundleOverrideKey)
|
|
23
|
+
UserDefaults.standard.set(isWeb, forKey: kIsWebOverrideKey)
|
|
24
|
+
|
|
25
|
+
DispatchQueue.main.async {
|
|
26
|
+
ReloadOverlay.show(showCloseButton: showCloseButton, showProgress: showProgress)
|
|
27
|
+
RCTTriggerReloadCommandListeners("AnythingLauncher.open(\(url))")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Function("reset") { () in
|
|
32
|
+
UserDefaults.standard.removeObject(forKey: kBundleOverrideKey)
|
|
33
|
+
UserDefaults.standard.removeObject(forKey: kIsWebOverrideKey)
|
|
34
|
+
|
|
35
|
+
DispatchQueue.main.async {
|
|
36
|
+
RCTTriggerReloadCommandListeners("AnythingLauncher.reset()")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Function("reload") {
|
|
41
|
+
DispatchQueue.main.async {
|
|
42
|
+
ReloadOverlay.show(showCloseButton: false, showProgress: false)
|
|
43
|
+
RCTTriggerReloadCommandListeners("AnythingLauncher.reload()")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Function("getAppLauncherURL") { () -> String? in
|
|
48
|
+
return UserDefaults.standard.string(forKey: "anything.appLauncherURL")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Function("setAppLauncherURL") { (url: String?) in
|
|
52
|
+
if let url = url {
|
|
53
|
+
UserDefaults.standard.set(url, forKey: "anything.appLauncherURL")
|
|
54
|
+
} else {
|
|
55
|
+
UserDefaults.standard.removeObject(forKey: "anything.appLauncherURL")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Function("isWeb") { () -> Bool in
|
|
60
|
+
return UserDefaults.standard.bool(forKey: kIsWebOverrideKey)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func installObserversIfNeeded() {
|
|
65
|
+
guard !observersInstalled else { return }
|
|
66
|
+
observersInstalled = true
|
|
67
|
+
let nc = NotificationCenter.default
|
|
68
|
+
|
|
69
|
+
nc.addObserver(forName: NSNotification.Name("RCTJavaScriptDidLoadNotification"),
|
|
70
|
+
object: nil, queue: .main) { _ in
|
|
71
|
+
DispatchQueue.main.async {
|
|
72
|
+
ReloadOverlay.hide()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
nc.addObserver(forName: NSNotification.Name("RCTContentDidAppearNotification"),
|
|
76
|
+
object: nil, queue: .main) { _ in
|
|
77
|
+
DispatchQueue.main.async {
|
|
78
|
+
ReloadOverlay.hide()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
nc.addObserver(forName: NSNotification.Name("RCTJavaScriptDidFailToLoadNotification"),
|
|
82
|
+
object: nil, queue: .main) { _ in
|
|
83
|
+
UserDefaults.standard.removeObject(forKey: kBundleOverrideKey)
|
|
84
|
+
DispatchQueue.main.async {
|
|
85
|
+
ReloadOverlay.hide()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
nc.addObserver(forName: NSNotification.Name("ALJavaScriptLoaderProgress"),
|
|
90
|
+
object: nil, queue: .main) { note in
|
|
91
|
+
let status = note.userInfo?["status"] as? String
|
|
92
|
+
let ratio = note.userInfo?["ratio"] as? Double
|
|
93
|
+
DispatchQueue.main.async {
|
|
94
|
+
ReloadOverlay.update(status: status ?? "Loading…", ratio: ratio)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
nc.addObserver(forName: NSNotification.Name("ALJavaScriptLoaderComplete"),
|
|
99
|
+
object: nil, queue: .main) { _ in
|
|
100
|
+
DispatchQueue.main.async {
|
|
101
|
+
ReloadOverlay.hide()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
import React
|
|
4
|
+
|
|
5
|
+
private let kBundleOverrideKey = "anything.bundleOverrideURL" // same key
|
|
6
|
+
private let kIsWebOverrideKey = "anything.bundleOverrideIsWeb"
|
|
7
|
+
|
|
8
|
+
// Pure UI helper. Not a Module.
|
|
9
|
+
final class ReloadOverlay {
|
|
10
|
+
static var window: UIWindow?
|
|
11
|
+
|
|
12
|
+
// Keep handles so we can update them
|
|
13
|
+
private static weak var progressLabel: UILabel?
|
|
14
|
+
private static weak var progressBar: UIProgressView?
|
|
15
|
+
|
|
16
|
+
private static let closeHandler = CloseHandler()
|
|
17
|
+
|
|
18
|
+
@MainActor
|
|
19
|
+
static func show(showCloseButton: Bool, showProgress: Bool) {
|
|
20
|
+
guard window == nil else { return }
|
|
21
|
+
|
|
22
|
+
let overlayWindow: UIWindow
|
|
23
|
+
if #available(iOS 13.0, *) {
|
|
24
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
25
|
+
if let activeScene = scenes.first(where: { $0.activationState == .foregroundActive }) ?? scenes.first {
|
|
26
|
+
overlayWindow = UIWindow(windowScene: activeScene)
|
|
27
|
+
overlayWindow.frame = activeScene.coordinateSpace.bounds
|
|
28
|
+
} else {
|
|
29
|
+
overlayWindow = UIWindow(frame: UIScreen.main.bounds)
|
|
30
|
+
}
|
|
31
|
+
overlayWindow.overrideUserInterfaceStyle = .unspecified
|
|
32
|
+
} else {
|
|
33
|
+
overlayWindow = UIWindow(frame: UIScreen.main.bounds)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
overlayWindow.windowLevel = .statusBar + 1
|
|
37
|
+
overlayWindow.backgroundColor = .clear
|
|
38
|
+
|
|
39
|
+
let vc = UIViewController()
|
|
40
|
+
vc.view.backgroundColor = .systemBackground
|
|
41
|
+
let guide = vc.view.safeAreaLayoutGuide
|
|
42
|
+
|
|
43
|
+
// Splash image
|
|
44
|
+
let imageView = UIImageView(image: UIImage(named: "splash-icon"))
|
|
45
|
+
imageView.translatesAutoresizingMaskIntoConstraints = false
|
|
46
|
+
imageView.contentMode = .scaleAspectFit
|
|
47
|
+
vc.view.addSubview(imageView)
|
|
48
|
+
|
|
49
|
+
var constraints: [NSLayoutConstraint] = [
|
|
50
|
+
imageView.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor),
|
|
51
|
+
imageView.centerYAnchor.constraint(equalTo: guide.centerYAnchor, constant: -20),
|
|
52
|
+
imageView.widthAnchor.constraint(equalToConstant: 72),
|
|
53
|
+
imageView.heightAnchor.constraint(equalToConstant: 75),
|
|
54
|
+
imageView.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor, constant: 24)
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
var label: UILabel?
|
|
58
|
+
var bar: UIProgressView?
|
|
59
|
+
|
|
60
|
+
if showProgress {
|
|
61
|
+
// Progress label
|
|
62
|
+
let l = UILabel()
|
|
63
|
+
l.translatesAutoresizingMaskIntoConstraints = false
|
|
64
|
+
l.font = .systemFont(ofSize: 13, weight: .regular)
|
|
65
|
+
l.textColor = .secondaryLabel
|
|
66
|
+
l.textAlignment = .center
|
|
67
|
+
l.text = "Preparing…"
|
|
68
|
+
vc.view.addSubview(l)
|
|
69
|
+
|
|
70
|
+
// Progress bar
|
|
71
|
+
let b = UIProgressView(progressViewStyle: .default)
|
|
72
|
+
b.translatesAutoresizingMaskIntoConstraints = false
|
|
73
|
+
b.progress = 0
|
|
74
|
+
b.progressTintColor = .label
|
|
75
|
+
b.trackTintColor = .tertiarySystemFill
|
|
76
|
+
b.transform = b.transform.scaledBy(x: 1, y: 2.0)
|
|
77
|
+
vc.view.addSubview(b)
|
|
78
|
+
|
|
79
|
+
// Pin to bottom safe area
|
|
80
|
+
constraints.append(contentsOf: [
|
|
81
|
+
b.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 24),
|
|
82
|
+
b.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -24),
|
|
83
|
+
b.bottomAnchor.constraint(equalTo: guide.bottomAnchor, constant: -24),
|
|
84
|
+
|
|
85
|
+
l.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 24),
|
|
86
|
+
l.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -24),
|
|
87
|
+
l.bottomAnchor.constraint(equalTo: b.topAnchor, constant: -8),
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
label = l
|
|
91
|
+
bar = b
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
NSLayoutConstraint.activate(constraints)
|
|
95
|
+
|
|
96
|
+
// Optional close button
|
|
97
|
+
if showCloseButton {
|
|
98
|
+
let closeButton = UIButton(type: .system)
|
|
99
|
+
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
100
|
+
closeButton.backgroundColor = .clear
|
|
101
|
+
if let img = UIImage(systemName: "xmark") {
|
|
102
|
+
closeButton.setImage(img, for: .normal)
|
|
103
|
+
closeButton.tintColor = .label
|
|
104
|
+
} else {
|
|
105
|
+
closeButton.setTitle("×", for: .normal)
|
|
106
|
+
closeButton.setTitleColor(.label, for: .normal)
|
|
107
|
+
closeButton.titleLabel?.font = .systemFont(ofSize: 20, weight: .semibold)
|
|
108
|
+
}
|
|
109
|
+
closeButton.accessibilityLabel = "Exit reload"
|
|
110
|
+
closeButton.addTarget(closeHandler, action: #selector(CloseHandler.onTap), for: .touchUpInside)
|
|
111
|
+
vc.view.addSubview(closeButton)
|
|
112
|
+
|
|
113
|
+
NSLayoutConstraint.activate([
|
|
114
|
+
closeButton.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8),
|
|
115
|
+
closeButton.trailingAnchor.constraint(equalTo: guide.trailingAnchor, constant: -8),
|
|
116
|
+
closeButton.heightAnchor.constraint(equalToConstant: 32),
|
|
117
|
+
closeButton.widthAnchor.constraint(equalToConstant: 32)
|
|
118
|
+
])
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
overlayWindow.rootViewController = vc
|
|
122
|
+
overlayWindow.isHidden = false
|
|
123
|
+
overlayWindow.makeKeyAndVisible()
|
|
124
|
+
window = overlayWindow
|
|
125
|
+
|
|
126
|
+
// store refs
|
|
127
|
+
progressLabel = label
|
|
128
|
+
progressBar = bar
|
|
129
|
+
|
|
130
|
+
if showProgress {
|
|
131
|
+
update(status: "Preparing…", ratio: nil)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@MainActor
|
|
136
|
+
static func update(status: String?, ratio: Double?) {
|
|
137
|
+
if let label = progressLabel, let status = status {
|
|
138
|
+
if let r = ratio {
|
|
139
|
+
let pct = Int((max(0.0, min(1.0, r))) * 100.0)
|
|
140
|
+
label.text = "\(status) \(pct)%"
|
|
141
|
+
} else {
|
|
142
|
+
label.text = status
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if let r = ratio, let bar = progressBar {
|
|
146
|
+
bar.progress = Float(max(0.0, min(1.0, r)))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@MainActor
|
|
151
|
+
static func hide() {
|
|
152
|
+
guard let w = window else { return }
|
|
153
|
+
// Add slight delay to avoid flashing
|
|
154
|
+
Task { @MainActor in
|
|
155
|
+
// try? await Task.sleep(nanoseconds: 300_000_000) // 300ms
|
|
156
|
+
guard window === w else { return } // don't hide if another overlay was shown
|
|
157
|
+
w.isHidden = true
|
|
158
|
+
window = nil
|
|
159
|
+
progressLabel = nil
|
|
160
|
+
progressBar = nil
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private final class CloseHandler: NSObject {
|
|
165
|
+
@objc func onTap() {
|
|
166
|
+
// Haptic feedback on close
|
|
167
|
+
let generator = UIImpactFeedbackGenerator(style: .medium)
|
|
168
|
+
generator.impactOccurred()
|
|
169
|
+
UserDefaults.standard.removeObject(forKey: kBundleOverrideKey)
|
|
170
|
+
UserDefaults.standard.removeObject(forKey: kIsWebOverrideKey)
|
|
171
|
+
DispatchQueue.main.async {
|
|
172
|
+
ReloadOverlay.hide()
|
|
173
|
+
RCTTriggerReloadCommandListeners("AnythingLauncher.reset()")
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public final class AnythingLauncherReloadOverlayModule: Module {
|
|
180
|
+
public func definition() -> ModuleDefinition {
|
|
181
|
+
Name("AnythingLauncherReloadOverlayModule")
|
|
182
|
+
|
|
183
|
+
Function("show") { (showCloseButton: Bool, showProgress: Bool) -> Void in
|
|
184
|
+
DispatchQueue.main.async {
|
|
185
|
+
ReloadOverlay.show(showCloseButton: showCloseButton, showProgress: showProgress)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Function("update") { (status: String?, ratio: Double?) -> Void in
|
|
190
|
+
DispatchQueue.main.async {
|
|
191
|
+
ReloadOverlay.update(status: status, ratio: ratio)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Function("hide") { () -> Void in
|
|
196
|
+
DispatchQueue.main.async {
|
|
197
|
+
ReloadOverlay.hide()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <objc/runtime.h>
|
|
3
|
+
#import <React/RCTJavaScriptLoader.h>
|
|
4
|
+
|
|
5
|
+
// Notification names you can observe from Swift.
|
|
6
|
+
// (You can also just use the raw strings in Swift and skip these if you like.)
|
|
7
|
+
NSString *const ALJavaScriptLoaderProgressNotification = @"ALJavaScriptLoaderProgress";
|
|
8
|
+
NSString *const ALJavaScriptLoaderCompleteNotification = @"ALJavaScriptLoaderComplete";
|
|
9
|
+
|
|
10
|
+
@implementation RCTJavaScriptLoader (ALProgress)
|
|
11
|
+
|
|
12
|
+
+ (void)load
|
|
13
|
+
{
|
|
14
|
+
static dispatch_once_t onceToken;
|
|
15
|
+
dispatch_once(&onceToken, ^{
|
|
16
|
+
// Swizzle the class method: +loadBundleAtURL:onProgress:onComplete:
|
|
17
|
+
Class meta = object_getClass((id)self);
|
|
18
|
+
SEL originalSEL = @selector(loadBundleAtURL:onProgress:onComplete:);
|
|
19
|
+
SEL swizzledSEL = @selector(al_loadBundleAtURL:onProgress:onComplete:);
|
|
20
|
+
|
|
21
|
+
Method original = class_getClassMethod(self, originalSEL);
|
|
22
|
+
Method swizzled = class_getClassMethod(self, swizzledSEL);
|
|
23
|
+
|
|
24
|
+
if (original && swizzled) {
|
|
25
|
+
method_exchangeImplementations(original, swizzled);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
+ (void)al_loadBundleAtURL:(NSURL *)scriptURL
|
|
31
|
+
onProgress:(RCTSourceLoadProgressBlock)onProgress
|
|
32
|
+
onComplete:(RCTSourceLoadBlock)onComplete
|
|
33
|
+
{
|
|
34
|
+
// Wrap progress: forward to original callback and also post a notification.
|
|
35
|
+
RCTSourceLoadProgressBlock wrappedProgress = ^(RCTLoadingProgress *progress) {
|
|
36
|
+
if (progress) {
|
|
37
|
+
NSNumber *done = progress.done ?: @(0);
|
|
38
|
+
NSNumber *total = progress.total ?: @(0);
|
|
39
|
+
|
|
40
|
+
double ratio = 0.0;
|
|
41
|
+
if (total.integerValue > 0) {
|
|
42
|
+
ratio = (double)done.integerValue / (double)total.integerValue;
|
|
43
|
+
if (ratio < 0.0) ratio = 0.0;
|
|
44
|
+
if (ratio > 1.0) ratio = 1.0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:ALJavaScriptLoaderProgressNotification
|
|
48
|
+
object:nil
|
|
49
|
+
userInfo:@{
|
|
50
|
+
@"status": progress.status ?: @"",
|
|
51
|
+
@"done": done,
|
|
52
|
+
@"total": total,
|
|
53
|
+
@"ratio": @(ratio)
|
|
54
|
+
}];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (onProgress) { onProgress(progress); }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Wrap completion: DO NOT touch private RCTSource ivars.
|
|
61
|
+
RCTSourceLoadBlock wrappedComplete = ^(NSError *error, RCTSource *source) {
|
|
62
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:ALJavaScriptLoaderCompleteNotification
|
|
63
|
+
object:nil
|
|
64
|
+
userInfo:@{
|
|
65
|
+
@"error": error ?: (id)kCFNull,
|
|
66
|
+
// Useful for debugging what you *asked* RN to load:
|
|
67
|
+
@"requestedURL": scriptURL.absoluteString ?: @""
|
|
68
|
+
}];
|
|
69
|
+
if (onComplete) { onComplete(error, source); }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Call the original implementation (now swapped).
|
|
73
|
+
[self al_loadBundleAtURL:scriptURL onProgress:wrappedProgress onComplete:wrappedComplete];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@end
|