beatmatchr/Desktop/YabaiPro/Sources/SettingsView.swift
BusyBee3333 7694d965c9 feat: Add structured signal editor with app dropdown and action builder
- Add AppDiscovery provider for running app enumeration
- Implement AppDropdownView with auto-launch functionality
- Create SignalAction models for 40+ yabai commands
- Build ActionBuilderView with nested parameter controls
- Add LiveShellPreview for real-time shell command generation
- Implement ActionValidator for conflict detection
- Add migration parser for existing raw action strings
- Include feature flag for safe rollout
- Maintain full backward compatibility
2025-12-31 01:44:13 -05:00

210 lines
8.3 KiB
Swift

//
// SettingsView.swift
// YabaiPro
//
// Created by Jake Shore
// Copyright © 2024 Jake Shore. All rights reserved.
//
import SwiftUI
struct SettingsView: View {
@ObservedObject var viewModel: SettingsViewModel
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("YabaiPro")
.font(.title2)
.fontWeight(.semibold)
.padding(.bottom, 8)
// Presets
VStack(alignment: .leading, spacing: 8) {
Text("Presets")
.font(.headline)
HStack {
Button("Default") {
viewModel.applyPreset(.default)
}
.buttonStyle(.bordered)
Button("Minimalist") {
viewModel.applyPreset(.minimalist)
}
.buttonStyle(.bordered)
}
}
Divider()
// Aesthetics
VStack(alignment: .leading, spacing: 12) {
Text("Aesthetics")
.font(.headline)
Toggle("Fade Inactive Windows", isOn: $viewModel.fadeInactiveWindows)
.help("Makes inactive windows semi-transparent (requires SIP disabled)")
if viewModel.fadeInactiveWindows {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Inactive Window Opacity: \(String(format: "%.1f", viewModel.inactiveWindowOpacity))")
.font(.subheadline)
if viewModel.hasUnappliedChanges {
Text("(unsaved)")
.font(.caption)
.foregroundColor(.orange)
}
}
Slider(value: $viewModel.inactiveWindowOpacity, in: 0.0...1.0, step: 0.1)
.help("Set opacity for inactive windows (0.0 = fully transparent, 1.0 = fully opaque). Click 'Apply Changes' to save.")
}
.padding(.leading, 16)
.padding(.top, 4)
}
Toggle("Disable Window Shadows", isOn: $viewModel.disableShadows)
.help("Creates a sharp, clean look (requires SIP disabled)")
VStack(alignment: .leading, spacing: 4) {
Text("Menu Bar Opacity: \(String(format: "%.1f", viewModel.menuBarOpacity))")
.font(.subheadline)
Slider(value: $viewModel.menuBarOpacity, in: 0.0...1.0, step: 0.1)
.help("0.0 = fully transparent, 1.0 = fully opaque (requires SIP disabled)")
}
VStack(alignment: .leading, spacing: 4) {
Text("Window Gap: \(Int(viewModel.windowGap))")
.font(.subheadline)
Slider(value: $viewModel.windowGap, in: 0...20, step: 1)
}
}
Divider()
// Focus & Behavior
VStack(alignment: .leading, spacing: 12) {
Text("Focus & Behavior")
.font(.headline)
Toggle("Focus Follows Mouse", isOn: $viewModel.focusFollowsMouse)
.help("Automatically focus windows under the mouse cursor")
}
Divider()
// Apply Button
VStack(alignment: .leading, spacing: 8) {
HStack {
Button("Apply Changes") {
viewModel.applyChanges()
}
.buttonStyle(.borderedProminent)
Spacer()
if viewModel.isApplying {
ProgressView()
.scaleEffect(0.7)
}
}
if let status = viewModel.lastStatus {
Text(status)
.font(.caption)
.foregroundColor(.secondary)
}
if !viewModel.hasAccessibilityPermission {
HStack {
Text("⚠️ Accessibility permission required")
.font(.caption)
.foregroundColor(.red)
Button("Grant Access") {
viewModel.openAccessibilitySettings()
}
.font(.caption)
}
.padding(.top, 4)
}
if viewModel.showSIPWarning {
Text("⚠️ Some features require SIP to be disabled in Recovery Mode")
.font(.caption)
.foregroundColor(.orange)
.padding(.top, 4)
}
if let saStatus = viewModel.scriptingAdditionStatus {
Text(saStatus)
.font(.caption)
.foregroundColor(saStatus.contains("") ? .red : saStatus.contains("⚠️") ? .orange : .green)
.padding(.top, 4)
}
// Remote pairing UI
VStack(alignment: .leading, spacing: 6) {
Text("Remote Control")
.font(.subheadline)
VStack(alignment: .leading, spacing: 8) {
HStack {
Button(viewModel.isRemoteServerRunning ? "Start Pairing" : "Start Server & Pair") {
viewModel.startPairing()
}
.buttonStyle(.bordered)
if let pin = viewModel.remotePairingPIN {
Text("PIN: \(pin)")
.font(.caption)
.padding(.leading, 6)
.foregroundColor(.cyan)
}
}
// Reachable addresses and QR
if !viewModel.reachableAddresses.isEmpty {
VStack(alignment: .leading, spacing: 6) {
Text("Reachable Addresses:")
.font(.caption)
ForEach(viewModel.reachableAddresses, id: \.self) { addr in
HStack {
Text(addr)
.font(.caption2)
.lineLimit(1)
Spacer()
Button("Copy") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(addr, forType: .string)
}
.buttonStyle(.bordered)
if let qr = viewModel.currentQRCode {
Button("Show QR") {
// show as separate window
let vc = NSHostingController(rootView: Image(nsImage: qr).resizable().scaledToFit().frame(width: 280, height: 280))
let win = NSWindow(contentViewController: vc)
win.styleMask = [.titled, .closable]
win.title = "Scan to Pair"
win.makeKeyAndOrderFront(nil)
}
.buttonStyle(.bordered)
}
}
}
}
}
}
}
}
}
.padding()
.frame(width: 320)
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(viewModel: SettingsViewModel())
}
}