Add OS metadata, preference hooks, and slider polish
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user