diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index e950be5e2..89f0631ac 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0106179F2AF38EFA005C7877 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FEC6682AD9369C00BAD9F5 /* Command.swift */; }; 015B976A2A4F2F4500F9FA4D /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 015B97692A4F2F4500F9FA4D /* Alamofire */; }; 015B976D2A4F2F6C00F9FA4D /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 015B976C2A4F2F6C00F9FA4D /* RxCocoa */; }; 015B976F2A4F2F6C00F9FA4D /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 015B976E2A4F2F6C00F9FA4D /* RxSwift */; }; @@ -221,6 +222,7 @@ 49D223392A1DA5F10002FFCB /* SSIDSuspendTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */; }; 49D6A45229AEEC15006487EF /* StatusItemTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A45129AEEC15006487EF /* StatusItemTool.swift */; }; 49D6A45629AEEC55006487EF /* StatusItemViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */; }; + 49FEC6692AD9369C00BAD9F5 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FEC6682AD9369C00BAD9F5 /* Command.swift */; }; 8A2BBEA727A03ACB0081EBEF /* ProxySetting.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8A2BBEA627A03ACB0081EBEF /* ProxySetting.sdef */; }; 8ACD21BB27A04C7800BC4632 /* ProxySettingCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ACD21BA27A04C7800BC4632 /* ProxySettingCommand.swift */; }; 8ACD21BD27A04ED500BC4632 /* ProxyModeChangeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ACD21BC27A04ED500BC4632 /* ProxyModeChangeCommand.swift */; }; @@ -399,6 +401,7 @@ 49D6A45529AEEC55006487EF /* StatusItemViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemViewProtocol.swift; sourceTree = ""; }; 49D8276627E9B01700159D93 /* LoginKitWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginKitWrapper.h; sourceTree = ""; }; 49D8276727E9B01700159D93 /* LoginKitWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginKitWrapper.m; sourceTree = ""; }; + 49FEC6682AD9369C00BAD9F5 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; 8A2BBEA627A03ACB0081EBEF /* ProxySetting.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = ProxySetting.sdef; sourceTree = ""; }; 8ACD21BA27A04C7800BC4632 /* ProxySettingCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxySettingCommand.swift; sourceTree = ""; }; 8ACD21BC27A04ED500BC4632 /* ProxyModeChangeCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyModeChangeCommand.swift; sourceTree = ""; }; @@ -515,6 +518,7 @@ 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */, 49B445152457CDF000B27E3E /* ClashStatusTool.swift */, 49D223382A1DA5F10002FFCB /* SSIDSuspendTool.swift */, + 49FEC6682AD9369C00BAD9F5 /* Command.swift */, ); path = Utils; sourceTree = ""; @@ -1019,6 +1023,7 @@ 01F335E82AD10D0B0048AF77 /* AlphaMetaDownloader.swift in Sources */, 01F335E92AD10D0B0048AF77 /* NetworkChangeNotifier.swift in Sources */, 01F335EA2AD10D0B0048AF77 /* GlobalShortCutViewController.swift in Sources */, + 0106179F2AF38EFA005C7877 /* Command.swift in Sources */, 01F335EB2AD10D0B0048AF77 /* Notification.swift in Sources */, 01F335EC2AD10D0B0048AF77 /* ClashMetaConfig.swift in Sources */, 01F335ED2AD10D0B0048AF77 /* ProxyModeChangeCommand.swift in Sources */, @@ -1135,6 +1140,7 @@ 49722FF0211F338B00650A41 /* EventStream.swift in Sources */, 499A486522EEA3FD00F6C675 /* Array+Safe.swift in Sources */, F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */, + 49FEC6692AD9369C00BAD9F5 /* Command.swift in Sources */, F92D0B2A236C759100575E15 /* NSTextField+Vibrancy.swift in Sources */, 49D223392A1DA5F10002FFCB /* SSIDSuspendTool.swift in Sources */, F910AA24240134AF00116E95 /* ProxyGroupMenu.swift in Sources */, diff --git a/ClashX/Basic/Logger.swift b/ClashX/Basic/Logger.swift index 1355ffeac..a16c5827b 100644 --- a/ClashX/Basic/Logger.swift +++ b/ClashX/Basic/Logger.swift @@ -41,8 +41,8 @@ class Logger { } } - static func log(_ msg: String, level: ClashLogLevel = .info, function: String = #function) { - shared.logToFile(msg: "[\(level.rawValue)] \(function) \(msg)", level: level) + static func log(_ msg: String, level: ClashLogLevel = .info, file: String = #file, function: String = #function) { + shared.logToFile(msg: "[\(level.rawValue)] \(file) \(function) \(msg)", level: level) } func logFilePath() -> String { diff --git a/ClashX/ClashWindowController.swift b/ClashX/ClashWindowController.swift index a983af73f..ffc9d66c1 100644 --- a/ClashX/ClashWindowController.swift +++ b/ClashX/ClashWindowController.swift @@ -71,6 +71,8 @@ class ClashWindowController: NSWindowController, NSWindowDe } window?.makeKeyAndOrderFront(self) window?.delegate = self + NSApp.activate(ignoringOtherApps: true) + window?.makeKeyAndOrderFront(nil) } func windowWillClose(_ notification: Notification) { diff --git a/ClashX/General/Utils/Command.swift b/ClashX/General/Utils/Command.swift new file mode 100644 index 000000000..528bb5f8d --- /dev/null +++ b/ClashX/General/Utils/Command.swift @@ -0,0 +1,34 @@ +// +// Command.swift +// ClashX +// +// Created by yicheng on 2023/10/13. +// Copyright © 2023 west2online. All rights reserved. +// + +import Foundation + +struct Command { + let cmd: String + let args: [String] + + func run() -> String { + var output = "" + + let task = Process() + task.launchPath = cmd + task.arguments = args + + let outpipe = Pipe() + task.standardOutput = outpipe + + task.launch() + + task.waitUntilExit() + let outdata = outpipe.fileHandleForReading.readDataToEndOfFile() + if var string = String(data: outdata, encoding: .utf8) { + output = string.trimmingCharacters(in: .newlines) + } + return output + } +} diff --git a/ClashX/General/Utils/NetworkChangeNotifier.swift b/ClashX/General/Utils/NetworkChangeNotifier.swift index 655a774ed..e51162167 100644 --- a/ClashX/General/Utils/NetworkChangeNotifier.swift +++ b/ClashX/General/Utils/NetworkChangeNotifier.swift @@ -198,8 +198,4 @@ class NetworkChangeNotifier { } return allowIPV6 ? ipv6 : nil } - - static func getCurrentSSID() -> String? { - return CWWiFiClient.shared().interface()?.ssid() - } } diff --git a/ClashX/General/Utils/SSIDSuspendTool.swift b/ClashX/General/Utils/SSIDSuspendTool.swift index 467f4bf27..0c88dba22 100644 --- a/ClashX/General/Utils/SSIDSuspendTool.swift +++ b/ClashX/General/Utils/SSIDSuspendTool.swift @@ -6,24 +6,48 @@ // Copyright © 2023 west2online. All rights reserved. // +import CoreLocation +import CoreWLAN import Foundation import RxCocoa import RxSwift +import AppKit -class SSIDSuspendTool { +class SSIDSuspendTool: NSObject { static let shared = SSIDSuspendTool() - var disposeBag = DisposeBag() - func setup() { - NotificationCenter - .default - .rx - .notification(.systemNetworkStatusDidChange) - .observe(on: MainScheduler.instance) - .delay(.seconds(2), scheduler: MainScheduler.instance) - .bind { [weak self] _ in - self?.update() - }.disposed(by: disposeBag) + private var ssidChangePublisher = PublishSubject() + private var disposeBag = DisposeBag() + private lazy var locationManager = CLLocationManager() + var showNoticeOnNotPermission = false + + func setup() { + if AppVersionUtil.hasVersionChanged { + showNoticeOnNotPermission = true + } + requestPermissionIfNeed() + do { + try CWWiFiClient.shared().startMonitoringEvent(with: .ssidDidChange) + CWWiFiClient.shared().delegate = self + ssidChangePublisher + .observe(on: MainScheduler.instance) + .debounce(.seconds(1), scheduler: MainScheduler.instance) + .delay(.seconds(1), scheduler: MainScheduler.instance) + .bind { [weak self] _ in + self?.update() + }.disposed(by: disposeBag) + } catch let err { + Logger.log(String(describing: err), level: .warning) + NotificationCenter + .default + .rx + .notification(.systemNetworkStatusDidChange) + .observe(on: MainScheduler.instance) + .delay(.seconds(2), scheduler: MainScheduler.instance) + .bind { [weak self] _ in + self?.update() + }.disposed(by: disposeBag) + } ConfigManager.shared .proxyShouldPaused .asObservable() @@ -40,6 +64,27 @@ class SSIDSuspendTool { update() } + func requestPermissionIfNeed() { + defer { + showNoticeOnNotPermission = false + } + if #available(macOS 14, *) { + if Settings.disableSSIDList.isEmpty { return } + if locationManager.authorizationStatus == .notDetermined { + Logger.log("request location permission") + locationManager.desiredAccuracy = kCLLocationAccuracyReduced + locationManager.delegate = self + locationManager.requestAlwaysAuthorization() + } else if locationManager.authorizationStatus != .authorized { + if showNoticeOnNotPermission { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.openLocationSettings() + } + } + } + } + } + func update() { if shouldSuspend() { ConfigManager.shared.proxyShouldPaused.accept(true) @@ -49,10 +94,52 @@ class SSIDSuspendTool { } func shouldSuspend() -> Bool { - if let currentSSID = NetworkChangeNotifier.getCurrentSSID() { + if let currentSSID = getCurrentSSID() { return Settings.disableSSIDList.contains(currentSSID) } else { return false } } + + private func getCurrentSSID() -> String? { + if #available(macOS 14, *) { + if locationManager.authorizationStatus != .authorized { + let info = Command(cmd: "/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport", args: ["-I"]).run() + let ssid = info.components(separatedBy: "\n") + .lazy + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .first { $0.starts(with: "SSID:") }? + .components(separatedBy: ":") + .last?.trimmingCharacters(in: .whitespacesAndNewlines) + return ssid + } + } + return CWWiFiClient.shared().interface()?.ssid() + } + + private func openLocationSettings() { + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Location")!) + NSApp.activate(ignoringOtherApps: true) + NSAlert.alert(with: NSLocalizedString("Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services.", comment: "")) + } +} + +extension SSIDSuspendTool: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + Logger.log("Location status: \(status.rawValue)") + if status != .authorized, showNoticeOnNotPermission { + openLocationSettings() + } + showNoticeOnNotPermission = false + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {} + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {} +} + +extension SSIDSuspendTool: CWEventDelegate { + func ssidDidChangeForWiFiInterface(withName interfaceName: String) { + ssidChangePublisher.onNext(interfaceName) + } } diff --git a/ClashX/Info.plist b/ClashX/Info.plist index b7c7ab4a0..ecd93338e 100644 --- a/ClashX/Info.plist +++ b/ClashX/Info.plist @@ -2,6 +2,10 @@ + NSLocationAlwaysAndWhenInUseUsageDescription + ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. + NSLocationWhenInUseUsageDescription + ClashX use location info to detect your current WiFi network SSID name and provide the auto suspend services. BETA CFBundleDevelopmentRegion diff --git a/ClashX/Models/ClashProxy.swift b/ClashX/Models/ClashProxy.swift index 13da3a63a..c1b9e9123 100644 --- a/ClashX/Models/ClashProxy.swift +++ b/ClashX/Models/ClashProxy.swift @@ -125,12 +125,8 @@ class ClashProxy: Codable { var proxys = [SpeedtestAbleItem]() for proxy in allProxys { if let p = resp.proxiesMap[proxy] { - if !ClashProxyType.isProxyGroup(p) { - if let provider = p.enclosingProvider { - proxys.append(.provider(name: p.name, provider: provider.name)) - } else { - proxys.append(.proxy(name: p.name)) - } + if let provider = p.enclosingProvider { + proxys.append(.provider(name: p.name, provider: provider.name)) } else { proxys.append(.group(name: p.name)) } diff --git a/ClashX/Support Files/en.lproj/Localizable.strings b/ClashX/Support Files/en.lproj/Localizable.strings index 930714f00..ff70478e5 100644 --- a/ClashX/Support Files/en.lproj/Localizable.strings +++ b/ClashX/Support Files/en.lproj/Localizable.strings @@ -200,6 +200,9 @@ /* No comment provided by engineer. */ "Open github release page to download " = "Open github release page to download "; +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services."; + /* No comment provided by engineer. */ "Ports Open Fail, Please try to restart ClashX" = "Ports Open Fail, Please try to restart ClashX"; diff --git a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings index dba729454..2bde81835 100644 --- a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings @@ -205,6 +205,9 @@ /* No comment provided by engineer. */ "Open github release page to download " = "打开 GitHub Release 下载 "; +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "请允许ClashX使用定位服务来获取当前所连接的WiFi名称从而提供按需暂停服务。"; + /* No comment provided by engineer. */ "Ports Open Fail, Please try to restart ClashX" = "端口打开失败,请尝试重启ClashX"; diff --git a/ClashX/Support Files/zh-Hant.lproj/Localizable.strings b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings index 5e3089c1f..83bf3f97a 100644 --- a/ClashX/Support Files/zh-Hant.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings @@ -172,6 +172,9 @@ /* No comment provided by engineer. */ "Open System Login Item Setting" = "打開系統登錄項設定"; +/* No comment provided by engineer. */ +"Please enable the location service for ClashX to detect your current WiFi network's SSID name and provide the auto-suspend services." = "請允許 ClashX 使用定位服務,以獲取您目前連接的 WiFi 名稱,並提供按需暫停服務。"; + /* No comment provided by engineer. */ "Ports Open Fail, Please try to restart ClashX" = "端口打開失敗,請嘗試重啟ClashX"; diff --git a/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift b/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift index 7b34d8277..4c1eb26cd 100644 --- a/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift +++ b/ClashX/ViewControllers/Settings/GeneralSettingViewController.swift @@ -134,6 +134,9 @@ class GeneralSettingViewController: NSViewController { if url.isUrlVaild() || url.isEmpty { Settings.benchMarkUrl = url } + SSIDSuspendTool.shared.showNoticeOnNotPermission = true + SSIDSuspendTool.shared.requestPermissionIfNeed() + SSIDSuspendTool.shared.update() } @IBAction func actionResetIgnoreList(_ sender: Any) {