ClashX.Meta/ClashX/General/Managers/MenuItemFactory.swift

472 lines
18 KiB
Swift
Raw Normal View History

2018-08-04 21:49:32 +08:00
//
2018-11-30 22:14:20 +08:00
// MenuItemFactory.swift
2018-08-04 21:49:32 +08:00
// ClashX
//
// Created by CYC on 2018/8/4.
2018-08-08 13:47:38 +08:00
// Copyright © 2018 yichengchen. All rights reserved.
2018-08-04 21:49:32 +08:00
//
import Cocoa
import RxCocoa
2019-10-20 13:40:50 +08:00
import SwiftyJSON
2018-08-04 21:49:32 +08:00
2018-11-30 22:14:20 +08:00
class MenuItemFactory {
private static var cachedProxyData: ClashProxyResp?
2023-08-07 23:57:24 +08:00
static var useViewToRenderProxy: Bool = AppDelegate.isAboveMacOS152
2022-07-26 09:14:58 +08:00
2022-06-25 18:05:40 +08:00
static var hideUnselectable: Int = UserDefaults.standard.object(forKey: "hideUnselectable") as? Int ?? NSControl.StateValue.off.rawValue {
didSet {
UserDefaults.standard.set(hideUnselectable, forKey: "hideUnselectable")
recreateProxyMenuItems()
}
}
2022-07-26 09:14:58 +08:00
2022-08-07 12:11:03 +08:00
static let updateAllProvidersTitle = NSLocalizedString("Update All Providers", comment: "")
// MARK: - Public
static func refreshExistingMenuItems() {
ApiRequest.getMergedProxyData {
info in
if info?.proxiesMap.keys != cachedProxyData?.proxiesMap.keys {
// force update menu
refreshMenuItems(mergedData: info)
return
}
2020-06-20 11:48:21 +08:00
for proxy in info?.proxies ?? [] {
NotificationCenter.default.post(name: .proxyUpdate(for: proxy.name), object: proxy, userInfo: nil)
}
}
}
static func recreateProxyMenuItems() {
ApiRequest.getMergedProxyData {
proxyInfo in
cachedProxyData = proxyInfo
refreshMenuItems(mergedData: proxyInfo)
2019-10-06 12:22:21 +08:00
}
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
static func recreateRuleProvidersMenuItems() {
ApiRequest.requestRuleProviderList {
refreshRuleProviderMenuItems($0.allProviders.map({ $0.value }))
}
}
static func refreshMenuItems(mergedData proxyInfo: ClashProxyResp?) {
2020-11-14 15:01:28 +08:00
let leftPadding = AppDelegate.shared.hasMenuSelected()
guard let proxyInfo = proxyInfo else { return }
2022-07-26 09:14:58 +08:00
2022-06-25 18:05:40 +08:00
let hideState = NSControl.StateValue(rawValue: hideUnselectable)
2022-07-26 09:14:58 +08:00
var menuItems = [NSMenuItem]()
2022-07-03 22:08:38 +08:00
var collapsedItems = [NSMenuItem]()
for proxy in proxyInfo.proxyGroups {
var menu: NSMenuItem?
switch proxy.type {
2020-11-14 15:01:28 +08:00
case .select: menu = generateSelectorMenuItem(proxyGroup: proxy, proxyInfo: proxyInfo, leftPadding: leftPadding)
case .urltest, .fallback: menu = generateUrlTestFallBackMenuItem(proxyGroup: proxy, proxyInfo: proxyInfo, leftPadding: leftPadding)
case .loadBalance:
menu = generateLoadBalanceMenuItem(proxyGroup: proxy, proxyInfo: proxyInfo, leftPadding: leftPadding)
case .relay:
menu = generateListOnlyMenuItem(proxyGroup: proxy, proxyInfo: proxyInfo)
default: continue
}
2022-07-03 22:08:38 +08:00
guard let menu = menu else {
continue
}
2024-01-26 16:41:20 +08:00
if let hidden = proxy.hidden, hidden {
continue
}
2022-07-26 09:14:58 +08:00
2022-07-03 22:08:38 +08:00
switch hideState {
case .mixed where [.urltest, .fallback, .loadBalance, .relay].contains(proxy.type):
collapsedItems.append(menu)
menu.isEnabled = true
case .on where [.urltest, .fallback, .loadBalance, .relay].contains(proxy.type):
continue
default:
menuItems.append(menu)
menu.isEnabled = true
}
}
2022-07-26 09:14:58 +08:00
2022-07-03 22:08:38 +08:00
if hideState == .mixed {
let collapsedItem = NSMenuItem(title: "Collapsed", action: nil, keyEquivalent: "")
collapsedItem.isEnabled = true
collapsedItem.submenu = .init(title: "")
collapsedItem.submenu?.items = collapsedItems
2022-07-26 09:14:58 +08:00
2022-07-03 22:08:38 +08:00
menuItems.append(collapsedItem)
}
2022-07-26 09:14:58 +08:00
let items = Array(menuItems.reversed())
updateProxyList(withMenus: items)
2022-07-26 09:14:58 +08:00
2022-07-11 22:21:29 +08:00
refreshProxyProviderMenuItems(mergedData: proxyInfo)
2022-07-12 12:35:21 +08:00
recreateRuleProvidersMenuItems()
}
2019-10-20 13:40:50 +08:00
2020-05-10 23:36:43 +08:00
static func generateSwitchConfigMenuItems(complete: @escaping (([NSMenuItem]) -> Void)) {
let generateMenuItem: ((String) -> NSMenuItem) = {
config in
let item = NSMenuItem(title: config, action: #selector(MenuItemFactory.actionSelectConfig(sender:)), keyEquivalent: "")
item.target = MenuItemFactory.self
item.state = ConfigManager.selectConfigName == config ? .on : .off
2020-05-10 23:36:43 +08:00
return item
}
if RemoteControlManager.selectConfig != nil {
complete([])
return
}
2020-05-10 23:36:43 +08:00
2022-11-20 12:08:46 +08:00
if ICloudManager.shared.useiCloud.value {
ICloudManager.shared.getConfigFilesList {
2020-05-10 23:36:43 +08:00
complete($0.map { generateMenuItem($0) })
}
} else {
complete(ConfigManager.getConfigFilesList().map { generateMenuItem($0) })
}
}
// MARK: - Private
// MARK: Updaters
static func updateProxyList(withMenus menus: [NSMenuItem]) {
let app = AppDelegate.shared
let startIndex = app.statusMenu.items.firstIndex(of: app.separatorLineTop)! + 1
let endIndex = app.statusMenu.items.firstIndex(of: app.sepatatorLineEndProxySelect)!
2023-07-16 12:16:15 +08:00
app.sepatatorLineEndProxySelect.isHidden = menus.isEmpty
2023-09-05 10:07:46 +08:00
for _ in 0 ..< endIndex - startIndex {
app.statusMenu.removeItem(at: startIndex)
}
for each in menus {
app.statusMenu.insertItem(each, at: startIndex)
}
}
// MARK: Generators
private static func generateSelectorMenuItem(proxyGroup: ClashProxy,
2020-11-14 15:01:28 +08:00
proxyInfo: ClashProxyResp,
leftPadding: Bool) -> NSMenuItem? {
let proxyMap = proxyInfo.proxiesMap
2019-10-20 13:40:50 +08:00
2020-04-08 11:24:13 +08:00
let isGlobalMode = ConfigManager.shared.currentConfig?.mode == .global
if !isGlobalMode {
if proxyGroup.name == "GLOBAL" { return nil }
}
2019-03-17 22:00:47 +08:00
let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "")
let selectedName = proxyGroup.now ?? ""
2023-07-12 14:39:53 +08:00
if !Settings.disableShowCurrentProxyInMenu {
2020-11-14 15:01:28 +08:00
menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: selectedName, hasLeftPadding: leftPadding)
2019-10-16 22:46:03 +08:00
}
let submenu = ProxyGroupMenu(title: proxyGroup.name)
2019-10-20 13:40:50 +08:00
for proxy in proxyGroup.all ?? [] {
guard let proxyModel = proxyMap[proxy] else { continue }
let proxyItem = ProxyMenuItem(proxy: proxyModel,
2020-04-26 16:40:29 +08:00
group: proxyGroup,
2020-04-26 18:32:20 +08:00
action: #selector(MenuItemFactory.actionSelectProxy(sender:)))
2018-11-30 22:14:20 +08:00
proxyItem.target = MenuItemFactory.self
submenu.add(delegate: proxyItem)
submenu.addItem(proxyItem)
}
2020-04-26 18:32:20 +08:00
if proxyGroup.isSpeedTestable && useViewToRenderProxy {
submenu.minimumWidth = proxyGroup.maxProxyNameLength + ProxyItemView.fixedPlaceHolderWidth
}
2019-10-16 22:46:03 +08:00
addSpeedTestMenuItem(submenu, proxyGroup: proxyGroup)
menu.submenu = submenu
return menu
}
2019-10-20 13:40:50 +08:00
2020-11-14 15:01:28 +08:00
private static func generateUrlTestFallBackMenuItem(proxyGroup: ClashProxy,
proxyInfo: ClashProxyResp,
leftPadding: Bool) -> NSMenuItem? {
let proxyMap = proxyInfo.proxiesMap
2019-03-17 22:00:47 +08:00
let selectedName = proxyGroup.now ?? ""
let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "")
2023-07-12 14:39:53 +08:00
if !Settings.disableShowCurrentProxyInMenu {
2020-11-14 15:01:28 +08:00
menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: selectedName, hasLeftPadding: leftPadding)
2019-10-16 22:46:03 +08:00
}
2019-03-17 22:00:47 +08:00
let submenu = NSMenu(title: proxyGroup.name)
for proxyName in proxyGroup.all ?? [] {
2019-10-20 13:40:50 +08:00
guard let proxy = proxyMap[proxyName] else { continue }
2020-04-26 18:32:20 +08:00
let proxyMenuItem = ProxyMenuItem(proxy: proxy, group: proxyGroup, action: #selector(empty), simpleItem: true)
proxyMenuItem.target = MenuItemFactory.self
if proxy.name == selectedName {
proxyMenuItem.state = .on
}
2019-10-20 13:40:50 +08:00
proxyMenuItem.submenu = ProxyDelayHistoryMenu(proxy: proxy)
2019-10-20 13:40:50 +08:00
submenu.addItem(proxyMenuItem)
}
2019-12-11 22:04:53 +08:00
addSpeedTestMenuItem(submenu, proxyGroup: proxyGroup)
menu.submenu = submenu
return menu
}
2019-10-20 13:40:50 +08:00
private static func addSpeedTestMenuItem(_ menu: NSMenu, proxyGroup: ClashProxy) {
2023-07-16 12:16:15 +08:00
guard !proxyGroup.speedtestAble.isEmpty else { return }
let speedTestItem = ProxyGroupSpeedTestMenuItem(group: proxyGroup)
let separator = NSMenuItem.separator()
menu.insertItem(separator, at: 0)
menu.insertItem(speedTestItem, at: 0)
(menu as? ProxyGroupMenu)?.add(delegate: speedTestItem)
2019-10-16 22:46:03 +08:00
}
2019-10-20 13:40:50 +08:00
private static func generateLoadBalanceMenuItem(proxyGroup: ClashProxy, proxyInfo: ClashProxyResp, leftPadding: Bool) -> NSMenuItem? {
let proxyMap = proxyInfo.proxiesMap
2019-03-17 22:00:47 +08:00
let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "")
2023-07-12 14:39:53 +08:00
if !Settings.disableShowCurrentProxyInMenu {
menu.view = ProxyGroupMenuItemView(group: proxyGroup.name, targetProxy: NSLocalizedString("Load Balance", comment: ""), hasLeftPadding: leftPadding, observeUpdate: false)
}
let submenu = ProxyGroupMenu(title: proxyGroup.name)
2019-10-20 13:40:50 +08:00
2019-03-17 22:00:47 +08:00
for proxy in proxyGroup.all ?? [] {
2019-10-20 13:40:50 +08:00
guard let proxyModel = proxyMap[proxy] else { continue }
let proxyItem = ProxyMenuItem(proxy: proxyModel,
2020-04-26 16:40:29 +08:00
group: proxyGroup,
2020-04-26 18:32:20 +08:00
action: #selector(empty))
proxyItem.target = MenuItemFactory.self
submenu.add(delegate: proxyItem)
submenu.addItem(proxyItem)
}
2020-04-26 18:32:20 +08:00
if proxyGroup.isSpeedTestable && useViewToRenderProxy {
submenu.minimumWidth = proxyGroup.maxProxyNameLength + ProxyItemView.fixedPlaceHolderWidth
}
2019-12-11 22:04:53 +08:00
addSpeedTestMenuItem(submenu, proxyGroup: proxyGroup)
menu.submenu = submenu
2019-10-20 13:40:50 +08:00
return menu
}
2020-03-24 23:34:57 +08:00
private static func generateListOnlyMenuItem(proxyGroup: ClashProxy, proxyInfo: ClashProxyResp) -> NSMenuItem? {
let menu = NSMenuItem(title: proxyGroup.name, action: nil, keyEquivalent: "")
let submenu = ProxyGroupMenu(title: proxyGroup.name)
let proxyMap = proxyInfo.proxiesMap
for proxy in proxyGroup.all ?? [] {
guard let proxyModel = proxyMap[proxy] else { continue }
let proxyItem = ProxyMenuItem(proxy: proxyModel,
2020-04-26 16:40:29 +08:00
group: proxyGroup,
2020-03-24 23:34:57 +08:00
action: #selector(empty),
2020-04-26 18:32:20 +08:00
simpleItem: true)
2020-03-24 23:34:57 +08:00
proxyItem.target = MenuItemFactory.self
submenu.add(delegate: proxyItem)
submenu.addItem(proxyItem)
}
menu.submenu = submenu
return menu
}
}
2019-10-20 13:40:50 +08:00
// MARK: - Experimental
extension MenuItemFactory {
static func addExperimentalMenuItem(_ menu: inout NSMenu) {
let useViewRender = NSMenuItem(title: NSLocalizedString("Enhance proxy list render", comment: ""), action: #selector(optionUseViewRenderMenuItemTap(sender:)), keyEquivalent: "")
useViewRender.target = self
menu.addItem(useViewRender)
updateUseViewRenderMenuItem(useViewRender)
}
static func updateUseViewRenderMenuItem(_ item: NSMenuItem) {
item.state = useViewToRenderProxy ? .on : .off
}
@objc static func optionUseViewRenderMenuItemTap(sender: NSMenuItem) {
useViewToRenderProxy = !useViewToRenderProxy
updateUseViewRenderMenuItem(sender)
recreateProxyMenuItems()
2018-11-30 22:14:20 +08:00
}
}
2022-07-05 14:38:39 +08:00
// MARK: - Meta
extension MenuItemFactory {
2022-07-26 09:14:58 +08:00
2022-07-11 22:21:29 +08:00
static func refreshProxyProviderMenuItems(mergedData proxyInfo: ClashProxyResp?) {
2022-07-05 14:38:39 +08:00
let app = AppDelegate.shared
guard let proxyInfo = proxyInfo,
let menu = app.proxyProvidersMenu,
let providers = proxyInfo.enclosingProviderResp
else { return }
2022-07-12 12:59:32 +08:00
2022-07-12 12:35:21 +08:00
let proxyProviders = providers.allProviders.filter {
$0.value.vehicleType == .HTTP
}.values.sorted(by: { $0.name < $1.name })
2022-07-26 09:14:58 +08:00
2022-07-12 12:59:32 +08:00
let isEmpty = proxyProviders.count == 0
app.proxyProvidersMenuItem.isEnabled = !isEmpty
guard !isEmpty else { return }
initUpdateAllProvidersMenuItem(for: menu, type: .proxy)
2022-07-12 12:35:21 +08:00
let maxNameLength = maxProvidersLength(for: proxyProviders.map({ $0.name }))
proxyProviders.forEach { provider in
let item = DualTitleMenuItem(
provider.name,
subTitle: providerUpdateTitle(provider.updatedAt),
action: #selector(actionUpdateSelectProvider),
maxLength: maxNameLength)
item.tag = ApiRequest.ProviderType.proxy.rawValue
item.target = self
menu.addItem(item)
}
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
static func refreshRuleProviderMenuItems(_ ruleProviders: [ClashRuleProvider]) {
let app = AppDelegate.shared
2022-07-12 12:59:32 +08:00
let isEmpty = ruleProviders.count == 0
app.ruleProvidersMenuItem.isEnabled = !isEmpty
2022-07-26 09:14:58 +08:00
2022-07-12 12:59:32 +08:00
guard !isEmpty,
let menu = app.ruleProvidersMenu
else { return }
2022-07-26 09:14:58 +08:00
2022-07-12 12:59:32 +08:00
initUpdateAllProvidersMenuItem(for: menu, type: .rule)
2022-07-12 12:35:21 +08:00
let maxNameLength = maxProvidersLength(for: ruleProviders.map({ $0.name }))
ruleProviders.sorted(by: { $0.name < $1.name })
.forEach { provider in
let item = DualTitleMenuItem(
provider.name,
subTitle: providerUpdateTitle(provider.updatedAt),
action: #selector(actionUpdateSelectProvider),
maxLength: maxNameLength)
item.tag = ApiRequest.ProviderType.rule.rawValue
item.target = self
menu.addItem(item)
}
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
static func initUpdateAllProvidersMenuItem(for menu: NSMenu, type: ApiRequest.ProviderType) {
2022-07-05 14:38:39 +08:00
if menu.items.count > 1 {
menu.items.enumerated().filter {
$0.offset > 1
}.forEach {
menu.removeItem($0.element)
}
} else {
2022-07-12 12:35:21 +08:00
let updateAllItem = NSMenuItem(title: updateAllProvidersTitle, action: #selector(actionUpdateAllProviders), keyEquivalent: "")
updateAllItem.tag = type.rawValue
2022-07-05 14:38:39 +08:00
updateAllItem.target = self
menu.addItem(updateAllItem)
menu.addItem(.separator())
}
2022-07-12 12:35:21 +08:00
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
static func maxProvidersLength(for names: [String]) -> CGFloat {
func getLength(_ string: String) -> CGFloat {
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: 20)
let attr = [NSAttributedString.Key.font: NSFont.menuBarFont(ofSize: 14)]
let length = (string as NSString)
.boundingRect(with: rect,
options: .usesLineFragmentOrigin,
attributes: attr).width
return length
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
var lengths = names.map {
getLength($0) + 65
}
lengths.append(getLength(updateAllProvidersTitle))
return lengths.max() ?? 0
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
static func providerUpdateTitle(_ updatedAt: String?) -> String? {
let dateCF = DateComponentsFormatter()
dateCF.allowedUnits = [.day, .hour, .minute]
dateCF.maximumUnitCount = 1
dateCF.unitsStyle = .abbreviated
dateCF.zeroFormattingBehavior = .dropAll
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
guard let dateStr = updatedAt,
let date = DateFormatter.provider.date(from: dateStr),
!date.timeIntervalSinceNow.isNaN,
!date.timeIntervalSinceNow.isInfinite,
let re = dateCF.string(from: abs(date.timeIntervalSinceNow)) else { return nil }
2022-07-26 09:14:58 +08:00
2022-08-07 12:11:03 +08:00
return re + NSLocalizedString(" ago", comment: "Provider update time title")
2022-07-05 14:38:39 +08:00
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
@objc static func actionUpdateAllProviders(sender: NSMenuItem) {
let type = ApiRequest.ProviderType(rawValue: sender.tag)!
let s = "Update All \(type.logString()) Providers"
2022-07-05 14:38:39 +08:00
Logger.log(s)
2022-07-12 12:35:21 +08:00
ApiRequest.updateAllProviders(for: type) {
2022-07-05 14:38:39 +08:00
Logger.log("\(s) \($0) failed")
let info = $0 == 0 ? "Success" : "\($0) failed"
NSUserNotificationCenter.default.post(title: s, info: info)
recreateProxyMenuItems()
}
}
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
@objc static func actionUpdateSelectProvider(sender: DualTitleMenuItem) {
2022-07-05 14:38:39 +08:00
let name = sender.originTitle
2022-07-12 12:35:21 +08:00
let type = ApiRequest.ProviderType(rawValue: sender.tag)!
2022-07-26 09:14:58 +08:00
2022-07-12 12:35:21 +08:00
let log = "Update \(type.logString()) Provider \(name)"
2022-07-05 14:38:39 +08:00
Logger.log(log)
2022-07-12 12:35:21 +08:00
ApiRequest.updateProvider(for: type, name: name) {
2022-07-05 14:38:39 +08:00
let info = $0 ? "Success" : "Failed"
Logger.log("\(log) info")
NSUserNotificationCenter.default.post(title: log, info: info)
recreateProxyMenuItems()
}
}
}
// MARK: - Action
2018-11-30 22:14:20 +08:00
extension MenuItemFactory {
2019-10-20 13:40:50 +08:00
@objc static func actionSelectProxy(sender: ProxyMenuItem) {
guard let proxyGroup = sender.menu?.title else { return }
let proxyName = sender.proxyName
2019-10-20 13:40:50 +08:00
ApiRequest.updateProxyGroup(group: proxyGroup, selectProxy: proxyName) { success in
if success {
2018-08-04 21:49:32 +08:00
for items in sender.menu?.items ?? [NSMenuItem]() {
items.state = .off
}
sender.state = .on
// remember select proxy
let newModel = SavedProxyModel(group: proxyGroup, selected: proxyName, config: ConfigManager.selectConfigName)
2019-11-02 00:04:42 +08:00
ConfigManager.selectedProxyRecords.removeAll { model -> Bool in
2019-12-11 20:27:17 +08:00
return model.key == newModel.key
}
2019-11-02 00:04:42 +08:00
ConfigManager.selectedProxyRecords.append(newModel)
// terminal Connections for this group
ConnectionManager.closeConnection(for: proxyGroup)
// refresh menu items
MenuItemFactory.refreshExistingMenuItems()
2018-08-04 21:49:32 +08:00
}
}
}
2019-10-20 13:40:50 +08:00
@objc static func actionSelectConfig(sender: NSMenuItem) {
2018-11-30 22:14:20 +08:00
let config = sender.title
AppDelegate.shared.updateConfig(configName: config, showNotification: false) {
err in
if err == nil {
ConnectionManager.closeAllConnection()
}
}
2018-11-30 22:14:20 +08:00
}
2019-10-20 13:40:50 +08:00
@objc static func empty() {}
}