diff --git a/ClashX.xcodeproj/project.pbxproj b/ClashX.xcodeproj/project.pbxproj index 40e147c..d33b288 100644 --- a/ClashX.xcodeproj/project.pbxproj +++ b/ClashX.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 495BFB8821919B9800C8779D /* RemoteConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */; }; 4960A6DB2136529200B940C9 /* JSBridgeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4960A6DA2136529200B940C9 /* JSBridgeHandler.swift */; }; 496322222AA5D89E00854231 /* UpdateExternalResourceAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */; }; - 496322242AA5D91200854231 /* BaseAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496322232AA5D91200854231 /* BaseAction.swift */; }; 4966E9E32118153A00A391FB /* NSUserNotificationCenter+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4966E9E22118153A00A391FB /* NSUserNotificationCenter+Extension.swift */; }; 4969E73D2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4969E73F2A5E3CB20012E005 /* ConnectionDetailInfoGeneralView.xib */; }; 496BDEE021196F1E00C5207F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 496BDEDF21196F1E00C5207F /* Logger.swift */; }; @@ -48,6 +47,7 @@ 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 */; }; + 49870ADB2AA75DC7002B106B /* TerminalCleanUpAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */; }; 4989F98E20D0AE990001E564 /* sampleConfig.yaml in Resources */ = {isa = PBXBuildFile; fileRef = 4989F98D20D0AE990001E564 /* sampleConfig.yaml */; }; 498BC2532929CC2A00CA8084 /* SettingTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498BC2522929CC2A00CA8084 /* SettingTabViewController.swift */; }; 498BC2552929CCAE00CA8084 /* GeneralSettingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 498BC2542929CCAE00CA8084 /* GeneralSettingViewController.swift */; }; @@ -191,7 +191,6 @@ 495BFB8721919B9800C8779D /* RemoteConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteConfigManager.swift; sourceTree = ""; }; 4960A6DA2136529200B940C9 /* JSBridgeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSBridgeHandler.swift; sourceTree = ""; }; 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExternalResourceAction.swift; sourceTree = ""; }; - 496322232AA5D91200854231 /* BaseAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAction.swift; sourceTree = ""; }; 4966E9E22118153A00A391FB /* NSUserNotificationCenter+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserNotificationCenter+Extension.swift"; sourceTree = ""; }; 4969E73E2A5E3CB20012E005 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ConnectionDetailInfoGeneralView.xib; sourceTree = ""; }; 4969E7412A5E3CB80012E005 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ConnectionDetailInfoGeneralView.strings"; sourceTree = ""; }; @@ -209,6 +208,7 @@ 4981C88E216BAE4D008CC14A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 4982F51E2344A216008804B0 /* Cgo+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Cgo+Convert.swift"; sourceTree = ""; }; 49862F9F218418C600A1D5EC /* ClashRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashRule.swift; sourceTree = ""; }; + 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCleanUpAction.swift; sourceTree = ""; }; 498960722340F21C00AFB7EC /* com.west2online.ClashX.ProxyConfigHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = com.west2online.ClashX.ProxyConfigHelper.entitlements; sourceTree = ""; }; 4989F98D20D0AE990001E564 /* sampleConfig.yaml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = sampleConfig.yaml; sourceTree = ""; }; 498BC2522929CC2A00CA8084 /* SettingTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingTabViewController.swift; sourceTree = ""; }; @@ -430,7 +430,7 @@ isa = PBXGroup; children = ( 496322212AA5D89E00854231 /* UpdateExternalResourceAction.swift */, - 496322232AA5D91200854231 /* BaseAction.swift */, + 49870ADA2AA75DC7002B106B /* TerminalCleanUpAction.swift */, ); path = Actions; sourceTree = ""; @@ -910,7 +910,6 @@ 499A485522ED707300F6C675 /* RemoteConfigViewController.swift in Sources */, 49D6A45229AEEC15006487EF /* StatusItemTool.swift in Sources */, 49CF3B5C20CE8068001EBF94 /* ClashResourceManager.swift in Sources */, - 496322242AA5D91200854231 /* BaseAction.swift in Sources */, 4952C3D02117027C004A4FA8 /* ConfigFileManager.swift in Sources */, 49BC061C212931F4005A0FE7 /* AboutViewController.swift in Sources */, 4949D154213242F600EF85E6 /* Paths.swift in Sources */, @@ -951,6 +950,7 @@ 492C4871210EF62E004554A0 /* ClashConfig.swift in Sources */, 492C4869210EE6B9004554A0 /* ApiRequest.swift in Sources */, 49CF3B6520CEE06C001EBF94 /* ConfigManager.swift in Sources */, + 49870ADB2AA75DC7002B106B /* TerminalCleanUpAction.swift in Sources */, F9E754D0239CC21F00CEE7CC /* WebPortalManager.swift in Sources */, 495BFB8821919B9800C8779D /* RemoteConfigManager.swift in Sources */, 4982F51F2344A216008804B0 /* Cgo+Convert.swift in Sources */, diff --git a/ClashX/Actions/BaseAction.swift b/ClashX/Actions/BaseAction.swift deleted file mode 100644 index 50274a1..0000000 --- a/ClashX/Actions/BaseAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// BaseAction.swift -// ClashX -// -// Created by yicheng on 2023/9/4. -// Copyright © 2023 west2online. All rights reserved. -// - -protocol BaseStaticAction { - static func run() -} diff --git a/ClashX/Actions/TerminalCleanUpAction.swift b/ClashX/Actions/TerminalCleanUpAction.swift new file mode 100644 index 0000000..0380645 --- /dev/null +++ b/ClashX/Actions/TerminalCleanUpAction.swift @@ -0,0 +1,80 @@ +// +// TerminalCleanUpAction.swift +// ClashX +// +// Created by yicheng on 2023/9/5. +// Copyright © 2023 west2online. All rights reserved. +// + +import AppKit +import Foundation +import RxSwift + +enum TerminalConfirmAction { + static func run() -> NSApplication.TerminateReply { + guard confirmAction() else { + return .terminateCancel + } + let group = DispatchGroup() + var shouldWait = false + + if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || + NetworkChangeNotifier.hasInterfaceProxySetToClash() { + Logger.log("ClashX quit need clean proxy setting") + shouldWait = true + group.enter() + + SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) { + group.leave() + } + } + + if !shouldWait { + Logger.log("ClashX quit without clean waiting") + return .terminateNow + } + + if let statusItem = AppDelegate.shared.statusItem, statusItem.menu != nil { + statusItem.menu = nil + } + AppDelegate.shared.disposeBag = DisposeBag() + + DispatchQueue.global(qos: .default).async { + let res = group.wait(timeout: .now() + 5) + switch res { + case .success: + Logger.log("ClashX quit after clean up finish") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + NSApp.reply(toApplicationShouldTerminate: true) + } + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + NSApp.reply(toApplicationShouldTerminate: true) + } + case .timedOut: + Logger.log("ClashX quit after clean up timeout") + DispatchQueue.main.async { + NSApp.reply(toApplicationShouldTerminate: true) + } + DispatchQueue.global().asyncAfter(deadline: .now() + 1) { + NSApp.reply(toApplicationShouldTerminate: true) + } + } + } + + Logger.log("ClashX quit wait for clean up") + return .terminateLater + } + + static func confirmAction() -> Bool { + if NSApp.activationPolicy() == .regular { + let alert = NSAlert() + alert.messageText = NSLocalizedString("Quit ClashX?", comment: "") + alert.informativeText = NSLocalizedString("The active connections will be interrupted.", comment: "") + alert.alertStyle = .informational + alert.addButton(withTitle: NSLocalizedString("Quit", comment: "")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "")) + return alert.runModal() == .alertFirstButtonReturn + } + return true + } +} diff --git a/ClashX/Actions/UpdateExternalResourceAction.swift b/ClashX/Actions/UpdateExternalResourceAction.swift index 09e6423..11eff05 100644 --- a/ClashX/Actions/UpdateExternalResourceAction.swift +++ b/ClashX/Actions/UpdateExternalResourceAction.swift @@ -7,7 +7,7 @@ // import Foundation -enum UpdateExternalResourceAction: BaseStaticAction { +enum UpdateExternalResourceAction { static func run() { ApiRequest.requestExternalProviderNames { provider in let group = DispatchGroup() diff --git a/ClashX/AppDelegate.swift b/ClashX/AppDelegate.swift index e51d8b9..7576f83 100644 --- a/ClashX/AppDelegate.swift +++ b/ClashX/AppDelegate.swift @@ -21,7 +21,7 @@ let statusItemLengthWithSpeed: CGFloat = 72 @main class AppDelegate: NSObject, NSApplicationDelegate { - var statusItem: NSStatusItem! + private(set) var statusItem: NSStatusItem! @IBOutlet var checkForUpdateMenuItem: NSMenuItem! @IBOutlet var statusMenu: NSMenu! @@ -160,54 +160,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { - let group = DispatchGroup() - var shouldWait = false - - if ConfigManager.shared.proxyPortAutoSet && !ConfigManager.shared.isProxySetByOtherVariable.value || NetworkChangeNotifier.isCurrentSystemSetToClash(looser: true) || - NetworkChangeNotifier.hasInterfaceProxySetToClash() { - Logger.log("ClashX quit need clean proxy setting") - shouldWait = true - group.enter() - - SystemProxyManager.shared.disableProxy(forceDisable: ConfigManager.shared.isProxySetByOtherVariable.value) { - group.leave() - } - } - - if !shouldWait { - Logger.log("ClashX quit without clean waiting") - return .terminateNow - } - - if statusItem != nil, statusItem.menu != nil { - statusItem.menu = nil - } - disposeBag = DisposeBag() - - DispatchQueue.global(qos: .default).async { - let res = group.wait(timeout: .now() + 5) - switch res { - case .success: - Logger.log("ClashX quit after clean up finish") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - NSApp.reply(toApplicationShouldTerminate: true) - } - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - NSApp.reply(toApplicationShouldTerminate: true) - } - case .timedOut: - Logger.log("ClashX quit after clean up timeout") - DispatchQueue.main.async { - NSApp.reply(toApplicationShouldTerminate: true) - } - DispatchQueue.global().asyncAfter(deadline: .now() + 1) { - NSApp.reply(toApplicationShouldTerminate: true) - } - } - } - - Logger.log("ClashX quit wait for clean up") - return .terminateLater + return TerminalConfirmAction.run() } func applicationWillTerminate(_ aNotification: Notification) { @@ -758,6 +711,10 @@ extension AppDelegate { @IBAction func actionQuit(_ sender: Any) { NSApplication.shared.terminate(self) } + + @IBAction func actionMoreSetting(_ sender: Any) { + ClashWindowController.create().showWindow(sender) + } } // MARK: Streaming Info diff --git a/ClashX/Base.lproj/Main.storyboard b/ClashX/Base.lproj/Main.storyboard index 1e84c57..9985b2b 100644 --- a/ClashX/Base.lproj/Main.storyboard +++ b/ClashX/Base.lproj/Main.storyboard @@ -9,7 +9,7 @@ - + @@ -529,6 +529,13 @@ + + + + + + + @@ -864,8 +871,7 @@ - - + diff --git a/ClashX/ClashWindowController.swift b/ClashX/ClashWindowController.swift index 1baec81..a983af7 100644 --- a/ClashX/ClashWindowController.swift +++ b/ClashX/ClashWindowController.swift @@ -24,7 +24,8 @@ private class ClashWindowsRecorder { class ClashWindowController: NSWindowController, NSWindowDelegate { var onWindowClose: (() -> Void)? - var lastSize: CGSize? { + private var fromCache = false + private var lastSize: CGSize? { get { if let str = UserDefaults.standard.value(forKey: "lastSize.\(T.className())") as? String { return NSSizeFromString(str) as CGSize @@ -40,12 +41,23 @@ class ClashWindowController: NSWindowController, NSWindowDe static func create() -> NSWindowController { if let wc = ClashWindowsRecorder.shared.windowControllers.first(where: { $0 is Self }) { + (wc as? ClashWindowController)?.fromCache = true return wc } let win = NSWindow() let wc = ClashWindowController(window: win) - wc.contentViewController = T() + if let X = T.self as? NibLoadable.Type { + wc.contentViewController = (X.createFromNib(in: .main) as! NSViewController) + } else { + wc.contentViewController = T() + } win.titlebarAppearsTransparent = false + win.styleMask.insert(.closable) + win.styleMask.insert(.resizable) + win.styleMask.insert(.miniaturizable) + if let title = wc.contentViewController?.title { + win.title = title + } ClashWindowsRecorder.shared.windowControllers.append(wc) return wc } @@ -53,10 +65,10 @@ class ClashWindowController: NSWindowController, NSWindowDe override func showWindow(_ sender: Any?) { super.showWindow(sender) NSApp.activate(ignoringOtherApps: true) - if let lastSize = lastSize, lastSize != .zero { + if !fromCache, let lastSize = lastSize, lastSize != .zero { window?.setContentSize(lastSize) + window?.center() } - window?.center() window?.makeKeyAndOrderFront(self) window?.delegate = self } diff --git a/ClashX/Extensions/NSView+Nib.swift b/ClashX/Extensions/NSView+Nib.swift index 09bef93..492a51d 100644 --- a/ClashX/Extensions/NSView+Nib.swift +++ b/ClashX/Extensions/NSView+Nib.swift @@ -27,3 +27,15 @@ extension NibLoadable where Self: NSView { return views.last as! Self } } + +extension NibLoadable where Self: NSViewController { + static var nibName: String? { + return String(describing: Self.self) + } + + static func createFromNib(in bundle: Bundle = Bundle.main) -> Self { + guard let nibName = nibName else { fatalError() } + let sb = NSStoryboard(name: "Main", bundle: Bundle.main) + return sb.instantiateController(withIdentifier: nibName) as! Self + } +} diff --git a/ClashX/Support Files/en.lproj/Localizable.strings b/ClashX/Support Files/en.lproj/Localizable.strings index 0b02369..b3bc572 100644 --- a/ClashX/Support Files/en.lproj/Localizable.strings +++ b/ClashX/Support Files/en.lproj/Localizable.strings @@ -184,6 +184,9 @@ /* No comment provided by engineer. */ "Quit" = "Quit"; +/* No comment provided by engineer. */ +"Quit ClashX?" = "Quit ClashX?"; + /* No comment provided by engineer. */ "Recent Connections" = "Recent Connections"; @@ -256,6 +259,9 @@ /* No comment provided by engineer. */ "Testing" = "Testing"; +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "The active connections will be interrupted."; + /* No comment provided by engineer. */ "The remote config name is duplicated" = "The remote config name is duplicated"; diff --git a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings index 297d17e..77b8c6c 100644 --- a/ClashX/Support Files/zh-Hans.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hans.lproj/Localizable.strings @@ -184,6 +184,9 @@ /* No comment provided by engineer. */ "Quit" = "退出"; +/* No comment provided by engineer. */ +"Quit ClashX?" = "退出ClashX?"; + /* No comment provided by engineer. */ "Recent Connections" = "最近连接"; @@ -256,6 +259,9 @@ /* No comment provided by engineer. */ "Testing" = "测速中"; +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "活动的连接将被强制打断。"; + /* No comment provided by engineer. */ "The remote config name is duplicated" = "托管配置文件名称重复"; diff --git a/ClashX/Support Files/zh-Hant.lproj/Localizable.strings b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings index deb180d..3ef2a7d 100644 --- a/ClashX/Support Files/zh-Hant.lproj/Localizable.strings +++ b/ClashX/Support Files/zh-Hant.lproj/Localizable.strings @@ -184,6 +184,9 @@ /* No comment provided by engineer. */ "Quit" = "退出"; +/* No comment provided by engineer. */ +"Quit ClashX?" = "退出 ClashX?"; + /* No comment provided by engineer. */ "Recent Connections" = "最近連線"; @@ -256,6 +259,9 @@ /* No comment provided by engineer. */ "Testing" = "測試中"; +/* No comment provided by engineer. */ +"The active connections will be interrupted." = "活躍的連接將被中斷。"; + /* No comment provided by engineer. */ "The remote config name is duplicated" = "遠端配置名稱重複"; diff --git a/ClashX/ViewControllers/ClashWebViewContoller.swift b/ClashX/ViewControllers/ClashWebViewContoller.swift index 0e3ab89..63ab610 100644 --- a/ClashX/ViewControllers/ClashWebViewContoller.swift +++ b/ClashX/ViewControllers/ClashWebViewContoller.swift @@ -33,14 +33,6 @@ class ClashWebViewContoller: NSViewController { let effectView = NSVisualEffectView() - static func createWindowController() -> NSWindowController { - let sb = NSStoryboard(name: "Main", bundle: Bundle.main) - let vc = sb.instantiateController(withIdentifier: "ClashWebViewContoller") as! ClashWebViewContoller - let wc = NSWindowController(window: NSWindow()) - wc.contentViewController = vc - return wc - } - override func loadView() { view = NSView(frame: NSRect(origin: .zero, size: minSize)) } @@ -86,9 +78,6 @@ class ClashWebViewContoller: NSViewController { view.window?.isOpaque = false view.window?.backgroundColor = NSColor.clear - view.window?.styleMask.insert(.closable) - view.window?.styleMask.insert(.resizable) - view.window?.styleMask.insert(.miniaturizable) view.window?.toolbar = NSToolbar() view.window?.toolbar?.showsBaselineSeparator = false view.wantsLayer = true diff --git a/ClashX/ViewControllers/Connections/DashboardViewController.swift b/ClashX/ViewControllers/Connections/DashboardViewController.swift index 6e0ea1c..eb25e9f 100644 --- a/ClashX/ViewControllers/Connections/DashboardViewController.swift +++ b/ClashX/ViewControllers/Connections/DashboardViewController.swift @@ -51,9 +51,6 @@ class DashboardViewController: NSViewController { super.viewWillAppear() toolbar.delegate = self view.window?.toolbar = toolbar - view.window?.styleMask.insert(.closable) - view.window?.styleMask.insert(.resizable) - view.window?.styleMask.insert(.miniaturizable) view.window?.backgroundColor = NSColor.clear if #available(macOS 11.0, *) { view.window?.toolbarStyle = .unifiedCompact diff --git a/ClashX/ViewControllers/Settings/SettingTabViewController.swift b/ClashX/ViewControllers/Settings/SettingTabViewController.swift index 9613952..a931555 100644 --- a/ClashX/ViewControllers/Settings/SettingTabViewController.swift +++ b/ClashX/ViewControllers/Settings/SettingTabViewController.swift @@ -8,7 +8,7 @@ import Cocoa -class SettingTabViewController: NSTabViewController { +class SettingTabViewController: NSTabViewController, NibLoadable { override func viewDidLoad() { super.viewDidLoad() tabStyle = .toolbar diff --git a/ClashX/zh-Hans.lproj/Main.strings b/ClashX/zh-Hans.lproj/Main.strings index 0e4595e..b90866e 100644 --- a/ClashX/zh-Hans.lproj/Main.strings +++ b/ClashX/zh-Hans.lproj/Main.strings @@ -312,3 +312,6 @@ /* Class = "NSButtonCell"; title = "Enable IPv6"; ObjectID = "KRm-2U-T4s"; */ "KRm-2U-T4s.title" = "启用IPv6"; + +/* Class = "NSMenuItem"; title = "Quit ClashX"; ObjectID = "TsN-g8-OjG"; */ +"TsN-g8-OjG.title" = "退出 ClashX"; diff --git a/ClashX/zh-Hant.lproj/Main.strings b/ClashX/zh-Hant.lproj/Main.strings index af6815e..b5100a5 100644 --- a/ClashX/zh-Hant.lproj/Main.strings +++ b/ClashX/zh-Hant.lproj/Main.strings @@ -312,3 +312,6 @@ /* Class = "NSButtonCell"; title = "Enable IPv6"; ObjectID = "KRm-2U-T4s"; */ "KRm-2U-T4s.title" = "啟用IPv6"; + +/* Class = "NSMenuItem"; title = "Quit ClashX"; ObjectID = "TsN-g8-OjG"; */ +"TsN-g8-OjG.title" = "退出 ClashX";