diff --git a/Kit/module/notifications.swift b/Kit/module/notifications.swift index 523da673..faee1f38 100644 --- a/Kit/module/notifications.swift +++ b/Kit/module/notifications.swift @@ -62,6 +62,18 @@ open class NotificationsWrapper: NSStackView { } } + public func newNotification(id rid: String, title: String, subtitle: String? = nil) { + let id = "Stats_\(self.module)_\(rid)" + + if self.ids[id] != nil { + removeNotification(id) + self.ids[id] = nil + } + + self.showNotification(id: id, title: title, subtitle: subtitle) + self.ids[id] = true + } + public func hideNotification(_ rid: String) { let id = "Stats_\(self.module)_\(rid)" if self.ids[id] != nil { diff --git a/Modules/Net/config.plist b/Modules/Net/config.plist index a22055d5..45bec3c2 100644 --- a/Modules/Net/config.plist +++ b/Modules/Net/config.plist @@ -81,7 +81,7 @@ popup notifications - + diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 04267108..5794e733 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -126,6 +126,7 @@ public class Network: Module { private let popupView: Popup private let settingsView: Settings private let portalView: Portal + private let notificationsView: Notifications private var usageReader: UsageReader? = nil private var processReader: ProcessReader? = nil @@ -154,12 +155,14 @@ public class Network: Module { self.settingsView = Settings(.network) self.popupView = Popup(.network) self.portalView = Portal(.network) + self.notificationsView = Notifications(.network) super.init( moduleType: .network, popup: self.popupView, settings: self.settingsView, - portal: self.portalView + portal: self.portalView, + notifications: self.notificationsView ) guard self.available else { return } @@ -220,6 +223,7 @@ public class Network: Module { self.popupView.usageCallback(value) self.portalView.usageCallback(value) + self.notificationsView.usageCallback(value) var upload: Int64 = value.bandwidth.upload var download: Int64 = value.bandwidth.download @@ -313,6 +317,7 @@ public class Network: Module { guard let value = raw, self.enabled else { return } self.popupView.connectivityCallback(value) + self.notificationsView.connectivityCallback(value) self.menuBar.widgets.filter{ $0.isActive }.forEach { (w: SWidget) in switch w.item { diff --git a/Modules/Net/notifications.swift b/Modules/Net/notifications.swift new file mode 100644 index 00000000..611a0b18 --- /dev/null +++ b/Modules/Net/notifications.swift @@ -0,0 +1,141 @@ +// +// notifications.swift +// Net +// +// Created by Serhiy Mytrovtsiy on 25/01/2025 +// Using Swift 6.0 +// Running on macOS 15.1 +// +// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved. +// + +import Cocoa +import Kit + +class Notifications: NotificationsWrapper { + private let connectionID: String = "connection" + private let interfaceID: String = "interface" + private let localID: String = "localIP" + private let publicID: String = "publicIP" + private let wifiID: String = "wifi" + + private var connectionState: Bool = false + private var interfaceState: Bool = false + private var localIPState: Bool = false + private var publicIPState: Bool = false + private var wifiState: Bool = false + + private var connection: Bool? + private var interface: String? + private var localIP: String? + private var publicIP: String? + private var wifi: String? + + public init(_ module: ModuleType) { + super.init(module, [self.connectionID, self.interfaceID, self.localID, self.publicID, self.wifiID]) + + self.connectionState = Store.shared.bool(key: "\(self.module)_notifications_connection_state", defaultValue: self.connectionState) + self.interfaceState = Store.shared.bool(key: "\(self.module)_notifications_interface_state", defaultValue: self.interfaceState) + self.localIPState = Store.shared.bool(key: "\(self.module)_notifications_localIP_state", defaultValue: self.localIPState) + self.publicIPState = Store.shared.bool(key: "\(self.module)_notifications_publicIP_state", defaultValue: self.publicIPState) + self.wifiState = Store.shared.bool(key: "\(self.module)_notifications_wifi_state", defaultValue: self.wifiState) + + self.addArrangedSubview(PreferencesSection([ + PreferencesRow(localizedString("Status"), component: switchView( + action: #selector(self.toggleConnectionState), + state: self.connectionState + )), + PreferencesRow(localizedString("Network interface"), component: switchView( + action: #selector(self.toggleInterfaceState), + state: self.interfaceState + )), + PreferencesRow(localizedString("Local IP"), component: switchView( + action: #selector(self.toggleLocalIPState), + state: self.localIPState + )), + PreferencesRow(localizedString("Public IP"), component: switchView( + action: #selector(self.toggleNPublicIPState), + state: self.publicIPState + )), + PreferencesRow(localizedString("WiFi network"), component: switchView( + action: #selector(self.toggleWiFiState), + state: self.wifiState + )) + ])) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + internal func usageCallback(_ value: Network_Usage) { + if self.interfaceState { + if value.interface?.BSDName != self.interface { + self.newNotification(id: self.interfaceID, title: localizedString("Network interface changed"), subtitle: nil) + } + self.interface = value.interface?.BSDName + } + + if self.localIPState { + if value.laddr != self.localIP { + self.newNotification(id: self.localID, title: localizedString("Local IP changed"), subtitle: nil) + } + self.localIP = value.laddr + } + + if self.publicIPState { + if value.raddr.v4 ?? value.raddr.v6 != self.publicIP { + self.newNotification(id: self.publicID, title: localizedString("Public IP changed"), subtitle: nil) + } + self.publicIP = value.raddr.v4 ?? value.raddr.v6 + } + + if self.wifiState { + if value.wifiDetails.ssid != self.wifi { + self.newNotification(id: self.wifiID, title: localizedString("WiFi network changed"), subtitle: nil) + } + self.wifi = value.wifiDetails.ssid + } + } + + internal func connectivityCallback(_ value: Network_Connectivity) { + guard self.connectionState else { return } + + if self.connection == nil { + self.connection = value.status + return + } + + if self.connection != value.status { + var title: String + if value.status { + title = localizedString("Internet connection established") + } else { + title = localizedString("Internet connection lost") + } + self.newNotification(id: self.connectionID, title: title, subtitle: nil) + } + self.connection = value.status + } + + @objc private func toggleConnectionState(_ sender: NSControl) { + self.interfaceState = controlState(sender) + Store.shared.set(key: "\(self.module)_notifications_connection_state", value: self.interfaceState) + } + @objc private func toggleInterfaceState(_ sender: NSControl) { + self.interfaceState = controlState(sender) + Store.shared.set(key: "\(self.module)_notifications_interface_state", value: self.interfaceState) + } + @objc private func toggleLocalIPState(_ sender: NSControl) { + self.interfaceState = controlState(sender) + Store.shared.set(key: "\(self.module)_notifications_localIP_state", value: self.interfaceState) + } + @objc private func toggleNPublicIPState(_ sender: NSControl) { + self.interfaceState = controlState(sender) + Store.shared.set(key: "\(self.module)_notifications_publicIP_state", value: self.interfaceState) + } + @objc private func toggleWiFiState(_ sender: NSControl) { + self.interfaceState = controlState(sender) + Store.shared.set(key: "\(self.module)_notifications_wifi_state", value: self.interfaceState) + } +} diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 60632f56..cb7e6272 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ 5C645BFF2C591F6600D8342A /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C645BFE2C591F6600D8342A /* widget.swift */; }; 5C645C002C591FFA00D8342A /* Net.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; }; 5C645C012C591FFA00D8342A /* Net.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9A3E17CC247A94AF00449CD1 /* Net.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 5C6F55A72D45694400AB58ED /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6F55A62D45694400AB58ED /* notifications.swift */; }; 5C7C1DF42C29A3A00060387D /* notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C1DF32C29A3A00060387D /* notifications.swift */; }; 5C8E001029269C7F0027C75A /* protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE493829265055000F2856 /* protocol.swift */; }; 5CA518382B543FE600EBCCC4 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA518372B543FE600EBCCC4 /* portal.swift */; }; @@ -539,6 +540,7 @@ 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; 5C621D812B4770D6004ED7AF /* process.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = process.swift; sourceTree = ""; }; 5C645BFE2C591F6600D8342A /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; }; + 5C6F55A62D45694400AB58ED /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5C7C1DF32C29A3A00060387D /* notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = notifications.swift; sourceTree = ""; }; 5C9F90A02A76B30500D41748 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; 5CA518372B543FE600EBCCC4 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; @@ -1075,6 +1077,7 @@ 9A3E17E9247B07BF00449CD1 /* popup.swift */, 5C23BC0B29A10BE000DBA990 /* portal.swift */, 9A58DEA324B3647600716A9F /* settings.swift */, + 5C6F55A62D45694400AB58ED /* notifications.swift */, 5C645BFE2C591F6600D8342A /* widget.swift */, 9A3E17CF247A94AF00449CD1 /* Info.plist */, 9A3E17DC247A94C300449CD1 /* config.plist */, @@ -2125,6 +2128,7 @@ 5C645BFF2C591F6600D8342A /* widget.swift in Sources */, 9A58DEA424B3647600716A9F /* settings.swift in Sources */, 9A3E17D9247A94B500449CD1 /* main.swift in Sources */, + 5C6F55A72D45694400AB58ED /* notifications.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };