diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 2aab534..53a7123 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1925,6 +1925,9 @@ } } } + }, + "Open Settings" : { + }, "Other methods" : { "localizations" : { @@ -2877,6 +2880,16 @@ "URL" : { "shouldTranslate" : false }, + "URLTest" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL 测试" + } + } + } + }, "User" : { "localizations" : { "zh-Hans" : { @@ -2969,4 +2982,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/MacLibrary/StatusBarController.swift b/MacLibrary/StatusBarController.swift index 6c38d33..c4b779a 100644 --- a/MacLibrary/StatusBarController.swift +++ b/MacLibrary/StatusBarController.swift @@ -16,8 +16,14 @@ public class StatusBarController: NSObject, NSMenuDelegate { private var menu: NSMenu? private var headerItem: NSMenuItem? private var headerView: StatusBarHeaderView? + private var urlTestAllItem: NSMenuItem? + private var urlTestAllView: StatusBarURLTestView? + private var urlTestGroupViews: [String: StatusBarURLTestView] = [:] private var groupsItem: NSMenuItem? private var profilesItem: NSMenuItem? + private var currentGroups: [LibboxOutboundGroup] = [] + private var isURLTestingAll = false + private var urlTestingGroups = Set() public init(environments: ExtensionEnvironments) { self.environments = environments @@ -71,6 +77,12 @@ public class StatusBarController: NSObject, NSMenuDelegate { } menu = nil headerView = nil + urlTestAllView = nil + urlTestAllItem = nil + urlTestGroupViews.removeAll() + currentGroups = [] + isURLTestingAll = false + urlTestingGroups.removeAll() commandClient?.disconnect() commandClient = nil } @@ -162,6 +174,7 @@ public class StatusBarController: NSObject, NSMenuDelegate { if isConnected { environments.commandClient.connect() } + updateURLTestAvailability() } private func loadProfiles() async { @@ -198,19 +211,41 @@ public class StatusBarController: NSObject, NSMenuDelegate { } private func updateGroupsMenu(_ groups: [LibboxOutboundGroup]?) { + currentGroups = groups ?? [] guard let submenu = groupsItem?.submenu else { return } submenu.removeAllItems() + urlTestGroupViews.removeAll() - guard let groups else { return } + let groups = currentGroups let selectableGroups = groups.filter(\.selectable) if selectableGroups.isEmpty { groupsItem?.isHidden = true + updateURLTestAvailability() return } groupsItem?.isHidden = environments.extensionProfile?.status.isConnected != true + let urlTestAllView = StatusBarURLTestView(title: NSLocalizedString("URLTest", comment: "")) { [weak self] in + self?.performURLTestForAllGroups() + } + let urlTestAllItem = NSMenuItem() + urlTestAllItem.view = urlTestAllView + self.urlTestAllView = urlTestAllView + self.urlTestAllItem = urlTestAllItem + submenu.addItem(urlTestAllItem) + + let closeAllItem = NSMenuItem( + title: NSLocalizedString("Close All Connections", comment: ""), + action: #selector(closeAllConnections), + keyEquivalent: "" + ) + closeAllItem.target = self + submenu.addItem(closeAllItem) + + submenu.addItem(NSMenuItem.separator()) + let font = NSFont.menuFont(ofSize: 0) let attrs: [NSAttributedString.Key: Any] = [.font: font] @@ -218,6 +253,15 @@ public class StatusBarController: NSObject, NSMenuDelegate { let groupItem = NSMenuItem(title: group.tag, action: nil, keyEquivalent: "") let groupSubmenu = NSMenu() + let groupURLTestView = StatusBarURLTestView(title: NSLocalizedString("URLTest", comment: "")) { [weak self] in + self?.performURLTestForGroup(group.tag) + } + let groupURLTestItem = NSMenuItem() + groupURLTestItem.view = groupURLTestView + groupSubmenu.addItem(groupURLTestItem) + groupSubmenu.addItem(NSMenuItem.separator()) + urlTestGroupViews[group.tag] = groupURLTestView + var outboundData: [(LibboxOutboundGroupItem, NSMenuItem)] = [] var maxTagWidth: CGFloat = 0 var maxDelayWidth: CGFloat = 0 @@ -275,6 +319,8 @@ public class StatusBarController: NSObject, NSMenuDelegate { groupItem.submenu = groupSubmenu submenu.addItem(groupItem) } + + updateURLTestAvailability() } @objc private func selectProfile(_ sender: NSMenuItem) { @@ -308,6 +354,14 @@ public class StatusBarController: NSObject, NSMenuDelegate { } } + @objc private func closeAllConnections() { + do { + try LibboxNewStandaloneCommandClient()!.closeConnections() + } catch { + showAlert(error: error) + } + } + @objc private func openApp() { NSApp.setActivationPolicy(.regular) if let window = NSApp.windows.first(where: { $0.identifier?.rawValue == "main" }) { @@ -365,6 +419,77 @@ public class StatusBarController: NSObject, NSMenuDelegate { alert.runModal() } + private func updateURLTestAvailability() { + let isConnected = environments.extensionProfile?.status.isConnected == true + let hasGroups = currentGroups.contains(where: \.selectable) + let isAnyGroupTesting = !urlTestingGroups.isEmpty + let allEnabled = isConnected && hasGroups && !isURLTestingAll && !isAnyGroupTesting + urlTestAllView?.setLoading(isURLTestingAll) + urlTestAllView?.setEnabled(allEnabled) + urlTestAllItem?.isHidden = !isConnected || !hasGroups + + for (tag, view) in urlTestGroupViews { + let isGroupTesting = urlTestingGroups.contains(tag) + view.setLoading(isGroupTesting) + view.setEnabled(isConnected && !isURLTestingAll && !isGroupTesting) + } + } + + private func performURLTestForAllGroups() { + let groups = currentGroups.filter(\.selectable) + guard !groups.isEmpty else { return } + guard environments.extensionProfile?.status.isConnected == true else { return } + guard !isURLTestingAll else { return } + setURLTestAllRunning(true) + + Task { + do { + let client = LibboxNewStandaloneCommandClient()! + for group in groups { + try await client.urlTest(group.tag) + } + } catch { + await showAlert(error: error) + } + await MainActor.run { + self.setURLTestAllRunning(false) + } + } + } + + private func performURLTestForGroup(_ tag: String) { + guard environments.extensionProfile?.status.isConnected == true else { return } + guard !isURLTestingAll else { return } + guard !urlTestingGroups.contains(tag) else { return } + setURLTestGroupRunning(tag, true) + + Task { + do { + let client = LibboxNewStandaloneCommandClient()! + try await client.urlTest(tag) + } catch { + await showAlert(error: error) + } + await MainActor.run { + self.setURLTestGroupRunning(tag, false) + } + } + } + + private func setURLTestAllRunning(_ running: Bool) { + isURLTestingAll = running + updateURLTestAvailability() + } + + private func setURLTestGroupRunning(_ tag: String, _ running: Bool) { + if running { + urlTestingGroups.insert(tag) + } else { + urlTestingGroups.remove(tag) + } + updateURLTestAvailability() + } + // MARK: - NSMenuDelegate public func menuWillOpen(_: NSMenu) { @@ -375,6 +500,77 @@ public class StatusBarController: NSObject, NSMenuDelegate { } } +// MARK: - StatusBarURLTestView + +@MainActor +private class StatusBarURLTestView: NSView { + private let button: NSButton + private let progressIndicator: NSProgressIndicator + private let action: () -> Void + private var isEnabled = true + private var isLoading = false + + init(title: String, action: @escaping () -> Void) { + self.action = action + button = NSButton(title: title, target: nil, action: nil) + progressIndicator = NSProgressIndicator() + super.init(frame: NSRect(x: 0, y: 0, width: 250, height: 30)) + setupView() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + button.target = self + button.action = #selector(buttonClicked) + button.bezelStyle = .texturedRounded + button.controlSize = .small + button.translatesAutoresizingMaskIntoConstraints = false + addSubview(button) + + progressIndicator.style = .spinning + progressIndicator.controlSize = .small + progressIndicator.isDisplayedWhenStopped = false + progressIndicator.translatesAutoresizingMaskIntoConstraints = false + addSubview(progressIndicator) + + NSLayoutConstraint.activate([ + button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), + button.trailingAnchor.constraint(lessThanOrEqualTo: progressIndicator.leadingAnchor, constant: -8), + button.centerYAnchor.constraint(equalTo: centerYAnchor), + + progressIndicator.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), + progressIndicator.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + } + + func setEnabled(_ enabled: Bool) { + isEnabled = enabled + updateState() + } + + func setLoading(_ loading: Bool) { + isLoading = loading + updateState() + } + + private func updateState() { + button.isEnabled = isEnabled && !isLoading + if isLoading { + progressIndicator.startAnimation(nil) + } else { + progressIndicator.stopAnimation(nil) + } + } + + @objc private func buttonClicked() { + action() + } +} + // MARK: - StatusBarHeaderView @MainActor