Remove the explanation text about configuring appcast URL and EdDSA key. This is configuration for developers, not end users.
299 lines
9.4 KiB
Swift
299 lines
9.4 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")
|
|
}
|
|
|
|
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()
|
|
}
|