Skip to content

Commit 174fd1b

Browse files
feat: Introduce unified PortRowView component for consistent port display
- Added PortContextMenu for shared context menu actions across port rows. - Created PortRowComponents for reusable UI elements in port rows. - Implemented PortRowView to support multiple display styles (table, nested, menu bar). - Refactored MenuBarPortRow and MenuBarNestedPortRow to utilize PortRowView. - Updated PortDetailView to use process type colors directly from PortInfo. - Removed legacy PortListRow and integrated its functionality into PortRowView.
1 parent 783f239 commit 174fd1b

28 files changed

+1415
-700
lines changed
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Foundation
22

33
extension AppState {
4-
/// Toggles favorite status for a port.
4+
/// Toggles favorite status for a port (delegates to FavoritesState)
55
func toggleFavorite(_ port: Int) {
6-
if favorites.contains(port) { favorites.remove(port) }
7-
else { favorites.insert(port) }
6+
favoritesState.toggle(port)
87
}
98

10-
/// Checks if a port is marked as favorite.
11-
func isFavorite(_ port: Int) -> Bool { favorites.contains(port) }
9+
/// Checks if a port is marked as favorite (delegates to FavoritesState)
10+
func isFavorite(_ port: Int) -> Bool {
11+
favoritesState.isFavorite(port)
12+
}
1213
}
Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,28 @@
11
import Foundation
22

33
extension AppState {
4-
/// Toggles watch status for a port.
4+
/// Toggles watch status for a port (delegates to WatchedPortsState)
55
func toggleWatch(_ port: Int) {
6-
if let idx = watchedPorts.firstIndex(where: { $0.port == port }) {
7-
previousPortStates.removeValue(forKey: port)
8-
watchedPorts.remove(at: idx)
9-
} else {
10-
watchedPorts.append(WatchedPort(port: port))
11-
}
6+
watchedPortsState.toggle(port)
127
}
138

14-
/// Checks if a port is being watched.
15-
func isWatching(_ port: Int) -> Bool { watchedPorts.contains { $0.port == port } }
9+
/// Checks if a port is being watched (delegates to WatchedPortsState)
10+
func isWatching(_ port: Int) -> Bool {
11+
watchedPortsState.isWatching(port)
12+
}
1613

17-
/// Updates notification preferences for a watched port.
14+
/// Updates notification preferences for a watched port (delegates to WatchedPortsState)
1815
func updateWatch(_ port: Int, onStart: Bool, onStop: Bool) {
19-
if let idx = watchedPorts.firstIndex(where: { $0.port == port }) {
20-
watchedPorts[idx].notifyOnStart = onStart
21-
watchedPorts[idx].notifyOnStop = onStop
22-
}
16+
watchedPortsState.updateWatch(port, onStart: onStart, onStop: onStop)
2317
}
2418

25-
/// Removes a watched port by its ID.
19+
/// Removes a watched port by its ID (delegates to WatchedPortsState)
2620
func removeWatch(_ id: UUID) {
27-
if let w = watchedPorts.first(where: { $0.id == id }) {
28-
previousPortStates.removeValue(forKey: w.port)
29-
}
30-
watchedPorts.removeAll { $0.id == id }
21+
watchedPortsState.removeWatch(id)
3122
}
3223

33-
/// Checks watched ports for state changes and triggers notifications.
24+
/// Checks watched ports for state changes and triggers notifications
3425
func checkWatchedPorts() {
35-
let activePorts = Set(ports.map { $0.port })
36-
for w in watchedPorts {
37-
let isActive = activePorts.contains(w.port)
38-
if let wasActive = previousPortStates[w.port] {
39-
if wasActive && !isActive && w.notifyOnStop {
40-
NotificationService.shared.notify(
41-
title: "Port \(w.port) Available",
42-
body: "Port is now free."
43-
)
44-
} else if !wasActive && isActive && w.notifyOnStart {
45-
let name = ports.first { $0.port == w.port }?.processName ?? "Unknown"
46-
NotificationService.shared.notify(
47-
title: "Port \(w.port) In Use",
48-
body: "Used by \(name)."
49-
)
50-
}
51-
}
52-
previousPortStates[w.port] = isActive
53-
}
26+
watchedPortsState.checkForChanges(ports: ports)
5427
}
5528
}

platforms/macos/Sources/AppState.swift

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ extension KeyboardShortcuts.Name {
3535
@Observable
3636
@MainActor
3737
final class AppState {
38+
// MARK: - Decomposed State Objects
39+
40+
/// Manages favorite ports (extracted state)
41+
let favoritesState: FavoritesState
42+
43+
/// Manages watched ports (extracted state)
44+
let watchedPortsState: WatchedPortsState
45+
3846
// MARK: - Port State
3947

4048
/// All currently scanned ports
@@ -114,30 +122,24 @@ final class AppState {
114122
return result
115123
}
116124

117-
// MARK: - Favorites
118-
119-
/// Cached favorites set, synced with UserDefaults
120-
private var _favorites: Set<Int> = Defaults[.favorites] {
121-
didSet { Defaults[.favorites] = _favorites }
122-
}
125+
// MARK: - Backward Compatibility Accessors
123126

124-
/// Port numbers marked as favorites by the user
127+
/// Port numbers marked as favorites by the user (delegates to FavoritesState)
125128
var favorites: Set<Int> {
126-
get { _favorites }
127-
set { _favorites = newValue }
129+
get { favoritesState.favorites }
130+
set { favoritesState.favorites = newValue }
128131
}
129132

130-
// MARK: - Watched Ports
131-
132-
/// Cached watched ports array, synced with UserDefaults
133-
private var _watchedPorts: [WatchedPort] = Defaults[.watchedPorts] {
134-
didSet { Defaults[.watchedPorts] = _watchedPorts }
133+
/// Ports being watched for state changes (delegates to WatchedPortsState)
134+
var watchedPorts: [WatchedPort] {
135+
get { watchedPortsState.watchedPorts }
136+
set { watchedPortsState.watchedPorts = newValue }
135137
}
136138

137-
/// Ports being watched for state changes
138-
var watchedPorts: [WatchedPort] {
139-
get { _watchedPorts }
140-
set { _watchedPorts = newValue }
139+
/// Tracks previous port states for watch notifications (delegates to WatchedPortsState)
140+
var previousPortStates: [Int: Bool] {
141+
get { watchedPortsState.previousPortStates }
142+
set { watchedPortsState.previousPortStates = newValue }
141143
}
142144

143145
// MARK: - Managers
@@ -154,17 +156,22 @@ final class AppState {
154156
// MARK: - Internal Properties (for extensions)
155157

156158
/// Port scanning actor
157-
let scanner = PortScanner()
159+
let scanner: PortScannerProtocol
158160

159161
/// Background task for auto-refresh
160162
@ObservationIgnored var refreshTask: Task<Void, Never>?
161163

162-
/// Tracks previous port states for watch notifications
163-
var previousPortStates: [Int: Bool] = [:]
164-
165164
// MARK: - Initialization
166165

167-
init() {
166+
init(
167+
scanner: PortScannerProtocol = PortScanner(),
168+
favoritesState: FavoritesState? = nil,
169+
watchedPortsState: WatchedPortsState? = nil
170+
) {
171+
self.scanner = scanner
172+
self.favoritesState = favoritesState ?? FavoritesState()
173+
self.watchedPortsState = watchedPortsState ?? WatchedPortsState()
174+
168175
setupKeyboardShortcuts()
169176
startAutoRefresh()
170177
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* ProcessType+Color.swift
3+
* PortKiller
4+
*
5+
* Provides color representation for process types in the UI.
6+
*/
7+
8+
import SwiftUI
9+
10+
extension ProcessType {
11+
/// Color associated with this process type for UI display
12+
var color: Color {
13+
switch self {
14+
case .webServer: return .blue
15+
case .database: return .purple
16+
case .development: return .orange
17+
case .system: return .gray
18+
case .other: return .secondary
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)