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:

+ +

Available variables

+ +""" + 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) + } } }