Refactor and Rename to CommandNotch
This commit is contained in:
385
Downterm/CommandNotch/Views/SettingsView.swift
Normal file
385
Downterm/CommandNotch/Views/SettingsView.swift
Normal file
@@ -0,0 +1,385 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Tabbed settings panel with General, Appearance, Animation, Terminal, Hotkeys, and About.
|
||||
struct SettingsView: View {
|
||||
|
||||
@State private var selectedTab: SettingsTab = .general
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List(SettingsTab.allCases, selection: $selectedTab) { tab in
|
||||
Label(tab.label, systemImage: tab.icon)
|
||||
.tag(tab)
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
||||
} detail: {
|
||||
ScrollView {
|
||||
detailView.padding()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.frame(minWidth: 600, minHeight: 400)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var detailView: some View {
|
||||
switch selectedTab {
|
||||
case .general: GeneralSettingsView()
|
||||
case .appearance: AppearanceSettingsView()
|
||||
case .animation: AnimationSettingsView()
|
||||
case .terminal: TerminalSettingsView()
|
||||
case .hotkeys: HotkeySettingsView()
|
||||
case .about: AboutSettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tabs
|
||||
|
||||
enum SettingsTab: String, CaseIterable, Identifiable {
|
||||
case general, appearance, animation, terminal, hotkeys, about
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .general: return "General"
|
||||
case .appearance: return "Appearance"
|
||||
case .animation: return "Animation"
|
||||
case .terminal: return "Terminal"
|
||||
case .hotkeys: return "Hotkeys"
|
||||
case .about: return "About"
|
||||
}
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
switch self {
|
||||
case .general: return "gearshape"
|
||||
case .appearance: return "paintbrush"
|
||||
case .animation: return "bolt.fill"
|
||||
case .terminal: return "terminal"
|
||||
case .hotkeys: return "keyboard"
|
||||
case .about: return "info.circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
||||
struct GeneralSettingsView: View {
|
||||
|
||||
@AppStorage(NotchSettings.Keys.showOnAllDisplays) private var showOnAllDisplays = NotchSettings.Defaults.showOnAllDisplays
|
||||
@AppStorage(NotchSettings.Keys.openNotchOnHover) private var openNotchOnHover = NotchSettings.Defaults.openNotchOnHover
|
||||
@AppStorage(NotchSettings.Keys.minimumHoverDuration) private var minimumHoverDuration = NotchSettings.Defaults.minimumHoverDuration
|
||||
@AppStorage(NotchSettings.Keys.showMenuBarIcon) private var showMenuBarIcon = NotchSettings.Defaults.showMenuBarIcon
|
||||
@AppStorage(NotchSettings.Keys.launchAtLogin) private var launchAtLogin = NotchSettings.Defaults.launchAtLogin
|
||||
@AppStorage(NotchSettings.Keys.enableGestures) private var enableGestures = NotchSettings.Defaults.enableGestures
|
||||
@AppStorage(NotchSettings.Keys.gestureSensitivity) private var gestureSensitivity = NotchSettings.Defaults.gestureSensitivity
|
||||
|
||||
@AppStorage(NotchSettings.Keys.notchHeightMode) private var notchHeightMode = NotchSettings.Defaults.notchHeightMode
|
||||
@AppStorage(NotchSettings.Keys.notchHeight) private var notchHeight = NotchSettings.Defaults.notchHeight
|
||||
@AppStorage(NotchSettings.Keys.nonNotchHeightMode) private var nonNotchHeightMode = NotchSettings.Defaults.nonNotchHeightMode
|
||||
@AppStorage(NotchSettings.Keys.nonNotchHeight) private var nonNotchHeight = NotchSettings.Defaults.nonNotchHeight
|
||||
|
||||
@AppStorage(NotchSettings.Keys.openWidth) private var openWidth = NotchSettings.Defaults.openWidth
|
||||
@AppStorage(NotchSettings.Keys.openHeight) private var openHeight = NotchSettings.Defaults.openHeight
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Display") {
|
||||
Toggle("Show on all displays", isOn: $showOnAllDisplays)
|
||||
Toggle("Show menu bar icon", isOn: $showMenuBarIcon)
|
||||
Toggle("Launch at login", isOn: $launchAtLogin)
|
||||
.onChange(of: launchAtLogin) { _, newValue in
|
||||
LaunchAtLoginHelper.setEnabled(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Hover Behavior") {
|
||||
Toggle("Open notch on hover", isOn: $openNotchOnHover)
|
||||
if openNotchOnHover {
|
||||
HStack {
|
||||
Text("Hover delay")
|
||||
Slider(value: $minimumHoverDuration, in: 0.0...2.0, step: 0.05)
|
||||
Text(String(format: "%.2fs", minimumHoverDuration))
|
||||
.monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Gestures") {
|
||||
Toggle("Enable gestures", isOn: $enableGestures)
|
||||
if enableGestures {
|
||||
HStack {
|
||||
Text("Sensitivity")
|
||||
Slider(value: $gestureSensitivity, in: 0.1...1.0, step: 0.05)
|
||||
Text(String(format: "%.2f", gestureSensitivity))
|
||||
.monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Closed Notch Size") {
|
||||
Picker("Notch screens", selection: $notchHeightMode) {
|
||||
ForEach(NotchHeightMode.allCases) { Text($0.label).tag($0.rawValue) }
|
||||
}
|
||||
if notchHeightMode == NotchHeightMode.custom.rawValue {
|
||||
HStack {
|
||||
Text("Custom height")
|
||||
Slider(value: $notchHeight, in: 16...64, step: 1)
|
||||
Text("\(Int(notchHeight))pt").monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
Picker("Non-notch screens", selection: $nonNotchHeightMode) {
|
||||
ForEach(NonNotchHeightMode.allCases) { Text($0.label).tag($0.rawValue) }
|
||||
}
|
||||
if nonNotchHeightMode == NonNotchHeightMode.custom.rawValue {
|
||||
HStack {
|
||||
Text("Custom height")
|
||||
Slider(value: $nonNotchHeight, in: 16...64, step: 1)
|
||||
Text("\(Int(nonNotchHeight))pt").monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Open Notch Size") {
|
||||
HStack {
|
||||
Text("Width")
|
||||
Slider(value: $openWidth, in: 300...1200, step: 10)
|
||||
Text("\(Int(openWidth))pt").monospacedDigit().frame(width: 60)
|
||||
}
|
||||
HStack {
|
||||
Text("Height")
|
||||
Slider(value: $openHeight, in: 100...600, step: 10)
|
||||
Text("\(Int(openHeight))pt").monospacedDigit().frame(width: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Appearance
|
||||
|
||||
struct AppearanceSettingsView: View {
|
||||
|
||||
@AppStorage(NotchSettings.Keys.enableShadow) private var enableShadow = NotchSettings.Defaults.enableShadow
|
||||
@AppStorage(NotchSettings.Keys.shadowRadius) private var shadowRadius = NotchSettings.Defaults.shadowRadius
|
||||
@AppStorage(NotchSettings.Keys.shadowOpacity) private var shadowOpacity = NotchSettings.Defaults.shadowOpacity
|
||||
@AppStorage(NotchSettings.Keys.cornerRadiusScaling) private var cornerRadiusScaling = NotchSettings.Defaults.cornerRadiusScaling
|
||||
@AppStorage(NotchSettings.Keys.notchOpacity) private var notchOpacity = NotchSettings.Defaults.notchOpacity
|
||||
@AppStorage(NotchSettings.Keys.blurRadius) private var blurRadius = NotchSettings.Defaults.blurRadius
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Shadow") {
|
||||
Toggle("Enable shadow", isOn: $enableShadow)
|
||||
if enableShadow {
|
||||
HStack {
|
||||
Text("Radius")
|
||||
Slider(value: $shadowRadius, in: 0...30, step: 1)
|
||||
Text(String(format: "%.0f", shadowRadius)).monospacedDigit().frame(width: 40)
|
||||
}
|
||||
HStack {
|
||||
Text("Opacity")
|
||||
Slider(value: $shadowOpacity, in: 0...1, step: 0.05)
|
||||
Text(String(format: "%.2f", shadowOpacity)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Shape") {
|
||||
Toggle("Scale corner radii when open", isOn: $cornerRadiusScaling)
|
||||
}
|
||||
Section("Opacity & Blur") {
|
||||
HStack {
|
||||
Text("Notch opacity")
|
||||
Slider(value: $notchOpacity, in: 0...1, step: 0.05)
|
||||
Text(String(format: "%.2f", notchOpacity)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
HStack {
|
||||
Text("Blur radius")
|
||||
Slider(value: $blurRadius, in: 0...20, step: 0.5)
|
||||
Text(String(format: "%.1f", blurRadius)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animation
|
||||
|
||||
struct AnimationSettingsView: View {
|
||||
|
||||
@AppStorage(NotchSettings.Keys.openSpringResponse) private var openResponse = NotchSettings.Defaults.openSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.openSpringDamping) private var openDamping = NotchSettings.Defaults.openSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeResponse = NotchSettings.Defaults.closeSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringResponse) private var hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
@AppStorage(NotchSettings.Keys.hoverSpringDamping) private var hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Open Animation") {
|
||||
springControls(response: $openResponse, damping: $openDamping)
|
||||
}
|
||||
Section("Close Animation") {
|
||||
springControls(response: $closeResponse, damping: $closeDamping)
|
||||
}
|
||||
Section("Hover Animation") {
|
||||
springControls(response: $hoverResponse, damping: $hoverDamping)
|
||||
}
|
||||
Section {
|
||||
Button("Reset to Defaults") {
|
||||
openResponse = NotchSettings.Defaults.openSpringResponse
|
||||
openDamping = NotchSettings.Defaults.openSpringDamping
|
||||
closeResponse = NotchSettings.Defaults.closeSpringResponse
|
||||
closeDamping = NotchSettings.Defaults.closeSpringDamping
|
||||
hoverResponse = NotchSettings.Defaults.hoverSpringResponse
|
||||
hoverDamping = NotchSettings.Defaults.hoverSpringDamping
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func springControls(response: Binding<Double>, damping: Binding<Double>) -> some View {
|
||||
HStack {
|
||||
Text("Response")
|
||||
Slider(value: response, in: 0.1...1.5, step: 0.01)
|
||||
Text(String(format: "%.2f", response.wrappedValue)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
HStack {
|
||||
Text("Damping")
|
||||
Slider(value: damping, in: 0.1...1.5, step: 0.01)
|
||||
Text(String(format: "%.2f", damping.wrappedValue)).monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal
|
||||
|
||||
struct TerminalSettingsView: View {
|
||||
|
||||
@AppStorage(NotchSettings.Keys.terminalFontSize) private var fontSize = NotchSettings.Defaults.terminalFontSize
|
||||
@AppStorage(NotchSettings.Keys.terminalShell) private var shellPath = NotchSettings.Defaults.terminalShell
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Font") {
|
||||
HStack {
|
||||
Text("Font size")
|
||||
Slider(value: $fontSize, in: 8...28, step: 1)
|
||||
Text("\(Int(fontSize))pt").monospacedDigit().frame(width: 50)
|
||||
}
|
||||
}
|
||||
Section("Shell") {
|
||||
TextField("Shell path (empty = $SHELL)", text: $shellPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("Leave empty to use your default shell ($SHELL or /bin/zsh).")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hotkeys
|
||||
|
||||
struct HotkeySettingsView: View {
|
||||
|
||||
@State private var toggleBinding = loadBinding(NotchSettings.Keys.hotkeyToggle, fallback: .cmdReturn)
|
||||
@State private var newTabBinding = loadBinding(NotchSettings.Keys.hotkeyNewTab, fallback: .cmdT)
|
||||
@State private var closeTabBinding = loadBinding(NotchSettings.Keys.hotkeyCloseTab, fallback: .cmdW)
|
||||
@State private var nextTabBinding = loadBinding(NotchSettings.Keys.hotkeyNextTab, fallback: .cmdShiftRB)
|
||||
@State private var prevTabBinding = loadBinding(NotchSettings.Keys.hotkeyPreviousTab, fallback: .cmdShiftLB)
|
||||
@State private var detachBinding = loadBinding(NotchSettings.Keys.hotkeyDetachTab, fallback: .cmdD)
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("Global") {
|
||||
HotkeyRecorderView(label: "Toggle notch", binding: bindAndSave($toggleBinding, key: NotchSettings.Keys.hotkeyToggle))
|
||||
}
|
||||
|
||||
Section("Terminal Tabs (active when notch is open)") {
|
||||
HotkeyRecorderView(label: "New tab", binding: bindAndSave($newTabBinding, key: NotchSettings.Keys.hotkeyNewTab))
|
||||
HotkeyRecorderView(label: "Close tab", binding: bindAndSave($closeTabBinding, key: NotchSettings.Keys.hotkeyCloseTab))
|
||||
HotkeyRecorderView(label: "Next tab", binding: bindAndSave($nextTabBinding, key: NotchSettings.Keys.hotkeyNextTab))
|
||||
HotkeyRecorderView(label: "Previous tab", binding: bindAndSave($prevTabBinding, key: NotchSettings.Keys.hotkeyPreviousTab))
|
||||
HotkeyRecorderView(label: "Detach tab", binding: bindAndSave($detachBinding, key: NotchSettings.Keys.hotkeyDetachTab))
|
||||
}
|
||||
|
||||
Section {
|
||||
Text("⌘1–9 always switch to tab by number.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Reset to Defaults") {
|
||||
toggleBinding = .cmdReturn
|
||||
newTabBinding = .cmdT
|
||||
closeTabBinding = .cmdW
|
||||
nextTabBinding = .cmdShiftRB
|
||||
prevTabBinding = .cmdShiftLB
|
||||
detachBinding = .cmdD
|
||||
|
||||
save(.cmdReturn, key: NotchSettings.Keys.hotkeyToggle)
|
||||
save(.cmdT, key: NotchSettings.Keys.hotkeyNewTab)
|
||||
save(.cmdW, key: NotchSettings.Keys.hotkeyCloseTab)
|
||||
save(.cmdShiftRB, key: NotchSettings.Keys.hotkeyNextTab)
|
||||
save(.cmdShiftLB, key: NotchSettings.Keys.hotkeyPreviousTab)
|
||||
save(.cmdD, key: NotchSettings.Keys.hotkeyDetachTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
}
|
||||
|
||||
/// Creates a binding that saves to UserDefaults on every change.
|
||||
private func bindAndSave(_ state: Binding<HotkeyBinding>, key: String) -> Binding<HotkeyBinding> {
|
||||
Binding(
|
||||
get: { state.wrappedValue },
|
||||
set: { newValue in
|
||||
state.wrappedValue = newValue
|
||||
save(newValue, key: key)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func save(_ binding: HotkeyBinding, key: String) {
|
||||
UserDefaults.standard.set(binding.toJSON(), forKey: key)
|
||||
}
|
||||
|
||||
private static func loadBinding(_ key: String, fallback: HotkeyBinding) -> HotkeyBinding {
|
||||
guard let json = UserDefaults.standard.string(forKey: key),
|
||||
let b = HotkeyBinding.fromJSON(json) else { return fallback }
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - About
|
||||
|
||||
struct AboutSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "terminal")
|
||||
.font(.system(size: 64))
|
||||
.foregroundStyle(.secondary)
|
||||
Text("CommandNotch")
|
||||
.font(.largeTitle.bold())
|
||||
Text("Version 0.3.0")
|
||||
.foregroundStyle(.secondary)
|
||||
Text("A drop-down terminal that lives in your notch.")
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user