Skip to content

Commit 58d4abe

Browse files
committed
feat: add settings UI with refresh rate and app blacklist
- Add AppSettings: UserDefaults-backed observable settings manager - Configurable refresh interval (1/2/3/5/10 seconds) - Minimum traffic threshold filter (All/1KB/10KB/100KB) - App blacklist with add/remove support - Add SettingsView: inline settings panel in dropdown menu - Integrate settings into AppDelegate: - Apply blacklist filter to traffic items - Apply threshold from settings - Auto-restart nettop when refresh interval changes (Combine) - Dynamic menu height via NSMenuDelegate - Add NSLock thread safety to NetworkDetails.update() - Update README: replace screenshots with ASCII art, update feature list - Add localization strings for all new UI elements (en/zh-Hans)
1 parent cdc8596 commit 58d4abe

8 files changed

Lines changed: 384 additions & 38 deletions

File tree

NetworkStatusBar.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
BE3EC932272FA7B8005AC578 /* NetworkStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3EC92F272FA7B8005AC578 /* NetworkStatus.swift */; };
1818
BE3EC933272FA7B8005AC578 /* StatusBarDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3EC930272FA7B8005AC578 /* StatusBarDetailsView.swift */; };
1919
BE3EC935272FA827005AC578 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BE3EC937272FA827005AC578 /* Localizable.strings */; };
20+
AA0001012026021600000001 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001002026021600000001 /* AppSettings.swift */; };
21+
AA0001032026021600000002 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001022026021600000002 /* SettingsView.swift */; };
2022
/* End PBXBuildFile section */
2123

2224
/* Begin PBXContainerItemProxy section */
@@ -55,6 +57,8 @@
5557
BE3EC93E272FC761005AC578 /* statusbar-snapshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "statusbar-snapshot.png"; sourceTree = "<group>"; };
5658
BE3EC93F272FC7A1005AC578 /* details-snapshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "details-snapshot.png"; sourceTree = "<group>"; };
5759
BE3EC940272FC8CD005AC578 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
60+
AA0001002026021600000001 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
61+
AA0001022026021600000002 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5862
/* End PBXFileReference section */
5963

6064
/* Begin PBXFrameworksBuildPhase section */
@@ -109,10 +113,12 @@
109113
isa = PBXGroup;
110114
children = (
111115
BE3EC902272FA6E6005AC578 /* NetworkStatusBarApp.swift */,
116+
AA0001002026021600000001 /* AppSettings.swift */,
112117
BE3EC92D272FA7A2005AC578 /* StatusBarView.swift */,
113118
BE3EC937272FA827005AC578 /* Localizable.strings */,
114119
BE3EC92F272FA7B8005AC578 /* NetworkStatus.swift */,
115120
BE3EC930272FA7B8005AC578 /* StatusBarDetailsView.swift */,
121+
AA0001022026021600000002 /* SettingsView.swift */,
116122
BE3EC906272FA6E7005AC578 /* Assets.xcassets */,
117123
BE3EC90B272FA6E7005AC578 /* NetworkStatusBar.entitlements */,
118124
BE3EC908272FA6E7005AC578 /* Preview Content */,
@@ -290,6 +296,8 @@
290296
BE3EC92E272FA7A2005AC578 /* StatusBarView.swift in Sources */,
291297
BE3EC903272FA6E6005AC578 /* NetworkStatusBarApp.swift in Sources */,
292298
BE3EC933272FA7B8005AC578 /* StatusBarDetailsView.swift in Sources */,
299+
AA0001012026021600000001 /* AppSettings.swift in Sources */,
300+
AA0001032026021600000002 /* SettingsView.swift in Sources */,
293301
);
294302
runOnlyForDeploymentPostprocessing = 0;
295303
};

NetworkStatusBar/AppSettings.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// AppSettings.swift
3+
// NetworkStatusBar
4+
//
5+
// Settings management using UserDefaults with observable properties.
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
final class AppSettings: ObservableObject {
12+
static let shared = AppSettings()
13+
14+
private let defaults = UserDefaults.standard
15+
16+
private enum Keys {
17+
static let refreshInterval = "refreshInterval"
18+
static let blacklist = "blacklist"
19+
static let showInactiveApps = "showInactiveApps"
20+
static let minTrafficThreshold = "minTrafficThreshold"
21+
}
22+
23+
/// Refresh interval in seconds (1-10)
24+
@Published var refreshInterval: Int {
25+
didSet {
26+
defaults.set(refreshInterval, forKey: Keys.refreshInterval)
27+
}
28+
}
29+
30+
/// Blacklisted app names that won't appear in the details list
31+
@Published var blacklist: [String] {
32+
didSet {
33+
defaults.set(blacklist, forKey: Keys.blacklist)
34+
}
35+
}
36+
37+
/// Whether to show apps with zero traffic
38+
@Published var showInactiveApps: Bool {
39+
didSet {
40+
defaults.set(showInactiveApps, forKey: Keys.showInactiveApps)
41+
}
42+
}
43+
44+
/// Minimum traffic threshold in bytes to show an app (default 1024)
45+
@Published var minTrafficThreshold: Int {
46+
didSet {
47+
defaults.set(minTrafficThreshold, forKey: Keys.minTrafficThreshold)
48+
}
49+
}
50+
51+
private init() {
52+
let interval = defaults.integer(forKey: Keys.refreshInterval)
53+
self.refreshInterval = interval > 0 ? interval : 1
54+
55+
self.blacklist = defaults.stringArray(forKey: Keys.blacklist) ?? []
56+
57+
if defaults.object(forKey: Keys.showInactiveApps) != nil {
58+
self.showInactiveApps = defaults.bool(forKey: Keys.showInactiveApps)
59+
} else {
60+
self.showInactiveApps = false
61+
}
62+
63+
let threshold = defaults.integer(forKey: Keys.minTrafficThreshold)
64+
self.minTrafficThreshold = threshold > 0 ? threshold : 1024
65+
}
66+
67+
func addToBlacklist(_ name: String) {
68+
let trimmed = name.trimmingCharacters(in: .whitespaces)
69+
guard !trimmed.isEmpty, !blacklist.contains(trimmed) else { return }
70+
blacklist.append(trimmed)
71+
}
72+
73+
func removeFromBlacklist(_ name: String) {
74+
blacklist.removeAll { $0 == name }
75+
}
76+
77+
func isBlacklisted(_ name: String) -> Bool {
78+
return blacklist.contains(name)
79+
}
80+
}

NetworkStatusBar/NetworkStatus.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ open class NetworkDetails {
2727

2828
var laststate: DataFrame = DataFrame()
2929
var process: Process = Process()
30+
private let lock = NSLock()
3031

3132
init() {
3233
self.prepare()
@@ -75,6 +76,9 @@ open class NetworkDetails {
7576
if data.isEmpty {
7677
return
7778
}
79+
lock.lock()
80+
defer { lock.unlock() }
81+
7882
var dataframe = (try? DataFrame(csvData: data, columns: Columns)) ?? DataFrame.init()
7983
if #available(macOS 13.0, *) {
8084
dataframe.renameColumn("Column 0", to: ColumnName)

NetworkStatusBar/NetworkStatusBarApp.swift

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by fatal cn on 2021/10/31.
66
//
77

8+
import Combine
89
import SwiftUI
910

1011
@main
@@ -23,13 +24,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {
2324
var statusItem: NSStatusItem?
2425

2526
var networkStatus: NetworkDetails = NetworkDetails()
27+
let settings = AppSettings.shared
28+
private var cancellables = Set<AnyCancellable>()
2629

2730
func onUpdate(update: NetworkStates) {
2831
DispatchQueue.main.async {
2932
self.iostates.total = update.total
30-
self.iostates.items = update.items.filter({ item in
31-
return item.total > 1024
32-
})
33+
let threshold = self.settings.minTrafficThreshold
34+
let blacklist = self.settings.blacklist
35+
self.iostates.items = update.items.filter { item in
36+
return item.total >= threshold && !blacklist.contains(item.name)
37+
}
3338
}
3439
}
3540

@@ -41,36 +46,65 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4146
statusItem = NSStatusBar.system.statusItem(withLength: 62)
4247

4348
networkStatus.callback = onUpdate
44-
DispatchQueue.global(qos: .userInteractive).async {
45-
self.networkStatus.run(refreshSeconds: 1)
46-
}
49+
startNetworkMonitoring()
50+
51+
// Watch for refresh interval changes
52+
settings.$refreshInterval
53+
.dropFirst()
54+
.removeDuplicates()
55+
.sink { [weak self] _ in
56+
self?.restartNetworkMonitoring()
57+
}
58+
.store(in: &cancellables)
4759

4860
if let button = statusItem?.button {
4961
let statusbarview = NSHostingView(rootView: StatusBarView(iostates: iostates))
5062
statusbarview.frame = NSRect(x: 0, y: 0, width: 62, height: button.frame.height)
5163
button.addSubview(statusbarview)
5264
}
5365

54-
statusItem?.menu = {
55-
let menu = NSMenu()
56-
menu.items = [
57-
{
58-
let menuitem = NSMenuItem()
59-
let detailsView = StatusBarDetailsView(iostates: iostates)
60-
let hostingView = NSHostingView(rootView: detailsView)
61-
hostingView.setFrameSize(NSSize(width: 280, height: 320))
62-
menuitem.view = hostingView
63-
return menuitem
64-
}(),
65-
NSMenuItem.separator(),
66-
NSMenuItem(
67-
title: NSLocalizedString("quit", comment: "quit the application"),
68-
action: #selector(quit),
69-
keyEquivalent: "q"
70-
),
71-
]
72-
return menu
73-
}()
66+
setupMenu()
67+
}
68+
69+
private func startNetworkMonitoring() {
70+
DispatchQueue.global(qos: .userInteractive).async {
71+
self.networkStatus.run(refreshSeconds: self.settings.refreshInterval)
72+
}
73+
}
74+
75+
private func restartNetworkMonitoring() {
76+
networkStatus.stop()
77+
startNetworkMonitoring()
78+
}
79+
80+
private func setupMenu() {
81+
let menu = NSMenu()
82+
menu.delegate = self
83+
84+
let detailsItem = NSMenuItem()
85+
let detailsView = StatusBarDetailsView(iostates: iostates)
86+
let hostingView = NSHostingView(rootView: detailsView)
87+
hostingView.setFrameSize(NSSize(width: 280, height: 320))
88+
detailsItem.view = hostingView
89+
90+
let settingsItem = NSMenuItem()
91+
let settingsHostingView = NSHostingView(rootView: SettingsView())
92+
settingsHostingView.setFrameSize(NSSize(width: 280, height: 10))
93+
settingsItem.view = settingsHostingView
94+
95+
menu.items = [
96+
detailsItem,
97+
NSMenuItem.separator(),
98+
settingsItem,
99+
NSMenuItem.separator(),
100+
NSMenuItem(
101+
title: NSLocalizedString("quit", comment: "quit the application"),
102+
action: #selector(quit),
103+
keyEquivalent: "q"
104+
),
105+
]
106+
107+
statusItem?.menu = menu
74108
}
75109

76110
func applicationWillTerminate(_ notification: Notification) {
@@ -81,3 +115,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
81115
NSApp.terminate(nil)
82116
}
83117
}
118+
119+
extension AppDelegate: NSMenuDelegate {
120+
func menuWillOpen(_ menu: NSMenu) {
121+
// Resize details view based on content
122+
if let detailsView = menu.items.first?.view as? NSHostingView<StatusBarDetailsView> {
123+
let fittingSize = detailsView.fittingSize
124+
detailsView.setFrameSize(NSSize(width: 280, height: max(fittingSize.height, 80)))
125+
}
126+
// Resize settings view
127+
if menu.items.count > 2,
128+
let settingsView = menu.items[2].view as? NSHostingView<SettingsView>
129+
{
130+
let fittingSize = settingsView.fittingSize
131+
settingsView.setFrameSize(NSSize(width: 280, height: fittingSize.height))
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)