304 lines
9.6 KiB
Swift
304 lines
9.6 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 sparkleUpdater: SparkleUpdater
|
|
|
|
@AppStorage("pingInterval") private var storedPingInterval: Int = 10
|
|
@AppStorage("refreshInterval") private var storedRefreshInterval: Int = 60
|
|
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: 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()
|
|
.environmentObject(sparkleUpdater)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
@EnvironmentObject var sparkleUpdater: SparkleUpdater
|
|
|
|
private var automaticallyChecksBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { sparkleUpdater.automaticallyChecksForUpdates },
|
|
set: { sparkleUpdater.automaticallyChecksForUpdates = $0 }
|
|
)
|
|
}
|
|
|
|
private var automaticallyDownloadsBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { sparkleUpdater.automaticallyDownloadsUpdates },
|
|
set: { sparkleUpdater.automaticallyDownloadsUpdates = $0 }
|
|
)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 18) {
|
|
Toggle("Automatically check for updates", isOn: automaticallyChecksBinding)
|
|
Toggle("Automatically download updates", isOn: automaticallyDownloadsBinding)
|
|
|
|
Button(action: sparkleUpdater.checkForUpdates) {
|
|
Label("Check for Updates Now", systemImage: "sparkles")
|
|
}
|
|
|
|
Text("Updates are delivered via Sparkle. Configure your appcast URL and public EdDSA key in Info.plist (keys `SUFeedURL` and `SUPublicEDKey`).")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.top, 4)
|
|
|
|
Spacer()
|
|
}
|
|
.toggleStyle(.switch)
|
|
.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()
|
|
}
|