168 lines
4.8 KiB
Swift
168 lines
4.8 KiB
Swift
import DesignSystem
|
|
import Env
|
|
import Models
|
|
import Observation
|
|
import SafariServices
|
|
import SwiftUI
|
|
|
|
extension View {
|
|
@MainActor func withSafariRouter() -> some View {
|
|
modifier(SafariRouter())
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private struct SafariRouter: ViewModifier {
|
|
@Environment(Theme.self) private var theme
|
|
@Environment(UserPreferences.self) private var preferences
|
|
@Environment(RouterPath.self) private var routerPath
|
|
|
|
#if !os(visionOS)
|
|
@State private var safariManager = InAppSafariManager()
|
|
#endif
|
|
|
|
func body(content: Content) -> some View {
|
|
content
|
|
.environment(\.openURL, OpenURLAction { url in
|
|
// Open internal URL.
|
|
routerPath.handle(url: url)
|
|
})
|
|
.onOpenURL { url in
|
|
// Open external URL (from icecubesapp://)
|
|
let urlString = url.absoluteString.replacingOccurrences(of: AppInfo.scheme, with: "https://")
|
|
guard let url = URL(string: urlString), url.host != nil else { return }
|
|
_ = routerPath.handle(url: url)
|
|
}
|
|
.onAppear {
|
|
routerPath.urlHandler = { url in
|
|
if url.absoluteString.contains("@twitter.com"), url.absoluteString.hasPrefix("mailto:") {
|
|
let username = url.absoluteString
|
|
.replacingOccurrences(of: "@twitter.com", with: "")
|
|
.replacingOccurrences(of: "mailto:", with: "")
|
|
let twitterLink = "https://twitter.com/\(username)"
|
|
if let url = URL(string: twitterLink) {
|
|
UIApplication.shared.open(url)
|
|
return .handled
|
|
}
|
|
}
|
|
#if !targetEnvironment(macCatalyst)
|
|
guard preferences.preferredBrowser == .inAppSafari else { return .systemAction }
|
|
// SFSafariViewController only supports initial URLs with http:// or https:// schemes.
|
|
guard let scheme = url.scheme, ["https", "http"].contains(scheme.lowercased()) else {
|
|
return .systemAction
|
|
}
|
|
#if os(visionOS)
|
|
return .systemAction
|
|
#else
|
|
return safariManager.open(url)
|
|
#endif
|
|
#else
|
|
return .systemAction
|
|
#endif
|
|
}
|
|
}
|
|
#if !os(visionOS)
|
|
.background {
|
|
WindowReader { window in
|
|
safariManager.windowScene = window.windowScene
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if !os(visionOS)
|
|
@MainActor
|
|
@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate {
|
|
var windowScene: UIWindowScene?
|
|
let viewController: UIViewController = .init()
|
|
var window: UIWindow?
|
|
|
|
@MainActor
|
|
func open(_ url: URL) -> OpenURLAction.Result {
|
|
guard let windowScene else { return .systemAction }
|
|
|
|
window = setupWindow(windowScene: windowScene)
|
|
|
|
let configuration = SFSafariViewController.Configuration()
|
|
configuration.entersReaderIfAvailable = UserPreferences.shared.inAppBrowserReaderView
|
|
|
|
let safari = SFSafariViewController(url: url, configuration: configuration)
|
|
safari.preferredBarTintColor = UIColor(Theme.shared.primaryBackgroundColor)
|
|
safari.preferredControlTintColor = UIColor(Theme.shared.tintColor)
|
|
safari.delegate = self
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.viewController.present(safari, animated: true)
|
|
}
|
|
|
|
return .handled
|
|
}
|
|
|
|
func setupWindow(windowScene: UIWindowScene) -> UIWindow {
|
|
let window = window ?? UIWindow(windowScene: windowScene)
|
|
|
|
window.rootViewController = viewController
|
|
window.makeKeyAndVisible()
|
|
|
|
switch Theme.shared.selectedScheme {
|
|
case .dark:
|
|
window.overrideUserInterfaceStyle = .dark
|
|
case .light:
|
|
window.overrideUserInterfaceStyle = .light
|
|
}
|
|
|
|
self.window = window
|
|
return window
|
|
}
|
|
|
|
nonisolated func safariViewControllerDidFinish(_: SFSafariViewController) {
|
|
Task { @MainActor in
|
|
window?.resignKey()
|
|
window?.isHidden = false
|
|
window = nil
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private struct WindowReader: UIViewRepresentable {
|
|
var onUpdate: (UIWindow) -> Void
|
|
|
|
func makeUIView(context _: Context) -> InjectView {
|
|
InjectView(onUpdate: onUpdate)
|
|
}
|
|
|
|
func updateUIView(_: InjectView, context _: Context) {}
|
|
|
|
class InjectView: UIView {
|
|
var onUpdate: (UIWindow) -> Void
|
|
|
|
init(onUpdate: @escaping (UIWindow) -> Void) {
|
|
self.onUpdate = onUpdate
|
|
super.init(frame: .zero)
|
|
isHidden = true
|
|
isUserInteractionEnabled = false
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func willMove(toWindow newWindow: UIWindow?) {
|
|
super.willMove(toWindow: newWindow)
|
|
|
|
if let window = newWindow {
|
|
onUpdate(window)
|
|
} else {
|
|
DispatchQueue.main.async { [weak self] in
|
|
if let window = self?.window {
|
|
self?.onUpdate(window)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|