From d0f4354c054e90d67b8b31f77e8bf7b31506af43 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Thu, 1 Aug 2024 23:37:08 +0200 Subject: [PATCH] feat: added macOS widget for Network module --- Modules/Net/main.swift | 7 ++ Modules/Net/widget.swift | 121 ++++++++++++++++++++++++++++++++ Stats.xcodeproj/project.pbxproj | 32 +++++++++ Widgets/widgets.swift | 2 + 4 files changed, 162 insertions(+) create mode 100644 Modules/Net/widget.swift diff --git a/Modules/Net/main.swift b/Modules/Net/main.swift index 4c0fca41..0a02d516 100644 --- a/Modules/Net/main.swift +++ b/Modules/Net/main.swift @@ -12,6 +12,7 @@ import Cocoa import Kit import SystemConfiguration +import WidgetKit public enum Network_t: String, Codable { case wifi @@ -235,6 +236,12 @@ public class Network: Module { default: break } } + + if #available(macOS 11.0, *) { + guard let blobData = try? JSONEncoder().encode(raw) else { return } + self.userDefaults?.set(blobData, forKey: "Network@UsageReader") + WidgetCenter.shared.reloadTimelines(ofKind: Network_entry.kind) + } } private func connectivityCallback(_ raw: Network_Connectivity?) { diff --git a/Modules/Net/widget.swift b/Modules/Net/widget.swift new file mode 100644 index 00000000..a5791ebe --- /dev/null +++ b/Modules/Net/widget.swift @@ -0,0 +1,121 @@ +// +// widget.swift +// Net +// +// Created by Serhiy Mytrovtsiy on 30/07/2024 +// Using Swift 5.0 +// Running on macOS 14.5 +// +// Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved. +// + +import SwiftUI +import WidgetKit +import Charts +import Kit + +public struct Network_entry: TimelineEntry { + public static let kind = "NetworkWidget" + public static var snapshot: Network_entry = Network_entry() + + public var date: Date { + Calendar.current.date(byAdding: .second, value: 5, to: Date())! + } + public var value: Network_Usage? = nil +} + +@available(macOS 11.0, *) +public struct Provider: TimelineProvider { + public typealias Entry = Network_entry + + private let userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") + + public func placeholder(in context: Context) -> Network_entry { + Network_entry() + } + + public func getSnapshot(in context: Context, completion: @escaping (Network_entry) -> Void) { + completion(Network_entry.snapshot) + } + + public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + var entry = Network_entry() + if let raw = userDefaults?.data(forKey: "Network@UsageReader"), let load = try? JSONDecoder().decode(Network_Usage.self, from: raw) { + entry.value = load + } + let entries: [Network_entry] = [entry] + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +@available(macOS 14.0, *) +public struct NetworkWidget: Widget { + private var downloadColor: Color = Color(nsColor: NSColor.systemBlue) + private var uploadColor: Color = Color(nsColor: NSColor.systemRed) + + public init() {} + + public var body: some WidgetConfiguration { + StaticConfiguration(kind: Network_entry.kind, provider: Provider()) { entry in + VStack(spacing: 10) { + if let value = entry.value { + VStack { + HStack { + VStack { + VStack(spacing: 0) { + Text(Units(bytes: value.bandwidth.download).getReadableTuple().0).font(.system(size: 24, weight: .regular)) + Text(Units(bytes: value.bandwidth.download).getReadableTuple().1).font(.system(size: 10, weight: .regular)) + } + Text("Download").font(.system(size: 12, weight: .regular)).foregroundColor(.gray) + }.frame(maxWidth: .infinity) + VStack { + VStack(spacing: 0) { + Text(Units(bytes: value.bandwidth.upload).getReadableTuple().0).font(.system(size: 24, weight: .regular)) + Text(Units(bytes: value.bandwidth.upload).getReadableTuple().1).font(.system(size: 10, weight: .regular)) + } + Text("Upload").font(.system(size: 12, weight: .regular)).foregroundColor(.gray) + }.frame(maxWidth: .infinity) + } + .frame(maxHeight: .infinity) + VStack(spacing: 3) { + HStack { + Text("Status").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + Text(value.status ? "UP" : "DOWN") + } + if let interface = value.interface { + HStack { + Text("Interface").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + Text(value.wifiDetails.ssid ?? interface.displayName) + } + } + HStack { + Text("IP").font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + if let raddr = value.raddr.v6 { + Text(raddr) + .font(.system(size: 8)) + .multilineTextAlignment(.trailing) + .frame(maxWidth: .infinity, alignment: .center) + } else if let raddr = value.raddr.v4 { + Text(raddr) + } else { + Text("Unknown") + } + } + } + } + } else { + Text("No data") + } + } + .containerBackground(for: .widget) { + Color.clear + } + } + .configurationDisplayName("Network widget") + .description("Displays network stats") + .supportedFamilies([.systemSmall]) + } +} diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 533ae18c..64a873f5 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -52,6 +52,9 @@ 5C4E8BE92B71031A00F148B6 /* Kit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C4E8BE82B7102A700F148B6 /* Kit.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5C5647F82A3F6B100098FFE9 /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5647F72A3F6B100098FFE9 /* Telemetry.swift */; }; 5C621D822B4770D6004ED7AF /* process.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C621D812B4770D6004ED7AF /* process.swift */; }; + 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, ); }; }; 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 */; }; @@ -238,6 +241,13 @@ remoteGlobalIDString = 9A2846F62666A9CC00EC1F6D; remoteInfo = Kit; }; + 5C645C022C591FFA00D8342A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A3E17CB247A94AF00449CD1; + remoteInfo = Net; + }; 5CE7E79A2C318513006BC92C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9A1410ED229E721100D29793 /* Project object */; @@ -374,6 +384,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 5C645C042C591FFA00D8342A /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 5C645C012C591FFA00D8342A /* Net.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 5CE7E79D2C318513006BC92C /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -509,6 +530,7 @@ 5C4E8BE82B7102A700F148B6 /* Kit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Kit.h; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -697,6 +719,7 @@ 5CE7E78E2C318512006BC92C /* SwiftUI.framework in Frameworks */, 5C0A9CAA2C46838A00EE6A89 /* GPU.framework in Frameworks */, 5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */, + 5C645C002C591FFA00D8342A /* Net.framework in Frameworks */, 5C0A9CB42C46839500EE6A89 /* Disk.framework in Frameworks */, 5C0A9CA52C46838300EE6A89 /* CPU.framework in Frameworks */, 5C0A9CAF2C46838F00EE6A89 /* RAM.framework in Frameworks */, @@ -1041,6 +1064,7 @@ 9A3E17E9247B07BF00449CD1 /* popup.swift */, 5C23BC0B29A10BE000DBA990 /* portal.swift */, 9A58DEA324B3647600716A9F /* settings.swift */, + 5C645BFE2C591F6600D8342A /* widget.swift */, 9A3E17CF247A94AF00449CD1 /* Info.plist */, 9A3E17DC247A94C300449CD1 /* config.plist */, ); @@ -1364,6 +1388,7 @@ 5CE7E7862C318512006BC92C /* Sources */, 5CE7E7872C318512006BC92C /* Frameworks */, 5CE7E7882C318512006BC92C /* Resources */, + 5C645C042C591FFA00D8342A /* Embed Frameworks */, ); buildRules = ( ); @@ -1372,6 +1397,7 @@ 5C0A9CAD2C46838A00EE6A89 /* PBXTargetDependency */, 5C0A9CB22C46838F00EE6A89 /* PBXTargetDependency */, 5C0A9CB72C46839500EE6A89 /* PBXTargetDependency */, + 5C645C032C591FFA00D8342A /* PBXTargetDependency */, ); name = WidgetsExtension; productName = WidgetsExtension; @@ -2078,6 +2104,7 @@ 9A3E17DB247A94BC00449CD1 /* readers.swift in Sources */, 9A3E17EA247B07BF00449CD1 /* popup.swift in Sources */, 5C23BC0C29A10BE000DBA990 /* portal.swift in Sources */, + 5C645BFF2C591F6600D8342A /* widget.swift in Sources */, 9A58DEA424B3647600716A9F /* settings.swift in Sources */, 9A3E17D9247A94B500449CD1 /* main.swift in Sources */, ); @@ -2219,6 +2246,11 @@ target = 9A2846F62666A9CC00EC1F6D /* Kit */; targetProxy = 5C2229B229CDFBF600F00E69 /* PBXContainerItemProxy */; }; + 5C645C032C591FFA00D8342A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A3E17CB247A94AF00449CD1 /* Net */; + targetProxy = 5C645C022C591FFA00D8342A /* PBXContainerItemProxy */; + }; 5CE7E79B2C318513006BC92C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5CE7E7892C318512006BC92C /* WidgetsExtension */; diff --git a/Widgets/widgets.swift b/Widgets/widgets.swift index 2f81fac0..9b0cf439 100644 --- a/Widgets/widgets.swift +++ b/Widgets/widgets.swift @@ -15,6 +15,7 @@ import CPU import GPU import RAM import Disk +import Net @main struct WidgetsBundle: WidgetBundle { @@ -23,5 +24,6 @@ struct WidgetsBundle: WidgetBundle { GPUWidget() RAMWidget() DiskWidget() + NetworkWidget() } }