Files
iKeyMon/Sources/Views/PreferencesView.swift
2025-11-22 18:56:55 +01:00

316 lines
10 KiB
Swift

import SwiftUI
struct PreferencesView: View {
private enum Tab: CaseIterable {
case monitor, notifications, alerts, updates
var title: String {
switch self {
case .monitor: return "Monitor"
case .notifications: return "Notifications"
case .alerts: return "Alerts"
case .updates: return "Updates"
}
}
var icon: String {
switch self {
case .monitor: return "waveform.path.ecg"
case .notifications: return "bell.badge"
case .alerts: return "exclamationmark.triangle"
case .updates: return "square.and.arrow.down"
}
}
}
@EnvironmentObject private var updateViewModel: UpdateViewModel
@AppStorage("pingInterval") private var storedPingInterval: Int = 10
@AppStorage("refreshInterval") private var storedRefreshInterval: Int = 60
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
@AppStorage("autoCheckUpdates") private var autoCheckUpdates: Bool = true
@AppStorage("includePrereleaseUpdates") private var includePrereleaseUpdates: Bool = true
@State private var pingIntervalSlider: Double = 10
@State private var refreshIntervalSlider: Double = 60
@State private var selection: Tab = .monitor
@State private var hoveredTab: Tab?
private let minimumInterval: Double = 10
private let maximumPingInterval: Double = 60
private let maximumRefreshInterval: Double = 600
var body: some View {
HStack(spacing: 0) {
sidebar
Divider()
ScrollView {
detailContent(for: selection)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.frame(minWidth: 360, maxWidth: .infinity, maxHeight: .infinity)
}
.frame(minWidth: 560, minHeight: 360)
.onAppear {
pingIntervalSlider = Double(storedPingInterval)
refreshIntervalSlider = Double(storedRefreshInterval)
}
.onChange(of: pingIntervalSlider) { _, newValue in
storedPingInterval = Int(newValue)
}
.onChange(of: refreshIntervalSlider) { _, newValue in
storedRefreshInterval = Int(newValue)
}
}
private var sidebar: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Preferences")
.font(.headline)
.padding(.bottom, 8)
ForEach(Tab.allCases, id: \.self) { tab in
Button {
selection = tab
} label: {
HStack(spacing: 10) {
Image(systemName: tab.icon)
.frame(width: 20)
Text(tab.title)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
Capsule(style: .continuous)
.fill(backgroundColor(for: tab))
)
}
.buttonStyle(.plain)
.focusable(false)
.contentShape(Capsule())
.foregroundColor(selection == tab ? .white : .primary)
.onHover { isHovering in
hoveredTab = isHovering ? tab : (hoveredTab == tab ? nil : hoveredTab)
}
}
Spacer()
}
.padding()
.frame(width: 180, alignment: .top)
}
@ViewBuilder
private func detailContent(for tab: Tab) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(tab.title)
.font(.title2)
.padding(.bottom, 12)
switch tab {
case .monitor:
MonitorPreferencesView(
pingIntervalSlider: $pingIntervalSlider,
refreshIntervalSlider: $refreshIntervalSlider,
showIntervalIndicator: $showIntervalIndicator,
minimumInterval: minimumInterval,
maximumPingInterval: maximumPingInterval,
maximumRefreshInterval: maximumRefreshInterval,
pingChanged: handlePingSliderEditing(_:),
refreshChanged: handleRefreshSliderEditing(_:)
)
case .notifications:
NotificationsPreferencesView()
case .alerts:
AlertsPreferencesView()
case .updates:
UpdatesPreferencesView(
autoCheckUpdates: $autoCheckUpdates,
includePrereleaseUpdates: $includePrereleaseUpdates
)
.environmentObject(updateViewModel)
}
}
}
private func handlePingSliderEditing(_ editing: Bool) {
if !editing {
storedPingInterval = Int(pingIntervalSlider)
}
}
private func handleRefreshSliderEditing(_ editing: Bool) {
if !editing {
storedRefreshInterval = Int(refreshIntervalSlider)
}
}
private func backgroundColor(for tab: Tab) -> Color {
if selection == tab {
return Color.accentColor
}
if hoveredTab == tab {
return Color.accentColor.opacity(0.2)
}
return Color.accentColor.opacity(0.08)
}
}
private struct MonitorPreferencesView: View {
@Binding var pingIntervalSlider: Double
@Binding var refreshIntervalSlider: Double
@Binding var showIntervalIndicator: Bool
let minimumInterval: Double
let maximumPingInterval: Double
let maximumRefreshInterval: Double
let pingChanged: (Bool) -> Void
let refreshChanged: (Bool) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 24) {
intervalSection(
title: "Ping interval",
value: $pingIntervalSlider,
range: minimumInterval...maximumPingInterval,
onEditingChanged: pingChanged
)
intervalSection(
title: "Refresh interval",
value: $refreshIntervalSlider,
range: minimumInterval...maximumRefreshInterval,
onEditingChanged: refreshChanged
)
Divider()
Toggle("Show interval indicator", isOn: $showIntervalIndicator)
.toggleStyle(.switch)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private func intervalSection(
title: String,
value: Binding<Double>,
range: ClosedRange<Double>,
onEditingChanged: @escaping (Bool) -> Void
) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(title)
.font(.headline)
Spacer()
Text("\(Int(value.wrappedValue)) seconds")
.font(.subheadline)
.foregroundColor(.secondary)
}
Slider(
value: value,
in: range,
step: 5
) {
Text(title)
} minimumValueLabel: {
Text("\(Int(range.lowerBound))s")
.font(.caption)
.foregroundColor(.secondary)
} maximumValueLabel: {
Text("\(Int(range.upperBound))s")
.font(.caption)
.foregroundColor(.secondary)
} onEditingChanged: { editing in
onEditingChanged(editing)
}
}
}
}
private struct UpdatesPreferencesView: View {
@Binding var autoCheckUpdates: Bool
@Binding var includePrereleaseUpdates: Bool
@EnvironmentObject var updateViewModel: UpdateViewModel
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Toggle("Automatically check for updates", isOn: $autoCheckUpdates)
.toggleStyle(.switch)
Toggle("Include pre-release builds", isOn: $includePrereleaseUpdates)
.toggleStyle(.switch)
HStack {
if updateViewModel.isChecking {
ProgressView()
.progressViewStyle(.circular)
Text("Checking for updates…")
.foregroundColor(.secondary)
} else {
Button("Check Now") {
updateViewModel.checkForUpdates(userInitiated: true)
}
}
}
if let release = updateViewModel.latestFetchedRelease {
VStack(alignment: .leading, spacing: 4) {
Text("Latest available: \(release.versionString)")
.font(.subheadline)
if release.prerelease {
Text("Pre-release build")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.top, 4)
}
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct NotificationsPreferencesView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Notifications")
.font(.headline)
.padding(.bottom)
Text("Configure notification behavior here.")
.foregroundColor(.secondary)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
private struct AlertsPreferencesView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Alerts")
.font(.headline)
.padding(.bottom)
Text("Configure alert thresholds and behavior.")
.foregroundColor(.secondary)
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
#Preview {
PreferencesView()
}