diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index 2649d7915..114cd99ca 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 49722FF0211F338B00650A41 /* EventStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49722FEB211F338B00650A41 /* EventStream.swift */; }; 49722FF1211F338B00650A41 /* Witness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49722FEC211F338B00650A41 /* Witness.swift */; }; 49761DA721C9497000AE13EF /* dashboard in Resources */ = {isa = PBXBuildFile; fileRef = 49761DA621C9497000AE13EF /* dashboard */; }; - 497F0DF320DE2FE50077AD41 /* Icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 497F0DF220DE2FE50077AD41 /* Icon.icns */; }; 4981C88B216BAE4A008CC14A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4981C88D216BAE4A008CC14A /* Localizable.strings */; }; 4982F51F2344A216008804B0 /* Cgo+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4982F51E2344A216008804B0 /* Cgo+Convert.swift */; }; 49862FA0218418C600A1D5EC /* ClashRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49862F9F218418C600A1D5EC /* ClashRule.swift */; }; @@ -55,6 +54,8 @@ 49D176A72355FE680093DD7B /* NetworkChangeNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A62355FE680093DD7B /* NetworkChangeNotifier.swift */; }; 49D176A9235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */; }; 49D176AB23575BB20093DD7B /* ProxyGroupMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */; }; + 6E17601124BD8E81009C0A83 /* RemoteControlManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E17601024BD8E81009C0A83 /* RemoteControlManager.swift */; }; + 6E17601324BD8E9D009C0A83 /* ExternalControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E17601224BD8E9D009C0A83 /* ExternalControlViewController.swift */; }; 6E2A2C7C2455369D007BBE7D /* ProxyDelayHistoryMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2A2C7B2455369D007BBE7D /* ProxyDelayHistoryMenu.swift */; }; 6E6B9F67245A65BE00F72A94 /* ClashStatusTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6B9F66245A65BE00F72A94 /* ClashStatusTool.swift */; }; 6E73DAF1248891410059D1C7 /* iCloudManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E73DAF0248891410059D1C7 /* iCloudManager.swift */; }; @@ -138,7 +139,6 @@ 49722FEC211F338B00650A41 /* Witness.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Witness.swift; sourceTree = ""; }; 49722FED211F338B00650A41 /* Witness.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Witness.h; sourceTree = ""; }; 49761DA621C9497000AE13EF /* dashboard */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dashboard; sourceTree = ""; }; - 497F0DF220DE2FE50077AD41 /* Icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = Icon.icns; sourceTree = ""; }; 4981C887216BAB8A008CC14A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 4981C88C216BAE4A008CC14A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 4981C88E216BAE4D008CC14A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; @@ -173,6 +173,8 @@ 49D176A8235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupSpeedTestMenuItem.swift; sourceTree = ""; }; 49D176AA23575BB20093DD7B /* ProxyGroupMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupMenuItemView.swift; sourceTree = ""; }; 5217C006C5A22A1CEA24BFC1 /* Pods-ClashX.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ClashX.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ClashX/Pods-ClashX.debug.xcconfig"; sourceTree = ""; }; + 6E17601024BD8E81009C0A83 /* RemoteControlManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlManager.swift; sourceTree = ""; }; + 6E17601224BD8E9D009C0A83 /* ExternalControlViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExternalControlViewController.swift; sourceTree = ""; }; 6E2A2C7B2455369D007BBE7D /* ProxyDelayHistoryMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyDelayHistoryMenu.swift; sourceTree = ""; }; 6E6B9F66245A65BE00F72A94 /* ClashStatusTool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClashStatusTool.swift; sourceTree = ""; }; 6E73DAF0248891410059D1C7 /* iCloudManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iCloudManager.swift; sourceTree = ""; }; @@ -271,6 +273,7 @@ 49CF3B6420CEE06C001EBF94 /* ConfigManager.swift */, 6E73DAF0248891410059D1C7 /* iCloudManager.swift */, 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */, + 6E17601024BD8E81009C0A83 /* RemoteControlManager.swift */, 4952C3BE2115C7CA004A4FA8 /* MenuItemFactory.swift */, F935B2FB23085515009E4D33 /* SystemProxyManager.swift */, F977FAAB2366790500C17F1F /* AutoUpgardeManager.swift */, @@ -358,6 +361,7 @@ children = ( 49BC061B212931F4005A0FE7 /* AboutViewController.swift */, 499976C721359F0400E7BF83 /* ClashWebViewContoller.swift */, + 6E17601224BD8E9D009C0A83 /* ExternalControlViewController.swift */, 499A485322ED707300F6C675 /* RemoteConfigViewController.swift */, ); path = ViewControllers; @@ -410,7 +414,6 @@ 49CF3B3A20CD783A001EBF94 /* Support Files */, 49CF3B2020CD7463001EBF94 /* AppDelegate.swift */, 49CF3B2420CD7465001EBF94 /* Assets.xcassets */, - 497F0DF220DE2FE50077AD41 /* Icon.icns */, 49CF3B2620CD7465001EBF94 /* Main.storyboard */, 49CF3B2920CD7465001EBF94 /* Info.plist */, 49CF3B2A20CD7465001EBF94 /* ClashX.entitlements */, @@ -572,7 +575,6 @@ 49CF3B2820CD7465001EBF94 /* Main.storyboard in Resources */, 499A485A22ED781100F6C675 /* RemoteConfigAddView.xib in Resources */, 4989F98420D02D200001E564 /* Country.mmdb in Resources */, - 497F0DF320DE2FE50077AD41 /* Icon.icns in Resources */, 4989F98E20D0AE990001E564 /* sampleConfig.yaml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -686,6 +688,7 @@ 49722FF0211F338B00650A41 /* EventStream.swift in Sources */, 499A486522EEA3FD00F6C675 /* Array+Safe.swift in Sources */, F92D0B24236BC12000575E15 /* SavedProxyModel.swift in Sources */, + 6E17601324BD8E9D009C0A83 /* ExternalControlViewController.swift in Sources */, F92D0B2A236C759100575E15 /* NSTextField+Vibrancy.swift in Sources */, F910AA24240134AF00116E95 /* ProxyGroupMenu.swift in Sources */, 4952C3BF2115C7CA004A4FA8 /* MenuItemFactory.swift in Sources */, @@ -696,6 +699,7 @@ 49B4575F244FD4D100463C39 /* PrivilegedHelperManager+Legacy.swift in Sources */, 4966E9E32118153A00A391FB /* NSUserNotificationCenter+Extension.swift in Sources */, F9E754D2239CC28D00CEE7CC /* NSAlert+Extension.swift in Sources */, + 6E17601124BD8E81009C0A83 /* RemoteControlManager.swift in Sources */, 499976C821359F0400E7BF83 /* ClashWebViewContoller.swift in Sources */, 49D176A9235614340093DD7B /* ProxyGroupSpeedTestMenuItem.swift in Sources */, F976275C23634DF8000EDEFE /* LoginServiceKit.swift in Sources */, @@ -876,11 +880,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5217C006C5A22A1CEA24BFC1 /* Pods-ClashX.debug.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development: shaoreceive@outlook.com (6VT57CBDXM)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.20.3.2; + CURRENT_PROJECT_VERSION = 1.20.4; DEVELOPMENT_TEAM = J7P29E7C2F; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -902,7 +907,7 @@ "$(PROJECT_DIR)/ClashX/goClash", ); MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.20.3.2; + MARKETING_VERSION = 1.20.4; PRODUCT_BUNDLE_IDENTIFIER = com.shaox.ClashX; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -917,11 +922,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = A1485BCE642059532D01B8BA /* Pods-ClashX.release.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development: shaoreceive@outlook.com (6VT57CBDXM)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.20.3.2; + CURRENT_PROJECT_VERSION = 1.20.4; DEVELOPMENT_TEAM = J7P29E7C2F; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -943,7 +949,7 @@ "$(PROJECT_DIR)/ClashX/goClash", ); MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.20.3.2; + MARKETING_VERSION = 1.20.4; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.shaox.ClashX; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ClashX/AppDelegate.swift b/ClashX/AppDelegate.swift index 33f3d6ce5..283d589e5 100644 --- a/ClashX/AppDelegate.swift +++ b/ClashX/AppDelegate.swift @@ -47,7 +47,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet var buildApiModeMenuitem: NSMenuItem! @IBOutlet var showProxyGroupCurrentMenuItem: NSMenuItem! @IBOutlet var copyExportCommandMenuItem: NSMenuItem! + @IBOutlet var copyExportCommandExternalMenuItem: NSMenuItem! @IBOutlet var experimentalMenu: NSMenu! + @IBOutlet var externalControlSeparator: NSMenuItem! var disposeBag = DisposeBag() var statusItemView: StatusItemView! @@ -73,6 +75,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusItemView.frame = CGRect(x: 0, y: 0, width: statusItemLengthWithSpeed, height: 22) statusMenu.delegate = self setupStatusMenuItemData() + AppVersionUtil.showUpgradeAlert() DispatchQueue.main.async { self.postFinishLaunching() } @@ -128,9 +131,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { Logger.log("ClashX quit need clean proxy setting") shouldWait = true group.enter() - let port = ConfigManager.shared.currentConfig?.port ?? 0 - let socketPort = ConfigManager.shared.currentConfig?.socketPort ?? 0 - SystemProxyManager.shared.disableProxy(port: port, socksPort: socketPort) { + SystemProxyManager.shared.disableProxy { group.leave() } } @@ -232,19 +233,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.proxyModeMenuItem.title = "\(NSLocalizedString("Proxy Mode", comment: "")) (\(config.mode.name))" - if old?.port != config.port || old?.socketPort != config.socketPort { - Logger.log("port config updated,new: \(config.port),\(config.socketPort)") + if old?.usedHttpPort != config.usedHttpPort || old?.usedSocksPort != config.usedSocksPort { + Logger.log("port config updated,new: \(config.usedHttpPort),\(config.usedSocksPort)") if ConfigManager.shared.proxyPortAutoSet { - SystemProxyManager.shared.enableProxy(port: config.port, socksPort: config.socketPort) + SystemProxyManager.shared.enableProxy(port: config.usedHttpPort, socksPort: config.usedSocksPort) } } - self.httpPortMenuItem.title = "Http Port: \(config.port)" - self.socksPortMenuItem.title = "Socks Port: \(config.socketPort)" + self.httpPortMenuItem.title = "Http Port: \(config.usedHttpPort)" + self.socksPortMenuItem.title = "Socks Port: \(config.usedSocksPort)" self.apiPortMenuItem.title = "Api Port: \(ConfigManager.shared.apiPort)" self.ipMenuItem.title = "IP: \(NetworkChangeNotifier.getPrimaryIPAddress() ?? "")" + if RemoteControlManager.selectConfig == nil { ClashStatusTool.checkPortConfig(cfg: config) + } }.disposed(by: disposeBag) } @@ -307,6 +310,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { Logger.log("proxy changed to no clashX setting: \(rawProxy)", level: .warning) NSUserNotificationCenter.default.postProxyChangeByOtherAppNotice() }.disposed(by: disposeBag) + + NotificationCenter + .default + .rx + .notification(.systemNetworkStatusIPUpdate).map({ _ in + NetworkChangeNotifier.getPrimaryIPAddress(allowIPV6: false) + }).bind { [weak self] _ in + if RemoteControlManager.selectConfig != nil { + self?.resetStreamApi() + } + }.disposed(by: disposeBag) } func updateProxyList(withMenus menus: [NSMenuItem]) { @@ -439,6 +453,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { AutoUpgardeManager.shared.setup() AutoUpgardeManager.shared.addChanelMenuItem(&experimentalMenu) updateExperimentalFeatureStatus() + RemoteControlManager.setupMenuItem(separator: externalControlSeparator) } func updateExperimentalFeatureStatus() { @@ -456,6 +471,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { SystemProxyManager.shared.disableProxy() SystemProxyManager.shared.enableProxy() } + + if RemoteControlManager.selectConfig != nil { + resetStreamApi() + } } @objc func healthHeckOnNetworkChange() { @@ -535,24 +554,22 @@ extension AppDelegate { } else { ConfigManager.shared.proxyPortAutoSet = !ConfigManager.shared.proxyPortAutoSet } - let port = ConfigManager.shared.currentConfig?.port ?? 0 - let socketPort = ConfigManager.shared.currentConfig?.socketPort ?? 0 if ConfigManager.shared.proxyPortAutoSet { if canSaveProxy { SystemProxyManager.shared.saveProxy() } - SystemProxyManager.shared.enableProxy(port: port, socksPort: socketPort) + SystemProxyManager.shared.enableProxy() } else { - SystemProxyManager.shared.disableProxy(port: port, socksPort: socketPort) + SystemProxyManager.shared.disableProxy() } } @IBAction func actionCopyExportCommand(_ sender: NSMenuItem) { let pasteboard = NSPasteboard.general pasteboard.clearContents() - let port = ConfigManager.shared.currentConfig?.port ?? 0 - let socksport = ConfigManager.shared.currentConfig?.socketPort ?? 0 + let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 + let socksport = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 let localhost = "127.0.0.1" let isLocalhostCopy = sender == copyExportCommandMenuItem let ip = isLocalhostCopy ? localhost : diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/Contents.json b/ClashX/Assets.xcassets/AppIcon.appiconset/Contents.json index 50ab7bd32..64dc11ee7 100644 --- a/ClashX/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ClashX/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,58 +1,68 @@ { "images" : [ { + "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128.png new file mode 100644 index 000000000..4c0771059 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png new file mode 100644 index 000000000..7c52ac912 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16.png new file mode 100644 index 000000000..7aefbef35 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png new file mode 100644 index 000000000..a36e64a7d Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256.png new file mode 100644 index 000000000..f72a48483 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png new file mode 100644 index 000000000..9b9097de1 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32.png new file mode 100644 index 000000000..a36e64a7d Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png new file mode 100644 index 000000000..2927ca94f Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512.png new file mode 100644 index 000000000..9b9097de1 Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ diff --git a/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png new file mode 100644 index 000000000..4842513bc Binary files /dev/null and b/ClashX/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ diff --git a/ClashX/Base.lproj/Main.storyboard b/ClashX/Base.lproj/Main.storyboard index e25d4a3b1..4124f31ad 100644 --- a/ClashX/Base.lproj/Main.storyboard +++ b/ClashX/Base.lproj/Main.storyboard @@ -178,9 +178,11 @@ + + @@ -333,6 +335,20 @@ + + + + + + + + + + + + + + @@ -851,7 +867,240 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ClashX/Basic/String+Extension.swift b/ClashX/Basic/String+Extension.swift index 25f1b88c8..0d94f3416 100644 --- a/ClashX/Basic/String+Extension.swift +++ b/ClashX/Basic/String+Extension.swift @@ -9,6 +9,7 @@ import Foundation extension String { func isUrlVaild() -> Bool { + guard count > 0 else { return false } guard let url = URL(string: self) else { return false } guard url.host != nil, diff --git a/ClashX/General/ApiRequest.swift b/ClashX/General/ApiRequest.swift index 5b2c4fae2..6281b0848 100644 --- a/ClashX/General/ApiRequest.swift +++ b/ClashX/General/ApiRequest.swift @@ -37,7 +37,7 @@ class ApiRequest { } private static func authHeader() -> HTTPHeaders { - let secret = ConfigManager.shared.apiSecret + let secret = ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret return (secret.count > 0) ? ["Authorization": "Bearer \(secret)"] : [:] } @@ -66,13 +66,22 @@ class ApiRequest { private var trafficWebSocket: WebSocket? private var loggingWebSocket: WebSocket? - private var trafficWebSocketRetryCount = 0 - private var loggingWebSocketRetryCount = 0 + private var trafficWebSocketRetryDelay: TimeInterval = 1 + private var loggingWebSocketRetryDelay: TimeInterval = 1 + private var trafficWebSocketRetryTimer: Timer? + private var loggingWebSocketRetryTimer: Timer? private var alamoFireManager: Session + static func useDirectApi() -> Bool { + if ConfigManager.shared.overrideApiURL != nil { + return false + } + return ConfigManager.builtInApiMode + } + static func requestConfig(completeHandler: @escaping ((ClashConfig) -> Void)) { - if !ConfigManager.builtInApiMode { + if !useDirectApi() { req("/configs").responseDecodable(of: ClashConfig.self) { resp in switch resp.result { @@ -115,7 +124,7 @@ class ApiRequest { let placeHolderErrorDesp = "Error occoured, Please try to fix it by restarting ClashX. " // DEV MODE: Use API - if !ConfigManager.builtInApiMode { + if !useDirectApi() { req("/configs", method: .put, parameters: ["Path": configPath], encoding: JSONEncoding.default).responseJSON { res in if res.response?.statusCode == 204 { ConfigManager.shared.isRunning = true @@ -320,22 +329,23 @@ extension ApiRequest { } func resetLogStreamApi() { - loggingWebSocketRetryCount = 0 + loggingWebSocketRetryTimer?.invalidate() + loggingWebSocketRetryTimer = nil + loggingWebSocketRetryDelay = 1 requestLog() } func resetTrafficStreamApi() { - trafficWebSocketRetryCount = 0 + trafficWebSocketRetryTimer?.invalidate() + trafficWebSocketRetryTimer = nil + trafficWebSocketRetryDelay = 1 requestTrafficInfo() } private func requestTrafficInfo() { + trafficWebSocketRetryTimer?.invalidate() + trafficWebSocketRetryTimer = nil trafficWebSocket?.disconnect(forceTimeout: 0, closeCode: 0) - trafficWebSocketRetryCount += 1 - if trafficWebSocketRetryCount > 5 { - NSUserNotificationCenter.default.postStreamApiConnectFail(api: "Traffic") - return - } let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending("/traffic"))!) @@ -348,12 +358,9 @@ extension ApiRequest { } private func requestLog() { + loggingWebSocketRetryTimer?.invalidate() + loggingWebSocketRetryTimer = nil loggingWebSocket?.disconnect() - loggingWebSocketRetryCount += 1 - if loggingWebSocketRetryCount > 5 { - NSUserNotificationCenter.default.postStreamApiConnectFail(api: "Log") - return - } let uriString = "/logs?level=".appending(ConfigManager.selectLoggingApiLevel.rawValue) let socket = WebSocket(url: URL(string: ConfigManager.apiUrl.appending(uriString))!) @@ -371,8 +378,10 @@ extension ApiRequest: WebSocketDelegate { func websocketDidConnect(socket: WebSocketClient) { guard let webSocket = socket as? WebSocket else { return } if webSocket == trafficWebSocket { + trafficWebSocketRetryDelay = 1 Logger.log("trafficWebSocket did Connect", level: .debug) } else { + loggingWebSocketRetryDelay = 1 Logger.log("loggingWebSocket did Connect", level: .debug) } } @@ -383,15 +392,28 @@ extension ApiRequest: WebSocketDelegate { } Logger.log(err.localizedDescription, level: .error) - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + guard let webSocket = socket as? WebSocket else { return } - if webSocket == self.trafficWebSocket { + + if webSocket == trafficWebSocket { Logger.log("trafficWebSocket did disconnect", level: .debug) - self.requestTrafficInfo() + trafficWebSocketRetryTimer?.invalidate() + trafficWebSocketRetryTimer = + Timer.scheduledTimer(withTimeInterval: trafficWebSocketRetryDelay, repeats: false, block: { + [weak self] _ in + if self?.trafficWebSocket?.isConnected == true { return } + self?.requestTrafficInfo() + }) + trafficWebSocketRetryDelay *= 2 } else { Logger.log("loggingWebSocket did disconnect", level: .debug) - self.requestLog() - } + loggingWebSocketRetryTimer = + Timer.scheduledTimer(withTimeInterval: loggingWebSocketRetryDelay, repeats: false, block: { + [weak self] _ in + if self?.loggingWebSocket?.isConnected == true { return } + self?.requestLog() + }) + loggingWebSocketRetryDelay *= 2 } } diff --git a/ClashX/General/Managers/ConfigManager.swift b/ClashX/General/Managers/ConfigManager.swift index 17b1ff341..6006f766d 100644 --- a/ClashX/General/Managers/ConfigManager.swift +++ b/ClashX/General/Managers/ConfigManager.swift @@ -16,6 +16,8 @@ class ConfigManager { private let disposeBag = DisposeBag() var apiPort = "8080" var apiSecret: String = "" + var overrideApiURL: URL? + var overrideSecret: String? var currentConfig: ClashConfig? { get { @@ -97,10 +99,21 @@ class ConfigManager { } static var apiUrl: String { + if let override = shared.overrideApiURL { + return override.absoluteString + } return "http://127.0.0.1:\(shared.apiPort)" } static var webSocketUrl: String { + if let override = shared.overrideApiURL, var comp = URLComponents(url: override, resolvingAgainstBaseURL: true) { + if comp.scheme == "https" { + comp.scheme = "wss" + } else { + comp.scheme = "ws" + } + return comp.url?.absoluteString ?? "" + } return "ws://127.0.0.1:\(shared.apiPort)" } diff --git a/ClashX/General/Managers/MenuItemFactory.swift b/ClashX/General/Managers/MenuItemFactory.swift index 5c7f81563..95e89b2c2 100644 --- a/ClashX/General/Managers/MenuItemFactory.swift +++ b/ClashX/General/Managers/MenuItemFactory.swift @@ -30,8 +30,8 @@ class MenuItemFactory { return } - for (name, proxy) in info?.proxiesMap ?? [:] { - NotificationCenter.default.post(name: .proxyUpdate(for: name), object: proxy, userInfo: nil) + for proxy in info?.proxies ?? [] { + NotificationCenter.default.post(name: .proxyUpdate(for: proxy.name), object: proxy, userInfo: nil) } } } diff --git a/ClashX/General/Managers/RemoteControlManager.swift b/ClashX/General/Managers/RemoteControlManager.swift new file mode 100644 index 000000000..5e9eec939 --- /dev/null +++ b/ClashX/General/Managers/RemoteControlManager.swift @@ -0,0 +1,129 @@ +// +// ClientOnlyManager.swift +// ClashX Pro +// +// Created by 称一称 on 2020/6/16. +// Copyright © 2020 west2online. All rights reserved. +// + +import Cocoa + +class RemoteControl: Codable { + let name: String + let url: String + let secret: String + let uuid: String + + init(name: String, url: String, secret: String) { + self.name = name + self.url = url + self.secret = secret + uuid = UUID().uuidString + } +} + +class RemoteControlManager { + static let shared = RemoteControlManager() + static var configs: [RemoteControl] = loadConfig() { + didSet { + if let encoded = try? JSONEncoder().encode(configs) { + UserDefaults.standard.set(encoded, forKey: "kRemoteControls") + } + updateMenuItems() + } + } + + static var selectConfig: RemoteControl? + private static var menuSeparator: NSMenuItem? + + static func loadConfig() -> [RemoteControl] { + if let savedConfigs = UserDefaults.standard.object(forKey: "kRemoteControls") as? Data { + if let loadedConfig = try? JSONDecoder().decode([RemoteControl].self, from: savedConfigs) { + return loadedConfig + } else { + assertionFailure() + return [] + } + } + return [] + } + + static func setupMenuItem(separator: NSMenuItem) { + menuSeparator = separator + updateMenuItems() + updateDropDownMenuItems() + } + + static func updateMenuItems() { + guard let separator = menuSeparator, let menu = separator.menu else { return } + let idx = menu.index(of: separator) + for _ in 0.. ExternalControlMenuItem { + return ExternalControlMenuItem(title: "None") + } +} diff --git a/ClashX/General/Managers/SystemProxyManager.swift b/ClashX/General/Managers/SystemProxyManager.swift index 0d0ff4f43..00371a943 100644 --- a/ClashX/General/Managers/SystemProxyManager.swift +++ b/ClashX/General/Managers/SystemProxyManager.swift @@ -46,8 +46,8 @@ class SystemProxyManager: NSObject { } func enableProxy() { - let port = ConfigManager.shared.currentConfig?.port ?? 0 - let socketPort = ConfigManager.shared.currentConfig?.socketPort ?? 0 + let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 + let socketPort = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 SystemProxyManager.shared.enableProxy(port: port, socksPort: socketPort) } @@ -64,10 +64,10 @@ class SystemProxyManager: NSObject { }) } - func disableProxy() { - let port = ConfigManager.shared.currentConfig?.port ?? 0 - let socketPort = ConfigManager.shared.currentConfig?.socketPort ?? 0 - SystemProxyManager.shared.disableProxy(port: port, socksPort: socketPort) + func disableProxy(complete: (() -> Void)? = nil) { + let port = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 + let socketPort = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 + SystemProxyManager.shared.disableProxy(port: port, socksPort: socketPort, complete: complete) } func disableProxy(port: Int, socksPort: Int, forceDisable: Bool = false, complete: (() -> Void)? = nil) { diff --git a/ClashX/General/Utils/AppVersionUtil.swift b/ClashX/General/Utils/AppVersionUtil.swift index bfcc13200..44465895e 100644 --- a/ClashX/General/Utils/AppVersionUtil.swift +++ b/ClashX/General/Utils/AppVersionUtil.swift @@ -40,3 +40,18 @@ class AppVersionUtil: NSObject { return shared.lastVersionNumber != currentVersion } } + +extension AppVersionUtil { + static func showUpgradeAlert() { + if hasVersionChanged && currentVersion.hasPrefix("1.30.0") && !isFirstLaunch { + let alert = NSAlert() + alert.messageText = NSLocalizedString("This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly.", comment: "") + alert.alertStyle = .informational + alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Details", comment: "")) + if alert.runModal() == .alertSecondButtonReturn { + NSWorkspace.shared.open(URL(string: "https://github.com/Dreamacro/clash/wiki/breaking-changes-in-1.0.0")!) + } + } + } +} diff --git a/ClashX/General/Utils/ClashStatusTool.swift b/ClashX/General/Utils/ClashStatusTool.swift index 2c58c84ae..402bb055f 100644 --- a/ClashX/General/Utils/ClashStatusTool.swift +++ b/ClashX/General/Utils/ClashStatusTool.swift @@ -10,9 +10,10 @@ import Cocoa class ClashStatusTool { static func checkPortConfig(cfg: ClashConfig?) { + guard ConfigManager.shared.isRunning else { return } guard let cfg = cfg else { return } - if cfg.port == 0 || cfg.socketPort == 0 { - Logger.log("checkPortConfig: \(cfg.port) \(cfg.socketPort)", level: .error) + if cfg.usedHttpPort == 0 { + Logger.log("checkPortConfig: \(cfg.mixedPort) ", level: .error) let alert = NSAlert() alert.messageText = NSLocalizedString("ClashX Start Error!", comment: "") alert.informativeText = NSLocalizedString("Ports Open Fail, Please try to restart ClashX", comment: "") diff --git a/ClashX/General/Utils/JSBridgeHandler.swift b/ClashX/General/Utils/JSBridgeHandler.swift index 807f3296b..ac0d998ab 100644 --- a/ClashX/General/Utils/JSBridgeHandler.swift +++ b/ClashX/General/Utils/JSBridgeHandler.swift @@ -79,10 +79,17 @@ class JsBridgeUtil { } bridge.registerHandler("apiInfo") { _, callback in + var host = "127.0.0.1" + var port = ConfigManager.shared.apiPort + if let override = ConfigManager.shared.overrideApiURL, + let overridedHost = override.host { + host = overridedHost + port = "\(override.port ?? 80)" + } let data = [ - "host": "127.0.0.1", - "port": ConfigManager.shared.apiPort, - "secret": ConfigManager.shared.apiSecret, + "host": host, + "port": port, + "secret": ConfigManager.shared.overrideSecret ?? ConfigManager.shared.apiSecret, ] callback?(data) } diff --git a/ClashX/General/Utils/NetworkChangeNotifier.swift b/ClashX/General/Utils/NetworkChangeNotifier.swift index e7f689213..1ef4a9003 100644 --- a/ClashX/General/Utils/NetworkChangeNotifier.swift +++ b/ClashX/General/Utils/NetworkChangeNotifier.swift @@ -80,8 +80,8 @@ class NetworkChangeNotifier { static func isCurrentSystemSetToClash() -> Bool { let (http, https, socks) = NetworkChangeNotifier.currentSystemProxySetting() - let currentPort = ConfigManager.shared.currentConfig?.port ?? 0 - let currentSocks = ConfigManager.shared.currentConfig?.socketPort ?? 0 + let currentPort = ConfigManager.shared.currentConfig?.usedHttpPort ?? 0 + let currentSocks = ConfigManager.shared.currentConfig?.usedSocksPort ?? 0 let proxySetted = http == currentPort && https == currentPort && socks == currentSocks return proxySetted diff --git a/ClashX/Icon.icns b/ClashX/Icon.icns deleted file mode 100644 index 8a662f567..000000000 Binary files a/ClashX/Icon.icns and /dev/null differ diff --git a/ClashX/Info.plist b/ClashX/Info.plist index 2a79224e0..b8be2bb15 100644 --- a/ClashX/Info.plist +++ b/ClashX/Info.plist @@ -21,8 +21,6 @@ CFBundleExecutable $(EXECUTABLE_NAME) - CFBundleIconFile - Icon CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion diff --git a/ClashX/Models/ClashConfig.swift b/ClashX/Models/ClashConfig.swift index 0de1afe17..110087212 100644 --- a/ClashX/Models/ClashConfig.swift +++ b/ClashX/Models/ClashConfig.swift @@ -33,14 +33,29 @@ enum ClashLogLevel: String, Codable { } class ClashConfig: Codable { - var port: Int - var socketPort: Int + private var port: Int + private var socksPort: Int var allowLan: Bool + var mixedPort: Int var mode: ClashProxyMode var logLevel: ClashLogLevel + var usedHttpPort: Int { + if mixedPort > 0 { + return mixedPort + } + return port + } + + var usedSocksPort: Int { + if mixedPort > 0 { + return mixedPort + } + return socksPort + } + private enum CodingKeys: String, CodingKey { - case port, socketPort = "socks-port", allowLan = "allow-lan", mode, logLevel = "log-level" + case port, socksPort = "socks-port", mixedPort = "mixed-port", allowLan = "allow-lan", mode, logLevel = "log-level" } static func fromData(_ data: Data) -> ClashConfig? { diff --git a/ClashX/Models/ClashProxy.swift b/ClashX/Models/ClashProxy.swift index bd490d959..7ca6020b1 100644 --- a/ClashX/Models/ClashProxy.swift +++ b/ClashX/Models/ClashProxy.swift @@ -145,10 +145,8 @@ class ClashProxy: Codable { } class ClashProxyResp { - // proxies without provider - let proxies: [ClashProxy] + var proxies: [ClashProxy] - // proxiesmap with provider included var proxiesMap: [ClashProxyName: ClashProxy] var enclosingProviderResp: ClashProviderResp? @@ -193,6 +191,7 @@ class ClashProxyResp { for proxy in provider.proxies { proxy.enclosingProvider = provider proxiesMap[proxy.name] = proxy + proxies.append(proxy) } } } diff --git a/ClashX/Support Files/en.lproj/Localizable.strings b/ClashX/Support Files/en.lproj/Localizable.strings index 121d898ec..6fa78c4b3 100644 --- a/ClashX/Support Files/en.lproj/Localizable.strings +++ b/ClashX/Support Files/en.lproj/Localizable.strings @@ -2,6 +2,9 @@ /* No comment provided by engineer. */ "Add a remote config" = "Add a remote config"; +/* No comment provided by engineer. */ +"Add a remote control config" = "Add a remote control config"; + /* No comment provided by engineer. */ "Apply and Quit" = "Apply and Quit"; @@ -157,3 +160,9 @@ /* No comment provided by engineer. */ "Use reload config to try reconnect." = "Use reload config to try reconnect."; + +/* No comment provided by engineer. */ +"Details" = "Details"; + +/* No comment provided by engineer. */ +"This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly." = "This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly."; diff --git a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings index 64a36b274..b16750013 100644 --- a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings @@ -2,6 +2,9 @@ /* No comment provided by engineer. */ "Add a remote config" = "添加托管配置文件"; +/* No comment provided by engineer. */ +"Add a remote control config" = "添加远程控制器配置"; + /* No comment provided by engineer. */ "Apply and Quit" = "应用并退出"; @@ -157,3 +160,9 @@ /* No comment provided by engineer. */ "Use reload config to try reconnect." = "使用重载配置文件按钮尝试重新连接"; + +/* No comment provided by engineer. */ +"Details" = "查看详情"; + +/* No comment provided by engineer. */ +"This version of ClashX contains a break change due to clash core 1.0 released. Check if your config is not working properly." = "由于Clash Core发布1.0版本,使用此版本的 ClashX 可能需要更新配置内容\n前往 https://github.com/Dreamacro/clash/wiki/breaking-changes-in-1.0.0 查看详情"; diff --git a/ClashX/ViewControllers/ClashWebViewContoller.swift b/ClashX/ViewControllers/ClashWebViewContoller.swift index 534f1807d..0c289de94 100644 --- a/ClashX/ViewControllers/ClashWebViewContoller.swift +++ b/ClashX/ViewControllers/ClashWebViewContoller.swift @@ -119,6 +119,12 @@ class ClashWebViewContoller: NSViewController { view.window?.styleMask.insert(.closable) view.window?.styleMask.insert(.resizable) view.window?.styleMask.insert(.miniaturizable) + if #available(OSX 10.13, *) { + view.window?.toolbar = NSToolbar() + view.window?.toolbar?.showsBaselineSeparator = false + view.wantsLayer = true + view.layer?.cornerRadius = 10 + } view.window?.minSize = minSize if let lastSize = lastSize, lastSize != .zero { @@ -143,7 +149,7 @@ class ClashWebViewContoller: NSViewController { func loadWebRecourses() { // defaults write com.shaox.ClashX webviewUrl "your url" - let defaultUrl = "\(ConfigManager.apiUrl)/ui/" + let defaultUrl = "http://127.0.0.1:\(ConfigManager.shared.apiPort)/ui/" let url = UserDefaults.standard.string(forKey: "webviewUrl") ?? defaultUrl if let url = URL(string: url) { webview.load(URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 0)) diff --git a/ClashX/ViewControllers/ExternalControlViewController.swift b/ClashX/ViewControllers/ExternalControlViewController.swift new file mode 100644 index 000000000..a6bae2170 --- /dev/null +++ b/ClashX/ViewControllers/ExternalControlViewController.swift @@ -0,0 +1,146 @@ +// +// ExternalControlViewController.swift +// ClashX Pro +// +// Created by 称一称 on 2020/6/16. +// Copyright © 2020 west2online. All rights reserved. +// + +import Cocoa + +class ExternalControlViewController: NSViewController { + @IBOutlet var tableView: NSTableView! + @IBOutlet var addButton: NSButton! + @IBOutlet var deleteButton: NSButton! + + override func viewDidLoad() { + super.viewDidLoad() + updateButtonStatus() + } + + func updateButtonStatus() { + let selectIdx = tableView.selectedRow + if selectIdx == -1 { + deleteButton.isEnabled = false + return + } + } + + @IBAction func actionAdd(_ sender: Any) { + let alertView = NSAlert() + alertView.addButton(withTitle: NSLocalizedString("OK", comment: "")) + alertView.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + alertView.messageText = NSLocalizedString("Add a remote control config", comment: "") + let addView = ExternalControlAddView(frame: .zero) + alertView.accessoryView = addView + let response = alertView.runModal() + guard response == .alertFirstButtonReturn else { return } + guard addView.isVaild() else { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Invalid input", comment: "") + alert.alertStyle = .warning + alert.runModal() + return + } + let model = RemoteControl(name: addView.nameField.stringValue, url: addView.urlTextField.stringValue, secret: addView.secretField.stringValue) + RemoteControlManager.configs.append(model) + tableView.reloadData() + } + + @IBAction func actionDelete(_ sender: Any) { + RemoteControlManager.configs.safeRemove(at: tableView.selectedRow) + tableView.reloadData() + } +} + +extension ExternalControlViewController: NSTableViewDataSource, NSTableViewDelegate { + func numberOfRows(in tableView: NSTableView) -> Int { + return RemoteControlManager.configs.count + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard let config = RemoteControlManager.configs[safe: row] else { return nil } + + func setupCell(withIdentifier: String, string: String, textFieldtag: Int = 1) -> NSView? { + let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: withIdentifier), owner: nil) + if let textField = cell?.viewWithTag(1) as? NSTextField { + textField.stringValue = string + } else { + assertionFailure() + } + + return cell + } + + switch tableColumn?.identifier.rawValue ?? "" { + case "url": + return setupCell(withIdentifier: "urlCell", string: config.url) + case "secret": + return setupCell(withIdentifier: "secretCell", string: config.secret) + case "name": + return setupCell(withIdentifier: "nameCell", string: config.name) + default: assertionFailure() + } + return nil + } +} + +class ExternalControlAddView: NSView { + let urlTextField = NSTextField() + let secretField = NSTextField() + let nameField = NSTextField() + + let urlLabel = NSTextField(labelWithString: "Api URL:") + let nameLabel = NSTextField(labelWithString: "Name:") + let secretLabel = NSTextField(labelWithString: "Secret:") + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + frame = NSRect(x: 0, y: 0, width: 300, height: 85) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupView() { + addSubview(urlTextField) + addSubview(secretField) + addSubview(urlLabel) + addSubview(secretLabel) + addSubview(nameField) + addSubview(nameLabel) + urlTextField.translatesAutoresizingMaskIntoConstraints = false + secretField.translatesAutoresizingMaskIntoConstraints = false + urlLabel.translatesAutoresizingMaskIntoConstraints = false + secretLabel.translatesAutoresizingMaskIntoConstraints = false + nameField.translatesAutoresizingMaskIntoConstraints = false + nameLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + secretField.trailingAnchor.constraint(equalTo: trailingAnchor), + urlTextField.trailingAnchor.constraint(equalTo: trailingAnchor), + nameField.trailingAnchor.constraint(equalTo: trailingAnchor), + urlTextField.topAnchor.constraint(equalTo: topAnchor), + urlTextField.leadingAnchor.constraint(equalTo: secretField.leadingAnchor), + urlTextField.bottomAnchor.constraint(equalTo: secretField.topAnchor, constant: -10), + nameField.topAnchor.constraint(equalTo: secretField.bottomAnchor, constant: 10), + urlLabel.centerYAnchor.constraint(equalTo: urlTextField.centerYAnchor), + secretLabel.centerYAnchor.constraint(equalTo: secretField.centerYAnchor), + urlTextField.leadingAnchor.constraint(equalTo: urlLabel.trailingAnchor, constant: 5), + secretField.leadingAnchor.constraint(equalTo: secretLabel.trailingAnchor, constant: 5), + urlLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 0), + secretLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 0), + urlTextField.widthAnchor.constraint(equalToConstant: 230), + nameField.leadingAnchor.constraint(equalTo: urlTextField.leadingAnchor), + nameLabel.centerYAnchor.constraint(equalTo: nameField.centerYAnchor), + nameLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), + nameField.leadingAnchor.constraint(equalTo: nameLabel.trailingAnchor, constant: 5), + ]) + } + + func isVaild() -> Bool { + return urlTextField.stringValue.isUrlVaild() && nameLabel.stringValue.count > 0 + } +} diff --git a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift index bc80b8df5..d729c9f44 100644 --- a/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift +++ b/ClashX/Views/ProxyGroupSpeedTestMenuItem.swift @@ -111,22 +111,26 @@ fileprivate class ProxyGroupSpeedTestMenuItemView: MenuItemBaseView { } } - if providers.count > 0 { + label.stringValue = NSLocalizedString("Testing", comment: "") + enclosingMenuItem?.isEnabled = false + setNeedsDisplay() + for provider in providers { - ApiRequest.healthCheck(proxy: provider) + testGroup.enter() + + ApiRequest.healthCheck(proxy: provider) { + testGroup.leave() + } } - enclosingMenuItem?.menu?.cancelTracking() - } else { - label.stringValue = NSLocalizedString("Testing", comment: "") - enclosingMenuItem?.isEnabled = false - setNeedsDisplay() testGroup.notify(queue: .main) { [weak self] in guard let self = self, let menu = self.enclosingMenuItem else { return } self.label.stringValue = menu.title menu.isEnabled = true self.setNeedsDisplay() + if providers.count > 0 { + MenuItemFactory.refreshExistingMenuItems() } } } diff --git a/ClashX/Views/ProxyMenuItem.swift b/ClashX/Views/ProxyMenuItem.swift index 00754d394..18bf61a00 100644 --- a/ClashX/Views/ProxyMenuItem.swift +++ b/ClashX/Views/ProxyMenuItem.swift @@ -76,6 +76,7 @@ class ProxyMenuItem: NSMenuItem { @objc private func proxyGroupInfoUpdate(note: Notification) { guard let group = note.object as? ClashProxy else { return } + guard ClashProxyType.isProxyGroup(group) else { return } let selected = group.now == proxyName updateSelected(selected) } diff --git a/ClashX/goClash/main.go b/ClashX/goClash/main.go index cee3203bb..0d9586311 100644 --- a/ClashX/goClash/main.go +++ b/ClashX/goClash/main.go @@ -23,7 +23,7 @@ func isAddrValid(addr string) bool { v := comps[len(comps)-1] if port, err := strconv.Atoi(v); err == nil { if port > 0 && port < 65535 { - return true + return checkPortAvailable(port) } } } @@ -54,6 +54,27 @@ func parseDefaultConfigThenStart(checkPort, allowLan bool) (*config.Config, erro if err != nil { return nil, err } + if cfg.General.MixedPort == 0 { + if cfg.General.Port > 0 { + cfg.General.MixedPort = cfg.General.Port + cfg.General.Port = 0 + } else if cfg.General.SocksPort > 0 { + cfg.General.MixedPort = cfg.General.SocksPort + cfg.General.SocksPort = 0 + } else { + cfg.General.MixedPort = 7890 + } + + if cfg.General.SocksPort == cfg.General.MixedPort { + cfg.General.SocksPort = 0 + } + + if cfg.General.Port == cfg.General.MixedPort { + cfg.General.Port = 0 + } + + } + if checkPort { if !isAddrValid(cfg.General.ExternalController) { port, err := freeport.GetFreePort() @@ -64,18 +85,12 @@ func parseDefaultConfigThenStart(checkPort, allowLan bool) (*config.Config, erro cfg.General.Secret = "" } cfg.General.AllowLan = allowLan - } - if !checkPortAvailable(cfg.General.Port) { + if !checkPortAvailable(cfg.General.MixedPort) { if port, err := freeport.GetFreePort(); err == nil { - cfg.General.Port = port + cfg.General.MixedPort = port } } - - if !checkPortAvailable(cfg.General.SocksPort) { - if port, err := freeport.GetFreePort(); err == nil { - cfg.General.SocksPort = port - } } go route.Start(cfg.General.ExternalController, cfg.General.Secret) @@ -101,7 +116,7 @@ func verifyClashConfig(content *C.char) *C.char { //export run func run(checkConfig, allowLan bool) *C.char { - cfg, err := parseDefaultConfigThenStart(checkConfig,allowLan) + cfg, err := parseDefaultConfigThenStart(checkConfig, allowLan) if err != nil { return C.CString(err.Error()) } diff --git a/ClashX/zh-Hans.lproj/Main.strings b/ClashX/zh-Hans.lproj/Main.strings index 237eafd8f..9071f6b54 100644 --- a/ClashX/zh-Hans.lproj/Main.strings +++ b/ClashX/zh-Hans.lproj/Main.strings @@ -160,3 +160,30 @@ /* Class = "NSMenuItem"; title = "Copy shell command (External IP)"; ObjectID = "7wl-vK-5JO"; */ "7wl-vK-5JO.title" = "复制终端代理命令(外部IP)"; + +/* Class = "NSTableColumn"; headerCell.title = "Api Secret"; ObjectID = "5hn-k8-CWe"; */ +"5hn-k8-CWe.headerCell.title" = "Api Secret"; + +/* Class = "NSTextFieldCell"; title = "External Controls"; ObjectID = "9gE-NX-2wJ"; */ +"9gE-NX-2wJ.title" = "远程控制器"; + +/* Class = "NSButtonCell"; title = "Delete"; ObjectID = "B2w-4r-5Kh"; */ +"B2w-4r-5Kh.title" = "删除"; + +/* Class = "NSButtonCell"; title = "Add"; ObjectID = "ZcF-10-jsl"; */ +"ZcF-10-jsl.title" = "添加"; + +/* Class = "NSViewController"; title = "External Manager"; ObjectID = "s6y-wL-pnr"; */ +"s6y-wL-pnr.title" = "远程控制器"; + +/* Class = "NSTableColumn"; headerCell.title = "Api Url"; ObjectID = "yO6-uZ-IRv"; */ +"yO6-uZ-IRv.headerCell.title" = "Api Url"; + +/* Class = "NSMenu"; title = "Remote Controller"; ObjectID = "1He-Eq-fSy"; */ +"1He-Eq-fSy.title" = "远程控制"; + +/* Class = "NSMenuItem"; title = "Remote Controller"; ObjectID = "BRR-WK-aeP"; */ +"BRR-WK-aeP.title" = "远程控制"; + +/* Class = "NSMenuItem"; title = " Manage"; ObjectID = "hlb-KQ-Fdr"; */ +"hlb-KQ-Fdr.title" = "管理"; diff --git a/Podfile.lock b/Podfile.lock index e336a2d73..6b715d7f1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -71,4 +71,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d3f4f5683423e593e4fca7cc5adb1846dfe66b1a -COCOAPODS: 1.9.1 +COCOAPODS: 1.9.3