Skip to content

Commit 01c9a1d

Browse files
committed
refactor: simplify settings UI, fix process blacklist labels and traffic list min height
- Rename 'App Blacklist' to 'Process Blacklist' for accuracy - Simplify blacklist UI: remove custom input/regex, keep native Picker only - Set min height (120pt) for traffic list to prevent area from collapsing - Move settings to standalone NSWindow, simplify dropdown menu - Fix 'task already launched' crash by creating new Process per run - Fix 'Publishing changes from within view updates' warning - Clean up unused localization keys - Update README with current UI layout
1 parent f97be93 commit 01c9a1d

7 files changed

Lines changed: 141 additions & 109 deletions

File tree

NetworkStatusBar/NetworkStatusBarApp.swift

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3232
DispatchQueue.main.async {
3333
self.iostates.total = update.total
3434
let threshold = self.settings.minTrafficThreshold
35-
let blacklist = self.settings.blacklist
35+
// Track all seen process names
36+
for item in update.items where !item.name.isEmpty {
37+
self.iostates.seenProcessNames.insert(item.name)
38+
}
3639
self.iostates.items = update.items.filter { item in
37-
return item.total >= threshold && !blacklist.contains(item.name)
40+
return item.total >= threshold && !self.settings.isBlacklisted(item.name)
3841
}
3942
}
4043
}
@@ -53,6 +56,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5356
settings.$refreshInterval
5457
.dropFirst()
5558
.removeDuplicates()
59+
.receive(on: DispatchQueue.main)
5660
.sink { [weak self] _ in
5761
self?.restartNetworkMonitoring()
5862
}
@@ -91,16 +95,24 @@ class AppDelegate: NSObject, NSApplicationDelegate {
9195
menu.items = [
9296
detailsItem,
9397
NSMenuItem.separator(),
94-
NSMenuItem(
95-
title: NSLocalizedString("settings", comment: "Settings"),
96-
action: #selector(openSettings),
97-
keyEquivalent: ","
98-
),
99-
NSMenuItem(
100-
title: NSLocalizedString("quit", comment: "quit the application"),
101-
action: #selector(quit),
102-
keyEquivalent: "q"
103-
),
98+
{
99+
let item = NSMenuItem(
100+
title: NSLocalizedString("settings", comment: "Settings"),
101+
action: #selector(openSettings),
102+
keyEquivalent: ","
103+
)
104+
item.image = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings")
105+
return item
106+
}(),
107+
{
108+
let item = NSMenuItem(
109+
title: NSLocalizedString("quit", comment: "quit the application"),
110+
action: #selector(quit),
111+
keyEquivalent: "q"
112+
)
113+
item.image = NSImage(systemSymbolName: "power", accessibilityDescription: "Quit")
114+
return item
115+
}(),
104116
]
105117

106118
statusItem?.menu = menu
@@ -113,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
113125
return
114126
}
115127

116-
let settingsView = SettingsView()
128+
let settingsView = SettingsView(seenProcessNames: iostates.seenProcessNames)
117129
let hostingController = NSHostingController(rootView: settingsView)
118130

119131
let window = NSWindow(contentViewController: hostingController)

NetworkStatusBar/SettingsView.swift

Lines changed: 70 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,99 +9,108 @@ import SwiftUI
99

1010
struct SettingsView: View {
1111
@ObservedObject var settings = AppSettings.shared
12-
@State private var newBlacklistItem: String = ""
12+
@State private var selectedProcess: String = ""
13+
var seenProcessNames: Set<String> = []
14+
15+
private var availableProcesses: [String] {
16+
let existing = Set(settings.blacklist)
17+
return seenProcessNames.subtracting(existing).sorted()
18+
}
1319

1420
var body: some View {
15-
Form {
16-
Section {
17-
Picker(
18-
NSLocalizedString("refresh_interval", comment: "Refresh Interval"),
19-
selection: $settings.refreshInterval
20-
) {
21+
VStack(alignment: .leading, spacing: 16) {
22+
// Section 1: Refresh interval
23+
GroupBox(label: Label(
24+
NSLocalizedString("refresh_interval", comment: ""),
25+
systemImage: "clock"
26+
)) {
27+
Picker("", selection: $settings.refreshInterval) {
2128
Text("1s").tag(1)
2229
Text("2s").tag(2)
2330
Text("3s").tag(3)
2431
Text("5s").tag(5)
2532
Text("10s").tag(10)
2633
}
2734
.pickerStyle(.segmented)
28-
} header: {
29-
Text(NSLocalizedString("refresh_interval", comment: "Refresh Interval"))
35+
.labelsHidden()
36+
.padding(.top, 4)
3037
}
3138

32-
Section {
33-
Picker(
34-
NSLocalizedString("min_threshold", comment: "Minimum Traffic Threshold"),
35-
selection: $settings.minTrafficThreshold
36-
) {
37-
Text(NSLocalizedString("show_all", comment: "Show All")).tag(0)
39+
// Section 2: Traffic threshold
40+
GroupBox(label: Label(
41+
NSLocalizedString("min_threshold", comment: ""),
42+
systemImage: "speedometer"
43+
)) {
44+
Picker("", selection: $settings.minTrafficThreshold) {
45+
Text(NSLocalizedString("show_all", comment: "")).tag(0)
3846
Text("1 KB/s").tag(1024)
3947
Text("10 KB/s").tag(10240)
4048
Text("100 KB/s").tag(102400)
4149
}
4250
.pickerStyle(.segmented)
43-
} header: {
44-
Text(NSLocalizedString("min_threshold", comment: "Minimum Traffic Threshold"))
51+
.labelsHidden()
52+
.padding(.top, 4)
4553
}
4654

47-
Section {
48-
Text(NSLocalizedString("blacklist_desc", comment: "Blacklist description"))
49-
.font(.callout)
50-
.foregroundColor(.secondary)
51-
52-
HStack {
53-
TextField(
54-
NSLocalizedString("app_name_placeholder", comment: "App name placeholder"),
55-
text: $newBlacklistItem
56-
)
57-
.textFieldStyle(.roundedBorder)
58-
.onSubmit { addBlacklistItem() }
59-
60-
Button(action: addBlacklistItem) {
61-
Image(systemName: "plus.circle.fill")
55+
// Section 3: Process Blacklist
56+
GroupBox(label: Label(
57+
NSLocalizedString("blacklist", comment: ""),
58+
systemImage: "nosign"
59+
)) {
60+
VStack(alignment: .leading, spacing: 8) {
61+
// Pick from seen processes
62+
if !availableProcesses.isEmpty {
63+
Picker(NSLocalizedString("select_process", comment: ""), selection: $selectedProcess) {
64+
Text("").tag("")
65+
ForEach(availableProcesses, id: \.self) { name in
66+
Text(name).tag(name)
67+
}
68+
}
69+
.onChange(of: selectedProcess) { value in
70+
guard !value.isEmpty else { return }
71+
settings.addToBlacklist(value)
72+
selectedProcess = ""
73+
}
6274
}
63-
.disabled(newBlacklistItem.trimmingCharacters(in: .whitespaces).isEmpty)
64-
}
6575

66-
if settings.blacklist.isEmpty {
67-
Text(NSLocalizedString("blacklist_empty", comment: "No blacklisted apps"))
68-
.foregroundColor(.secondary)
69-
.italic()
70-
} else {
71-
List {
72-
ForEach(settings.blacklist, id: \.self) { name in
73-
HStack {
74-
Text(name)
75-
Spacer()
76-
Button(action: { settings.removeFromBlacklist(name) }) {
77-
Image(systemName: "trash")
78-
.foregroundColor(.red)
76+
// Blacklist items
77+
if settings.blacklist.isEmpty {
78+
Text(NSLocalizedString("blacklist_empty", comment: ""))
79+
.foregroundColor(.secondary)
80+
.italic()
81+
.padding(.vertical, 2)
82+
} else {
83+
VStack(spacing: 2) {
84+
ForEach(settings.blacklist, id: \.self) { name in
85+
HStack {
86+
Text(name)
87+
.lineLimit(1)
88+
Spacer()
89+
Button(action: { settings.removeFromBlacklist(name) }) {
90+
Image(systemName: "xmark.circle.fill")
91+
.foregroundColor(.secondary)
92+
}
93+
.buttonStyle(.plain)
7994
}
80-
.buttonStyle(.plain)
95+
.padding(.horizontal, 8)
96+
.padding(.vertical, 4)
97+
.background(Color.primary.opacity(0.04))
98+
.cornerRadius(4)
8199
}
82100
}
83101
}
84-
.frame(minHeight: 60, maxHeight: 160)
85102
}
86-
} header: {
87-
Text(NSLocalizedString("blacklist", comment: "App Blacklist"))
103+
.padding(.top, 4)
88104
}
89105
}
90-
.navigationTitle(NSLocalizedString("settings", comment: "Settings"))
91-
.frame(width: 400, height: 420)
106+
.padding(20)
107+
.frame(width: 360)
92108
.fixedSize()
93109
}
94-
95-
private func addBlacklistItem() {
96-
let trimmed = newBlacklistItem.trimmingCharacters(in: .whitespaces)
97-
guard !trimmed.isEmpty else { return }
98-
settings.addToBlacklist(trimmed)
99-
newBlacklistItem = ""
100-
}
101110
}
102111

103112
struct SettingsView_Previews: PreviewProvider {
104113
static var previews: some View {
105-
SettingsView()
114+
SettingsView(seenProcessNames: ["Chrome", "Slack", "Terminal", "ClashX", "Surge", "Safari"])
106115
}
107116
}

NetworkStatusBar/StatusBarDetailsView.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,22 @@ struct StatusBarDetailsView: View {
4141
.padding(.horizontal, 8)
4242

4343
// Process list
44-
if iostates.items.isEmpty {
45-
Text(NSLocalizedString("no_activity", comment: "No network activity"))
46-
.foregroundColor(.secondary)
47-
.font(.system(size: 11))
48-
.frame(maxWidth: .infinity, alignment: .center)
49-
.padding(.vertical, 20)
50-
} else {
51-
ScrollView {
52-
VStack(spacing: 0) {
44+
ScrollView {
45+
VStack(spacing: 0) {
46+
if iostates.items.isEmpty {
47+
Text(NSLocalizedString("no_activity", comment: "No network activity"))
48+
.foregroundColor(.secondary)
49+
.font(.system(size: 11))
50+
.frame(maxWidth: .infinity, alignment: .center)
51+
.padding(.vertical, 20)
52+
} else {
5353
ForEach(iostates.items) { item in
5454
StatusBarDetailsItemView(state: item)
5555
}
5656
}
5757
}
58-
.frame(maxHeight: 260)
5958
}
59+
.frame(minHeight: 120, maxHeight: 260)
6060
}
6161
.frame(width: 280)
6262
}

NetworkStatusBar/StatusBarView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import SwiftUI
1010
class IOStates: ObservableObject {
1111
@Published var total: NetworkState = NetworkState()
1212
@Published var items: [NetworkState] = []
13+
@Published var seenProcessNames: Set<String> = []
1314
}
1415

1516
struct StatusBarView: View {

NetworkStatusBar/en.lproj/Localizable.strings

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"refresh_interval"="Refresh Interval";
1313
"min_threshold"="Min Traffic Threshold";
1414
"show_all"="All";
15-
"blacklist"="App Blacklist";
16-
"blacklist_desc"="Excluded apps will not appear in the traffic list.";
17-
"app_name_placeholder"="Enter app name...";
18-
"blacklist_empty"="No blacklisted apps";
15+
"blacklist"="Process Blacklist";
16+
"blacklist_empty"="No blacklisted processes";
17+
"select_process"="Select process";

NetworkStatusBar/zh-Hans.lproj/Localizable.strings

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"refresh_interval"="刷新频率";
1313
"min_threshold"="最低流量阈值";
1414
"show_all"="全部";
15-
"blacklist"="应用黑名单";
16-
"blacklist_desc"="被排除的应用将不会出现在流量列表中。";
17-
"app_name_placeholder"="输入应用名称...";
18-
"blacklist_empty"="暂无黑名单应用";
15+
"blacklist"="进程黑名单";
16+
"blacklist_empty"="暂无黑名单进程";
17+
"select_process"="选择进程";

README.md

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Network Status Bar
22

3-
A macOS menu bar tool for monitoring network traffic, written in **SwiftUI**.
3+
A macOS menu bar tool for monitoring real-time network traffic per process, written in **SwiftUI**.
44

55
## Preview
66

@@ -13,7 +13,7 @@ A macOS menu bar tool for monitoring network traffic, written in **SwiftUI**.
1313
└──────────────────────────────────────┘
1414
```
1515

16-
### Dropdown Details
16+
### Dropdown Menu
1717

1818
```
1919
┌────────────────────────────────┐
@@ -27,34 +27,46 @@ A macOS menu bar tool for monitoring network traffic, written in **SwiftUI**.
2727
│ Terminal ↑ 1.2KB/s │
2828
│ ↓ 0.2KB/s │
2929
├────────────────────────────────┤
30-
│ ⚙ Settings │
30+
│ ⚙ Settings ⌘, │
31+
│ ⏻ Quit ⌘Q │
32+
└────────────────────────────────┘
33+
```
34+
35+
### Settings Window
36+
37+
```
38+
┌─ Settings ─────────────────────┐
3139
│ │
32-
│ Refresh Interval │
33-
│ [1s] [2s] [3s] [5s] [10s] │
40+
│ 🕐 Refresh Interval │
41+
│ ┌────────────────────────┐ │
42+
│ │ 1s 2s 3s 5s 10s │ │
43+
│ └────────────────────────┘ │
3444
│ │
35-
│ Min Traffic Threshold │
36-
│ [All] [1KB] [10KB] [100KB] │
45+
│ 🏎 Min Traffic Threshold │
46+
│ ┌────────────────────────┐ │
47+
│ │ All 1KB 10KB 100KB │ │
48+
│ └────────────────────────┘ │
3749
│ │
38-
App Blacklist
39-
│ ┌──────────────────┐ [+]
40-
│ │ Enter app name...│
41-
│ └──────────────────
50+
🚫 Process Blacklist
51+
│ ┌────────────────────────┐
52+
│ │ Select process [▾] │
53+
│ └────────────────────────┘
4254
│ ┌─────────────────────┐ │
43-
│ │ ClashX [x] │ │
44-
│ │ Surge [x] │ │
55+
│ │ ClashX (x) │ │
56+
│ │ Surge (x) │ │
4557
│ └─────────────────────┘ │
46-
├────────────────────────────────┤
47-
│ Quit ⌘Q │
58+
│ │
4859
└────────────────────────────────┘
4960
```
5061

5162
## Features
5263

5364
- [x] SwiftUI based macOS menu bar app
5465
- [x] Real-time network upload/download speed on status bar
55-
- [x] Per-application network traffic in dropdown menu
66+
- [x] Per-process network traffic breakdown in dropdown menu
5667
- [x] Configurable refresh interval (1s / 2s / 3s / 5s / 10s)
5768
- [x] Configurable minimum traffic threshold filter
58-
- [x] App blacklist to exclude specific applications (e.g. proxies)
69+
- [x] Process blacklist — select from detected processes to exclude (e.g. proxies)
70+
- [x] Standalone settings window (⌘,)
5971
- [x] Settings persist via UserDefaults
6072
- [x] Localization support (English / 中文)

0 commit comments

Comments
 (0)