Remove the Show/Hide Logs button and logs view from the Updates preferences tab. Keep the logging infrastructure in SparkleUpdater for diagnostics, but don't display it in the UI.
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()
|
|
}
|