Files
2026-04-23 08:12:53 +08:00

251 lines
9.4 KiB
Swift

import Libbox
import Library
import NetworkExtension
import SwiftUI
#if os(macOS)
import CoreLocation
#endif
#if os(macOS)
@MainActor
private final class WIFIStateLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var onAuthorizationGranted: (() -> Void)?
private var pendingAuthorizationRequest = false
override init() {
super.init()
manager.delegate = self
}
func requestAuthorizationAndShowWarning() {
let status = manager.authorizationStatus
switch status {
case .notDetermined:
pendingAuthorizationRequest = true
manager.requestAlwaysAuthorization()
default:
break
}
}
nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
Task { @MainActor in
guard self.pendingAuthorizationRequest else { return }
self.pendingAuthorizationRequest = false
let status = manager.authorizationStatus
if status == .authorized || status == .authorizedAlways {
self.onAuthorizationGranted?()
}
}
}
}
#endif
@MainActor
public struct GlobalChecksModifier: ViewModifier {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@Environment(\.selection) private var selection
@State private var alert: AlertState?
@State private var notStarted = false
@Environment(\.openURL) private var openURL
#if os(macOS)
@StateObject private var wifiLocationManager = WIFIStateLocationManager()
#endif
public init() {}
public func body(content: Content) -> some View {
contentView(content)
.alert($alert)
.onAppear {
handleImportProfile()
handleImportRemoteProfile()
}
.onChangeCompat(of: importProfile.wrappedValue) { _ in
Task { @MainActor in handleImportProfile() }
}
.onChangeCompat(of: importRemoteProfile.wrappedValue) { _ in
Task { @MainActor in handleImportRemoteProfile() }
}
.onChangeCompat(of: environments.extensionProfile?.status) { status in
handleStatusChange(status)
}
}
@ViewBuilder
private func contentView(_ content: Content) -> some View {
#if os(macOS)
content
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresWIFIState)) { _ in
Task { @MainActor in handleWiFiStateNotification() }
}
.onReceive(NotificationCenter.default.publisher(for: .extensionRequiresHelperService)) { _ in
Task { @MainActor in handleHelperServiceNotification() }
}
#else
content
#endif
}
private func handleImportProfile() {
guard let profile = importProfile.wrappedValue else { return }
importProfile.wrappedValue = nil
alert = AlertState(
title: String(localized: "Import Profile"),
message: String(localized: "Are you sure to import profile \(profile.name)?"),
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
selection.wrappedValue = .dashboard
Task {
do {
try await profile.importProfile()
} catch {
await MainActor.run {
alert = AlertState(action: "import profile", error: error)
}
return
}
environments?.profileUpdate.send()
}
},
secondaryButton: .cancel()
)
}
private func handleImportRemoteProfile() {
guard let remoteProfile = importRemoteProfile.wrappedValue else { return }
importRemoteProfile.wrappedValue = nil
alert = AlertState(
title: String(localized: "Import Remote Profile"),
message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
primaryButton: .default(String(localized: "Import")) { [weak environments, selection] in
selection.wrappedValue = .dashboard
environments?.pendingImportRemoteProfile = ImportRemoteProfileRequest(name: remoteProfile.name, url: remoteProfile.url)
},
secondaryButton: .cancel()
)
}
private func handleStatusChange(_ status: NEVPNStatus?) {
Task { @MainActor in
guard let status else { return }
switch status {
case .connected:
notStarted = false
await checkDeprecatedNotes()
case .connecting:
notStarted = true
case .disconnected:
if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) {
if notStarted, let profile = environments.extensionProfile {
await checkLastDisconnectError(profile: profile)
}
}
notStarted = false
default:
break
}
}
}
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
private nonisolated func checkLastDisconnectError(profile: ExtensionProfile) async {
if let alertState = await profile.checkLastDisconnectError() {
await MainActor.run {
alert = alertState
}
}
}
private nonisolated func checkDeprecatedNotes() async {
let disableWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
guard !disableWarnings else { return }
guard let reports = try? LibboxNewStandaloneCommandClient()!.getDeprecatedNotes() else { return }
if reports.hasNext() {
await MainActor.run {
showNextDeprecatedNote(reports)
}
}
}
private func showNextDeprecatedNote(_ reports: any LibboxDeprecatedNoteIteratorProtocol) {
guard reports.hasNext() else { return }
let report = reports.next()!
let continueChain: () -> Void = {
Task.detached {
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await MainActor.run {
showNextDeprecatedNote(reports)
}
}
}
#if os(tvOS)
var state = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
dismissButton: .cancel(String(localized: "Ok"))
)
state.onDismiss = continueChain
alert = state
#else
if report.migrationLink.isEmpty {
var state = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
dismissButton: .cancel(String(localized: "Ok"))
)
state.onDismiss = continueChain
alert = state
} else {
alert = AlertState(
title: String(localized: "Deprecated Warning"),
message: report.message(),
primaryButton: .default(String(localized: "Documentation")) {
openURL(URL(string: report.migrationLink)!)
},
secondaryButton: .cancel(String(localized: "Ok")),
onDismiss: continueChain
)
}
#endif
}
#if os(macOS)
private func handleWiFiStateNotification() {
guard Variant.useSystemExtension else { return }
wifiLocationManager.onAuthorizationGranted = {
alert = AlertState(
title: String(localized: "WiFi State Access"),
message: String(localized: "In the standalone version of SFM, reading WiFi state requires this app to be running. After you quit the SFM app, the sing-box service cannot continue to provide `wifi_ssid` and `wifi_bssid` routing rules.")
)
}
wifiLocationManager.requestAuthorizationAndShowWarning()
}
private func handleHelperServiceNotification() {
guard Variant.useSystemExtension, HelperServiceManager.rootHelperStatus != .enabled else { return }
alert = AlertState(
title: String(localized: "Helper Service Required"),
message: String(localized: "The sing-box service requires Helper Service to provide process lookup functionality, which supports `process_name` and `process_path` routing rules."),
primaryButton: .default(String(localized: "App Settings")) {
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
},
secondaryButton: .cancel()
)
}
#endif
}
public extension View {
func globalChecks() -> some View {
modifier(GlobalChecksModifier())
}
}