204 lines
7.0 KiB
Swift
204 lines
7.0 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
/// The top bar panel shown from the menu bar icon.
|
|
struct TopBarView: View {
|
|
@ObservedObject var model: AppListModel
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
header
|
|
Divider()
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
section(title: "Apps", apps: model.regularApps, emptyText: "No apps running")
|
|
|
|
Divider().padding(.vertical, 4)
|
|
|
|
backgroundSection
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
.frame(maxHeight: 360)
|
|
|
|
Divider()
|
|
footer
|
|
}
|
|
.frame(width: 320)
|
|
}
|
|
|
|
private var header: some View {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "menubar.rectangle")
|
|
.foregroundStyle(.secondary)
|
|
Text("SimpleTopBar")
|
|
.font(.headline)
|
|
Spacer()
|
|
Button {
|
|
model.refresh()
|
|
} label: {
|
|
Image(systemName: "arrow.clockwise")
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Refresh")
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
private func section(title: String, apps: [RunningApp], emptyText: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text(title.uppercased())
|
|
.font(.caption2)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.horizontal, 12)
|
|
.padding(.top, 4)
|
|
.padding(.bottom, 2)
|
|
|
|
if apps.isEmpty {
|
|
Text(emptyText)
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 4)
|
|
} else {
|
|
ForEach(apps) { app in
|
|
AppRow(app: app, model: model)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collapsible dropdown for background apps. When collapsed it is a single
|
|
/// header row; when expanded it reveals a limited number of apps and hides
|
|
/// the overflow behind "Show N more".
|
|
private var backgroundSection: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Button {
|
|
model.toggleBackgroundExpanded()
|
|
} label: {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: model.backgroundExpanded ? "chevron.down" : "chevron.right")
|
|
.font(.caption2)
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: 10)
|
|
Text("Background Apps")
|
|
.font(.caption2)
|
|
.fontWeight(.semibold)
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Text("\(model.backgroundApps.count)")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 1)
|
|
.background(Capsule().fill(Color.secondary.opacity(0.15)))
|
|
}
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 4)
|
|
.help(model.backgroundExpanded ? "Collapse background apps" : "Expand background apps")
|
|
|
|
if model.backgroundExpanded {
|
|
if model.backgroundApps.isEmpty {
|
|
Text("No background apps")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 4)
|
|
} else {
|
|
ForEach(model.visibleBackgroundApps) { app in
|
|
AppRow(app: app, model: model)
|
|
}
|
|
|
|
if model.canShowMoreBackgroundApps {
|
|
Button {
|
|
model.showMoreBackgroundApps()
|
|
} label: {
|
|
Label(
|
|
"Show \(model.hiddenBackgroundCount) more",
|
|
systemImage: "ellipsis.circle"
|
|
)
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var footer: some View {
|
|
HStack(spacing: 8) {
|
|
Text("\(model.regularApps.count) apps · \(model.backgroundApps.count) background")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tertiary)
|
|
Spacer()
|
|
Button(LoginItem.isEnabled ? "Disable Launch at Login" : "Launch at Login") {
|
|
if !LoginItem.toggle() {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Could not update Launch at Login"
|
|
alert.informativeText = "Launch at Login requires a signed app bundle in /Applications. It is not available when running via `swift run`."
|
|
alert.alertStyle = .warning
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
}
|
|
.font(.caption2)
|
|
.buttonStyle(.plain)
|
|
|
|
Button("Quit") { NSApp.terminate(nil) }
|
|
.font(.caption2)
|
|
.buttonStyle(.plain)
|
|
.keyboardShortcut("q")
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
}
|
|
}
|
|
|
|
private struct AppRow: View {
|
|
let app: RunningApp
|
|
@ObservedObject var model: AppListModel
|
|
|
|
var body: some View {
|
|
Button {
|
|
model.activate(app)
|
|
} label: {
|
|
HStack(spacing: 8) {
|
|
Image(nsImage: app.icon)
|
|
.resizable()
|
|
.frame(width: 18, height: 18)
|
|
Text(app.name)
|
|
.font(.system(size: 12))
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
if app.isActive {
|
|
Image(systemName: "circle.fill")
|
|
.font(.system(size: 5))
|
|
.foregroundStyle(Color.accentColor)
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
.contentShape(Rectangle())
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 3)
|
|
.opacity(app.isHidden ? 0.55 : 1)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Activate \(app.name)")
|
|
.contextMenu {
|
|
Button("Activate") { model.activate(app) }
|
|
Button(app.isHidden ? "Unhide" : "Hide") { model.toggleHidden(app) }
|
|
Divider()
|
|
Button("Quit \(app.name)") { model.quit(app) }
|
|
}
|
|
}
|
|
}
|