From f053555492a5402dbbd527ded514aec16ac12654 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Fri, 13 Sep 2024 19:08:40 +0200 Subject: [PATCH] feat: added Text widget to the Network module with a description of the value that could be used in it (#1868) --- Kit/extensions.swift | 6 +++ Kit/helpers.swift | 25 +++++++++++ Modules/Net/config.plist | 12 ++++++ Modules/Net/main.swift | 68 +++++++++++++++++++++++++++++- Modules/Net/settings.swift | 85 +++++++++++++++++++++++++++++++++++--- 5 files changed, 189 insertions(+), 7 deletions(-) diff --git a/Kit/extensions.swift b/Kit/extensions.swift index 7ba857ba..a8af075b 100644 --- a/Kit/extensions.swift +++ b/Kit/extensions.swift @@ -558,3 +558,9 @@ public extension TimeZone { } } } + +extension CGFloat { + func roundedUpToNearestTen() -> CGFloat { + return ceil(self / 10) * 10 + } +} diff --git a/Kit/helpers.swift b/Kit/helpers.swift index 3b45b090..d27b4f0b 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -13,6 +13,7 @@ import Cocoa import ServiceManagement import UserNotifications +import WebKit public struct LaunchAtLogin { private static let id = "\(Bundle.main.bundleIdentifier!).LaunchAtLogin" @@ -1571,3 +1572,27 @@ public class PreferencesSwitch: NSStackView { self.with.isEnabled = controlState(sender) } } + +public class HelpHUD: NSPanel { + public init(_ text: String, origin: CGPoint = CGPoint(x: 0, y: 0), size: CGSize = CGSize(width: 420, height: 300)) { + super.init( + contentRect: NSRect(origin: origin, size: size), + styleMask: [.hudWindow, .titled, .closable], + backing: .buffered, defer: false + ) + self.isFloatingPanel = true + self.isMovableByWindowBackground = true + self.level = .floating + self.title = "Help" + + let webView = WKWebView() + webView.setValue(false, forKey: "drawsBackground") + webView.loadHTMLString("\(text)", baseURL: nil) + self.contentView = webView + } + + public func show() { + self.makeKeyAndOrderFront(self) + self.center() + } +} diff --git a/Modules/Net/config.plist b/Modules/Net/config.plist index 40f1fb05..57db9eb5 100644 --- a/Modules/Net/config.plist +++ b/Modules/Net/config.plist @@ -54,6 +54,18 @@ + text + + Default + + Order + 4 + Preview + + Value + 192.168.0.1 + + Settings diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 0a02d516..0167e7e9 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -146,6 +146,9 @@ public class Network: Module { private var publicIPRefreshInterval: String { Store.shared.string(key: "\(self.name)_publicIPRefreshInterval", defaultValue: "never") } + private var textValue: String { + Store.shared.string(key: "\(self.name)_textWidgetValue", defaultValue: "$addr.public - $status") + } public init() { self.settingsView = Settings(.network) @@ -233,13 +236,74 @@ public class Network: Module { switch w.item { case let widget as SpeedWidget: widget.setValue(upload: upload, download: download) case let widget as NetworkChart: widget.setValue(upload: Double(upload), download: Double(download)) + case let widget as TextWidget: + var text = self.textValue + let pairs = TextWidget.parseText(text) + pairs.forEach { pair in + var replacement: String? = nil + + switch pair.key { + case "$addr": + switch pair.value { + case "public": replacement = value.raddr.v4 ?? value.raddr.v6 ?? "-" + case "publicV4": replacement = value.raddr.v4 ?? "-" + case "publicV6": replacement = value.raddr.v6 ?? "-" + case "private": replacement = value.laddr ?? "-" + default: return + } + case "$interface": + switch pair.value { + case "displayName": replacement = value.interface?.displayName ?? "-" + case "BSDName": replacement = value.interface?.BSDName ?? "-" + case "address": replacement = value.interface?.address ?? "-" + default: return + } + case "$wifi": + switch pair.value { + case "ssid": replacement = value.wifiDetails.ssid ?? "-" + case "bssid": replacement = value.wifiDetails.bssid ?? "-" + case "RSSI": replacement = "\(value.wifiDetails.RSSI ?? 0)" + case "noise": replacement = "\(value.wifiDetails.noise ?? 0)" + case "transmitRate": replacement = "\(value.wifiDetails.transmitRate ?? 0)" + case "standard": replacement = value.wifiDetails.standard ?? "-" + case "mode": replacement = value.wifiDetails.mode ?? "-" + case "security": replacement = value.wifiDetails.security ?? "-" + case "channel": replacement = value.wifiDetails.channel ?? "-" + case "channelBand": replacement = value.wifiDetails.channelBand ?? "-" + case "channelWidth": replacement = value.wifiDetails.channelWidth ?? "-" + case "channelNumber": replacement = value.wifiDetails.channelNumber ?? "-" + default: return + } + case "$status": + replacement = localizedString(value.status ? "UP" : "DOWN") + case "$upload": + switch pair.value { + case "total": replacement = Units(bytes: value.total.upload).getReadableMemory() + default: replacement = Units(bytes: value.bandwidth.upload).getReadableMemory() + } + case "$download": + switch pair.value { + case "total": replacement = Units(bytes: value.total.download).getReadableMemory() + default: replacement = Units(bytes: value.bandwidth.download).getReadableMemory() + } + case "$type": + replacement = value.connectionType?.rawValue ?? "-" + default: return + } + + if let replacement { + let key = pair.value.isEmpty ? pair.key : "\(pair.key).\(pair.value)" + text = text.replacingOccurrences(of: key, with: replacement) + } + } + widget.setValue(text) default: break } } if #available(macOS 11.0, *) { - guard let blobData = try? JSONEncoder().encode(raw) else { return } - self.userDefaults?.set(blobData, forKey: "Network@UsageReader") +// guard let blobData = try? JSONEncoder().encode(raw) else { return } +// self.userDefaults?.set(blobData, forKey: "Network@UsageReader") WidgetCenter.shared.reloadTimelines(ofKind: Network_entry.kind) } } diff --git a/Modules/Net/settings.swift b/Modules/Net/settings.swift index 9c5f6f9c..02411798 100644 --- a/Modules/Net/settings.swift +++ b/Modules/Net/settings.swift @@ -13,6 +13,44 @@ import Cocoa import Kit import SystemConfiguration +var textWidgetHelp = """ +

Description

+You can use a combination of any of the variables. There is only one limitation: there must be a space between each variable. +

Examples:

+
    +
  • $addr.public - $status
  • +
  • $addr.public - $wifi.ssid - $status
  • +
+

Available variables

+
    +
  • $addr.public: Public IP address.
  • +
  • $addr.publicV4: Public IPv4 address.
  • +
  • $addr.publicV6: Public IPv6 address.
  • +
  • $addr.private: Private/local IP address.
  • +
  • $interface.displayName: Network interface name.
  • +
  • $interface.BSDName: BSD name of the network interface.
  • +
  • $interface.address: MAC address of the network interface.
  • +
  • $wifi.ssid: Wi-Fi network name.
  • +
  • $wifi.bssid: MAC address of the Wi-Fi access point (BSSID).
  • +
  • $wifi.RSSI: Signal strength of the Wi-Fi network (RSSI).
  • +
  • $wifi.noise: Noise level of the Wi-Fi network.
  • +
  • $wifi.transmitRate: Transmit rate (connection speed) of the Wi-Fi network.
  • +
  • $wifi.standard: Wi-Fi standard (e.g., 802.11a/b/g/n/ac).
  • +
  • $wifi.mode: Operating mode of the Wi-Fi (e.g., infrastructure, adhoc).
  • +
  • $wifi.security: Type of security used by the Wi-Fi network.
  • +
  • $wifi.channel: Wi-Fi channel being used.
  • +
  • $wifi.channelBand: Frequency band of the Wi-Fi channel (e.g., 2.4 GHz, 5 GHz).
  • +
  • $wifi.channelWidth: Channel width used in MHz.
  • +
  • $wifi.channelNumber: Channel number used by the Wi-Fi network.
  • +
  • $status: Status of the network connection. "UP" if active, "DOWN" if inactive.
  • +
  • $upload.total: Total amount of data uploaded over the connection.
  • +
  • $upload: Current upload bandwidth used.
  • +
  • $download.total: Total amount of data downloaded over the connection.
  • +
  • $download: Current download bandwidth used.
  • +
  • $type: Type of network connection (e.g., Ethernet, Wi-Fi, Cellular).
  • +
+""" + internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { private var numberOfProcesses: Int = 8 private var readerType: String = "interface" @@ -24,6 +62,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { private var ICMPHost: String = "1.1.1.1" private var publicIPRefreshInterval: String = "never" private var baseValue: String = "byte" + private var textValue: String = "$addr.public - $status" public var callback: (() -> Void) = {} public var callbackWhenUpdateNumberOfProcesses: (() -> Void) = {} @@ -35,6 +74,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { private var sliderView: NSView? = nil private var section: PreferencesSection? = nil private var widgetThresholdSection: PreferencesSection? = nil + private let textWidgetHelpPanel: HelpHUD = HelpHUD(textWidgetHelp) private var list: [Network_interface] = [] @@ -57,6 +97,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { self.ICMPHost = Store.shared.string(key: "\(self.title)_ICMPHost", defaultValue: self.ICMPHost) self.publicIPRefreshInterval = Store.shared.string(key: "\(self.title)_publicIPRefreshInterval", defaultValue: self.publicIPRefreshInterval) self.baseValue = Store.shared.string(key: "\(self.title)_base", defaultValue: self.baseValue) + self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue) super.init(frame: NSRect.zero) self.orientation = .vertical @@ -170,9 +211,38 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { valueField.delegate = self valueField.placeholderString = localizedString("Leave empty to disable the check") + let ICMPField = self.inputField(id: "ICMP", value: self.ICMPHost, placeholder: localizedString("Leave empty to disable the check")) self.addArrangedSubview(PreferencesSection([ - PreferencesRow(localizedString("Connectivity host (ICMP)"), component: valueField) + PreferencesRow(localizedString("Connectivity host (ICMP)"), component: ICMPField) { + NSWorkspace.shared.open(URL(string: "https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol")!) + } ])) + + if widgets.contains(where: { $0 == .text }) { + let textField = self.inputField(id: "text", value: self.textValue, placeholder: localizedString("This will be visible in the text widget")) + self.addArrangedSubview(PreferencesSection([ + PreferencesRow(localizedString("Text widget value"), component: textField) { [weak self] in + self?.textWidgetHelpPanel.show() + } + ])) + } + } + + private func inputField(id: String, value: String, placeholder: String) -> NSView { + let field: NSTextField = NSTextField() + field.identifier = NSUserInterfaceItemIdentifier(id) + field.widthAnchor.constraint(equalToConstant: 250).isActive = true + field.font = NSFont.systemFont(ofSize: 12, weight: .regular) + field.textColor = .textColor + field.isEditable = true + field.isSelectable = true + field.usesSingleLineMode = true + field.maximumNumberOfLines = 1 + field.focusRingType = .none + field.stringValue = value + field.delegate = self + field.placeholderString = placeholder + return field } @objc private func handleSelection(_ sender: NSPopUpButton) { @@ -229,10 +299,15 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { } func controlTextDidChange(_ notification: Notification) { - if let textField = notification.object as? NSTextField { - self.ICMPHost = textField.stringValue - Store.shared.set(key: "\(self.title)_ICMPHost", value: self.ICMPHost) - self.ICMPHostCallback(self.ICMPHost.isEmpty) + if let field = notification.object as? NSTextField { + if field.identifier == NSUserInterfaceItemIdentifier("ICMP") { + self.ICMPHost = field.stringValue + Store.shared.set(key: "\(self.title)_ICMPHost", value: self.ICMPHost) + self.ICMPHostCallback(self.ICMPHost.isEmpty) + } else if field.identifier == NSUserInterfaceItemIdentifier("text") { + self.textValue = field.stringValue + Store.shared.set(key: "\(self.title)_textWidgetValue", value: self.textValue) + } } }