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 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 {

View File

@@ -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(
VStack(alignment: .leading, spacing: 24) {
intervalSection(
title: "Ping interval",
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)
}
range: minimumInterval...maximumPingInterval,
onEditingChanged: pingChanged
)
Group {
Text("Refresh interval")
.font(.headline)
Slider(
intervalSection(
title: "Refresh interval",
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)
}
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<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 {

View File

@@ -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) {
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 }

View File

@@ -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
)
}