Add OS metadata, preference hooks, and slider polish

This commit is contained in:
Micha
2025-11-19 18:15:33 +01:00
parent 4efe1a2324
commit d3f9126245
4 changed files with 104 additions and 60 deletions

View File

@@ -292,6 +292,13 @@ private extension APIv2_12 {
case securityUpdateCount = "security_update_count" case securityUpdateCount = "security_update_count"
case rebootRequired = "reboot_required" 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 let label: String
@@ -309,6 +316,16 @@ private extension APIv2_12 {
case endOfLife = "end_of_life" case endOfLife = "end_of_life"
case updates 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 { func toDomain() -> ServerInfo {

View File

@@ -28,9 +28,10 @@ struct PreferencesView: View {
@State private var pingIntervalSlider: Double = 10 @State private var pingIntervalSlider: Double = 10
@State private var refreshIntervalSlider: Double = 60 @State private var refreshIntervalSlider: Double = 60
@State private var selection: Tab = .monitor @State private var selection: Tab = .monitor
@State private var hoveredTab: Tab?
private let minimumInterval: Double = 10 private let minimumInterval: Double = 10
private let maximumPingInterval: Double = 600 private let maximumPingInterval: Double = 60
private let maximumRefreshInterval: Double = 600 private let maximumRefreshInterval: Double = 600
var body: some View { var body: some View {
@@ -74,13 +75,20 @@ struct PreferencesView: View {
Spacer() Spacer()
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.padding(.horizontal, 6) .padding(.horizontal, 10)
.background( .frame(maxWidth: .infinity, alignment: .leading)
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(selection == tab ? Color.accentColor.opacity(0.25) : Color.clear)
)
} }
.buttonStyle(.plain) .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() Spacer()
@@ -127,6 +135,15 @@ struct PreferencesView: View {
storedRefreshInterval = Int(refreshIntervalSlider) 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 { private struct MonitorPreferencesView: View {
@@ -141,56 +158,20 @@ private struct MonitorPreferencesView: View {
let refreshChanged: (Bool) -> Void let refreshChanged: (Bool) -> Void
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 24) {
Group { intervalSection(
Text("Ping interval") title: "Ping interval",
.font(.headline)
Slider(
value: $pingIntervalSlider, value: $pingIntervalSlider,
in: minimumInterval...maximumPingInterval, range: minimumInterval...maximumPingInterval,
step: 5 onEditingChanged: pingChanged
) { )
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)
}
Group { intervalSection(
Text("Refresh interval") title: "Refresh interval",
.font(.headline)
Slider(
value: $refreshIntervalSlider, value: $refreshIntervalSlider,
in: minimumInterval...maximumRefreshInterval, range: minimumInterval...maximumRefreshInterval,
step: 5 onEditingChanged: refreshChanged
) { )
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)
}
Divider() Divider()
@@ -201,6 +182,42 @@ private struct MonitorPreferencesView: View {
} }
.frame(maxWidth: .infinity, alignment: .leading) .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 NotificationsPreferencesView: View { private struct NotificationsPreferencesView: View {

View File

@@ -10,16 +10,19 @@ import SwiftUI
struct ServerDetailView: View { struct ServerDetailView: View {
@Binding var server: Server @Binding var server: Server
var isFetching: Bool var isFetching: Bool
@AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true
@State private var progress: Double = 0 @State private var progress: Double = 0
let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect() let timer = Timer.publish(every: 1.0 / 60.0, on: .main, in: .common).autoconnect()
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if showIntervalIndicator {
ProgressView(value: progress) ProgressView(value: progress)
.progressViewStyle(LinearProgressViewStyle()) .progressViewStyle(LinearProgressViewStyle())
.padding(.horizontal) .padding(.horizontal)
.frame(height: 2) .frame(height: 2)
}
if server.info == nil { if server.info == nil {
ProgressView("Fetching server info...") ProgressView("Fetching server info...")
@@ -54,6 +57,7 @@ struct ServerDetailView: View {
} }
} }
.onReceive(timer) { _ in .onReceive(timer) { _ in
guard showIntervalIndicator else { return }
withAnimation(.linear(duration: 1.0 / 60.0)) { withAnimation(.linear(duration: 1.0 / 60.0)) {
progress += 1.0 / (60.0 * 60.0) progress += 1.0 / (60.0 * 60.0)
if progress >= 1 { progress = 0 } if progress >= 1 { progress = 0 }

View File

@@ -100,7 +100,13 @@ struct GeneralView: View {
Text("Additional PHP interpreters") Text("Additional PHP interpreters")
} value: { } value: {
InfoCell( 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 monospaced: true
) )
} }