diff --git a/Sources/Model/API/Versions/APIv2_12.swift b/Sources/Model/API/Versions/APIv2_12.swift index d8d3848..60a9780 100644 --- a/Sources/Model/API/Versions/APIv2_12.swift +++ b/Sources/Model/API/Versions/APIv2_12.swift @@ -292,6 +292,13 @@ private extension APIv2_12 { case securityUpdateCount = "security_update_count" case rebootRequired = "reboot_required" } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + updateCount = try container.decodeIfPresent(Int.self, forKey: .updateCount) ?? 0 + securityUpdateCount = try container.decodeIfPresent(Int.self, forKey: .securityUpdateCount) ?? 0 + rebootRequired = try container.decodeIfPresent(Bool.self, forKey: .rebootRequired) ?? false + } } let label: String @@ -309,6 +316,16 @@ private extension APIv2_12 { case endOfLife = "end_of_life" case updates } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + label = try container.decodeIfPresent(String.self, forKey: .label) ?? "" + distribution = try container.decodeIfPresent(String.self, forKey: .distribution) ?? "" + version = try container.decodeIfPresent(String.self, forKey: .version) ?? "" + architecture = try container.decodeIfPresent(String.self, forKey: .architecture) ?? "" + endOfLife = try container.decodeIfPresent(Bool.self, forKey: .endOfLife) ?? false + updates = try container.decodeIfPresent(Updates.self, forKey: .updates) + } } func toDomain() -> ServerInfo { diff --git a/Sources/Views/PreferencesView.swift b/Sources/Views/PreferencesView.swift index cce0b9f..bf196c3 100644 --- a/Sources/Views/PreferencesView.swift +++ b/Sources/Views/PreferencesView.swift @@ -28,9 +28,10 @@ struct PreferencesView: View { @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 = 600 + private let maximumPingInterval: Double = 60 private let maximumRefreshInterval: Double = 600 var body: some View { @@ -74,13 +75,20 @@ struct PreferencesView: View { Spacer() } .padding(.vertical, 8) - .padding(.horizontal, 6) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(selection == tab ? Color.accentColor.opacity(0.25) : Color.clear) - ) + .padding(.horizontal, 10) + .frame(maxWidth: .infinity, alignment: .leading) } .buttonStyle(.plain) + .focusable(false) + .contentShape(Capsule()) + .background( + Capsule(style: .continuous) + .fill(backgroundColor(for: tab)) + ) + .foregroundColor(selection == tab ? .white : .primary) + .onHover { isHovering in + hoveredTab = isHovering ? tab : (hoveredTab == tab ? nil : hoveredTab) + } } Spacer() @@ -127,6 +135,15 @@ struct PreferencesView: View { 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 { @@ -141,56 +158,20 @@ private struct MonitorPreferencesView: View { let refreshChanged: (Bool) -> Void var body: some View { - VStack(alignment: .leading, spacing: 18) { - Group { - Text("Ping interval") - .font(.headline) - Slider( - value: $pingIntervalSlider, - in: minimumInterval...maximumPingInterval, - step: 5 - ) { - Text("Ping interval") - } minimumValueLabel: { - Text("\(Int(minimumInterval))s") - .font(.caption) - .foregroundColor(.secondary) - } maximumValueLabel: { - Text("\(Int(maximumPingInterval))s") - .font(.caption) - .foregroundColor(.secondary) - } onEditingChanged: { editing in - pingChanged(editing) - } - Text("Current: \(Int(pingIntervalSlider)) seconds") - .font(.caption) - .foregroundColor(.secondary) - } + VStack(alignment: .leading, spacing: 24) { + intervalSection( + title: "Ping interval", + value: $pingIntervalSlider, + range: minimumInterval...maximumPingInterval, + onEditingChanged: pingChanged + ) - Group { - Text("Refresh interval") - .font(.headline) - Slider( - value: $refreshIntervalSlider, - in: minimumInterval...maximumRefreshInterval, - step: 5 - ) { - Text("Refresh interval") - } minimumValueLabel: { - Text("\(Int(minimumInterval))s") - .font(.caption) - .foregroundColor(.secondary) - } maximumValueLabel: { - Text("\(Int(maximumRefreshInterval))s") - .font(.caption) - .foregroundColor(.secondary) - } onEditingChanged: { editing in - refreshChanged(editing) - } - Text("Current: \(Int(refreshIntervalSlider)) seconds") - .font(.caption) - .foregroundColor(.secondary) - } + intervalSection( + title: "Refresh interval", + value: $refreshIntervalSlider, + range: minimumInterval...maximumRefreshInterval, + onEditingChanged: refreshChanged + ) Divider() @@ -201,6 +182,42 @@ private struct MonitorPreferencesView: View { } .frame(maxWidth: .infinity, alignment: .leading) } + + private func intervalSection( + title: String, + value: Binding, + range: ClosedRange, + 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 NotificationsPreferencesView: View { diff --git a/Sources/Views/ServerDetailView.swift b/Sources/Views/ServerDetailView.swift index 2e5e071..718aaa0 100644 --- a/Sources/Views/ServerDetailView.swift +++ b/Sources/Views/ServerDetailView.swift @@ -10,16 +10,19 @@ import SwiftUI struct ServerDetailView: View { @Binding var server: Server var isFetching: Bool + @AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true @State private var progress: Double = 0 let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() var body: some View { VStack(spacing: 0) { - ProgressView(value: progress) - .progressViewStyle(LinearProgressViewStyle()) - .padding(.horizontal) - .frame(height: 2) + if showIntervalIndicator { + ProgressView(value: progress) + .progressViewStyle(LinearProgressViewStyle()) + .padding(.horizontal) + .frame(height: 2) + } if server.info == nil { ProgressView("Fetching server info...") @@ -54,6 +57,7 @@ struct ServerDetailView: View { } } .onReceive(timer) { _ in + guard showIntervalIndicator else { return } withAnimation(.linear(duration: 1.0 / 60.0)) { progress += 1.0 / (60.0 * 60.0) if progress >= 1 { progress = 0 } diff --git a/Sources/Views/Tabs/GeneralView.swift b/Sources/Views/Tabs/GeneralView.swift index 8e9fe23..b9cc62f 100644 --- a/Sources/Views/Tabs/GeneralView.swift +++ b/Sources/Views/Tabs/GeneralView.swift @@ -100,7 +100,13 @@ struct GeneralView: View { Text("Additional PHP interpreters") } value: { InfoCell( - value: server.info?.additionalPHPInterpreters?.map { $0.versionFull } ?? [], + value: { + let interpreters = server.info?.additionalPHPInterpreters ?? [] + if interpreters.isEmpty { + return ["None"] + } + return interpreters.map { $0.versionFull } + }(), monospaced: true ) }