Files
Gaze/Gaze/Views/Containers/SettingsWindowView.swift
2026-01-15 09:05:50 -05:00

218 lines
6.4 KiB
Swift

//
// SettingsWindowView.swift
// Gaze
//
// Created by Mike Freno on 1/8/26.
//
import SwiftUI
@MainActor
final class SettingsWindowPresenter {
static let shared = SettingsWindowPresenter()
private weak var windowController: NSWindowController?
private var closeObserver: NSObjectProtocol?
func show(settingsManager: SettingsManager, initialTab: Int = 0) {
if focusExistingWindow(tab: initialTab) {
return
}
createWindow(settingsManager: settingsManager, initialTab: initialTab)
}
func focus(tab: Int) {
_ = focusExistingWindow(tab: tab)
}
func close() {
windowController?.close()
windowController = nil
removeCloseObserver()
}
@discardableResult
private func focusExistingWindow(tab: Int?) -> Bool {
guard let window = windowController?.window else {
windowController = nil
return false
}
if let tab {
NotificationCenter.default.post(
name: Notification.Name("SwitchToSettingsTab"),
object: tab
)
}
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return true
}
private func createWindow(settingsManager: SettingsManager, initialTab: Int) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 700),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.identifier = WindowIdentifiers.settings
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.toolbarStyle = .unified
window.toolbar = NSToolbar()
window.center()
window.setFrameAutosaveName("SettingsWindow")
window.isReleasedWhenClosed = false
let contentView = SettingsWindowView(
settingsManager: settingsManager,
initialTab: initialTab
)
window.contentView = NSHostingView(rootView: contentView)
let controller = NSWindowController(window: window)
controller.showWindow(nil)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
windowController = controller
removeCloseObserver()
closeObserver = NotificationCenter.default.addObserver(
forName: NSWindow.willCloseNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor [weak self] in
self?.windowController = nil
self?.removeCloseObserver()
}
}
}
@MainActor
private func removeCloseObserver() {
if let closeObserver {
NotificationCenter.default.removeObserver(closeObserver)
self.closeObserver = nil
}
}
deinit {
Task { @MainActor in
removeCloseObserver()
}
}
}
struct SettingsWindowView: View {
@ObservedObject var settingsManager: SettingsManager
@State private var selectedSection: SettingsSection
init(settingsManager: SettingsManager, initialTab: Int = 0) {
self.settingsManager = settingsManager
_selectedSection = State(initialValue: SettingsSection(rawValue: initialTab) ?? .general)
}
var body: some View {
ZStack {
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.ignoresSafeArea()
VStack(spacing: 0) {
NavigationSplitView {
List(SettingsSection.allCases, selection: $selectedSection) { section in
NavigationLink(value: section) {
Label(section.title, systemImage: section.iconName)
}
}
.listStyle(.sidebar)
} detail: {
detailView(for: selectedSection)
}
#if DEBUG
Divider()
HStack {
Button("Retrigger Onboarding") {
retriggerOnboarding()
}
.buttonStyle(.bordered)
Spacer()
}
.padding()
#endif
}
}
#if APPSTORE
.frame(
minWidth: 1000,
minHeight: 700
)
#else
.frame(
minWidth: 1000,
minHeight: 900
)
#endif
.onReceive(
NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab"))
) { notification in
if let tab = notification.object as? Int,
let section = SettingsSection(rawValue: tab)
{
selectedSection = section
}
}
}
@ViewBuilder
private func detailView(for section: SettingsSection) -> some View {
switch section {
case .general:
GeneralSetupView(
settingsManager: settingsManager,
isOnboarding: false
)
case .lookAway:
LookAwaySetupView(settingsManager: settingsManager)
case .blink:
BlinkSetupView(settingsManager: settingsManager)
case .posture:
PostureSetupView(settingsManager: settingsManager)
case .enforceMode:
EnforceModeSetupView(settingsManager: settingsManager)
case .userTimers:
UserTimersView(
userTimers: Binding(
get: { settingsManager.settings.userTimers },
set: { settingsManager.settings.userTimers = $0 }
)
)
case .smartMode:
SmartModeSetupView(settingsManager: settingsManager)
}
}
#if DEBUG
private func retriggerOnboarding() {
OnboardingWindowPresenter.shared.close()
SettingsWindowPresenter.shared.close()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
OnboardingWindowPresenter.shared.show(settingsManager: self.settingsManager)
}
}
#endif
}
#Preview {
SettingsWindowView(settingsManager: SettingsManager.shared)
}