197 lines
7.4 KiB
Swift
197 lines
7.4 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
struct ContentView: View {
|
|
@Environment(SystemMonitor.self) private var monitor
|
|
@State private var selection: TaskSection?
|
|
@State private var searchText = ""
|
|
@State private var showingAbout = false
|
|
@State private var showingNewTask = false
|
|
@State private var detailSelection: Int32?
|
|
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
|
@AppStorage("sidebarCompact") private var sidebarCompact = false
|
|
|
|
init() {
|
|
let stored = UserDefaults.standard.string(forKey: "defaultStartPage") ?? TaskSection.processes.rawValue
|
|
_selection = State(initialValue: TaskSection(rawValue: stored) ?? .processes)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationSplitView(columnVisibility: $columnVisibility) {
|
|
Sidebar(selection: $selection, isCompact: sidebarCompact)
|
|
.navigationSplitViewColumnWidth(
|
|
min: sidebarCompact ? 68 : 190,
|
|
ideal: sidebarCompact ? 68 : 210,
|
|
max: sidebarCompact ? 68 : 250
|
|
)
|
|
} detail: {
|
|
Group {
|
|
switch selection ?? .processes {
|
|
case .processes:
|
|
ProcessesView(searchText: searchText) { process in
|
|
detailSelection = process.pid
|
|
selection = .details
|
|
}
|
|
case .performance:
|
|
PerformanceView()
|
|
case .history:
|
|
AppHistoryView(searchText: searchText) { process in
|
|
detailSelection = process.pid
|
|
selection = .details
|
|
}
|
|
case .startup:
|
|
StartupAppsView()
|
|
case .users:
|
|
UsersView { process in
|
|
detailSelection = process.pid
|
|
selection = .details
|
|
}
|
|
case .details:
|
|
DetailsView(searchText: searchText, selection: $detailSelection)
|
|
case .services:
|
|
ServicesView(searchText: searchText) { process in
|
|
detailSelection = process.pid
|
|
selection = .details
|
|
}
|
|
case .settings:
|
|
SettingsView()
|
|
}
|
|
}
|
|
.searchable(text: $searchText, placement: .toolbar, prompt: "Type a name, PID, or user")
|
|
.toolbar {
|
|
TaskToolbarContent(showingNewTask: $showingNewTask, showingAbout: $showingAbout)
|
|
}
|
|
}
|
|
.navigationTitle("Task Manager")
|
|
.animation(.snappy(duration: 0.22), value: sidebarCompact)
|
|
.onChange(of: columnVisibility) { _, visibility in
|
|
guard visibility == .detailOnly else { return }
|
|
columnVisibility = .all
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
|
|
withAnimation(.snappy(duration: 0.22)) { sidebarCompact.toggle() }
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingNewTask) {
|
|
NewTaskView(isPresented: $showingNewTask)
|
|
}
|
|
.alert("Task Manager", isPresented: Binding(
|
|
get: { monitor.errorMessage != nil },
|
|
set: { if !$0 { monitor.errorMessage = nil } }
|
|
)) {
|
|
Button("OK", role: .cancel) { monitor.errorMessage = nil }
|
|
} message: {
|
|
Text(monitor.errorMessage ?? "")
|
|
}
|
|
.alert("Task Manager for macOS", isPresented: $showingAbout) {
|
|
Button("OK", role: .cancel) { }
|
|
} message: {
|
|
Text("A native SwiftUI system monitor inspired by Windows Task Manager.")
|
|
}
|
|
.onAppear {
|
|
DispatchQueue.main.async {
|
|
NSApp.keyWindow?.level = UserDefaults.standard.bool(forKey: "alwaysOnTop") ? .floating : .normal
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct TaskToolbarContent: ToolbarContent {
|
|
@Environment(SystemMonitor.self) private var monitor
|
|
@Binding var showingNewTask: Bool
|
|
@Binding var showingAbout: Bool
|
|
|
|
var body: some ToolbarContent {
|
|
@Bindable var monitor = monitor
|
|
ToolbarItemGroup(placement: .primaryAction) {
|
|
Button {
|
|
showingNewTask = true
|
|
} label: {
|
|
Label("Run new task", systemImage: "plus.square")
|
|
}
|
|
.help("Launch an application or executable")
|
|
|
|
Button {
|
|
monitor.refresh()
|
|
} label: {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
}
|
|
.help("Refresh now (⌘R)")
|
|
|
|
Menu {
|
|
UpdateSpeedPicker(selection: $monitor.updateSpeed)
|
|
Divider()
|
|
Button("About Task Manager") { showingAbout = true }
|
|
} label: {
|
|
Image(systemName: "ellipsis")
|
|
}
|
|
.menuStyle(.borderlessButton)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct Sidebar: View {
|
|
@Binding var selection: TaskSection?
|
|
let isCompact: Bool
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
ScrollView {
|
|
VStack(spacing: 4) {
|
|
ForEach(TaskSection.allCases.filter { $0 != .settings }) { section in
|
|
Button {
|
|
selection = section
|
|
} label: {
|
|
sidebarLabel(section.rawValue, icon: section.icon)
|
|
.padding(.horizontal, isCompact ? 0 : 8)
|
|
.frame(height: 40)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.frame(maxWidth: .infinity)
|
|
.background(selection == section ? Color.accentColor.opacity(0.18) : .clear)
|
|
.clipShape(RoundedRectangle(cornerRadius: 7))
|
|
.help(section.rawValue)
|
|
.accessibilityLabel(section.rawValue)
|
|
.accessibilityAddTraits(selection == section ? .isSelected : [])
|
|
}
|
|
}
|
|
.padding(.horizontal, isCompact ? 8 : 10)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
Divider().padding(.horizontal, isCompact ? 12 : 16)
|
|
|
|
Button {
|
|
selection = .settings
|
|
} label: {
|
|
sidebarLabel("Settings", icon: "gearshape")
|
|
.padding(.horizontal, isCompact ? 0 : 8)
|
|
.frame(height: 40)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.font(.callout)
|
|
.frame(maxWidth: .infinity)
|
|
.background(selection == .settings ? Color.accentColor.opacity(0.18) : .clear)
|
|
.clipShape(RoundedRectangle(cornerRadius: 7))
|
|
.padding(.horizontal, isCompact ? 8 : 10)
|
|
.padding(.vertical, 8)
|
|
.foregroundStyle(selection == .settings ? Color.primary : Color.secondary)
|
|
.help("Settings")
|
|
.accessibilityLabel("Settings")
|
|
}
|
|
}
|
|
|
|
private func sidebarLabel(_ title: String, icon: String) -> some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: icon)
|
|
.frame(width: 22, height: 20)
|
|
if !isCompact {
|
|
Text(title).lineLimit(1)
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: isCompact ? .center : .leading)
|
|
}
|
|
}
|