// // ServerDetailView.swift // iKeyMon // // Created by tracer on 30.03.25. // import SwiftUI struct ServerActionFeedback: Identifiable { let id = UUID() let title: String let message: String } struct ServerDetailView: View { @Binding var server: Server var isFetching: Bool var canRestart: Bool = false var isRestarting: Bool = false var onRestart: (() async -> ServerActionFeedback)? = nil @AppStorage("showIntervalIndicator") private var showIntervalIndicator: Bool = true @AppStorage("refreshInterval") private var refreshInterval: Int = 60 private var showPlaceholder: Bool { server.info == nil } @State private var progress: Double = 0 @State private var showRestartSheet = false @State private var restartFeedback: ServerActionFeedback? 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) } ZStack(alignment: .topTrailing) { VStack(spacing: 0) { Spacer().frame(height: 6) TabView { GeneralView( server: resolvedBinding, canRestart: canRestart, isRestarting: isRestarting ) { showRestartSheet = true } .tabItem { Text("General").unredacted() } ResourcesView(server: resolvedBinding) .tabItem { Text("Resources").unredacted() } ServicesView(server: resolvedBinding) .tabItem { Text("Services").unredacted() } } .redacted(reason: showPlaceholder ? .placeholder : []) .shimmering(active: showPlaceholder) } if showPlaceholder || isFetching { LoadingBadge() .padding() } } .padding(0) } .overlay(alignment: .bottomTrailing) { if let feedback = restartFeedback { RestartFeedbackBanner( feedback: feedback, onDismiss: { restartFeedback = nil } ) .padding() } } .onReceive(timer) { _ in guard showIntervalIndicator else { return } withAnimation(.linear(duration: 1.0 / 60.0)) { progress += 1.0 / (Double(refreshInterval) * 60.0) if progress >= 1 { progress = 0 } } } .sheet(isPresented: $showRestartSheet) { RestartConfirmationSheet( hostname: server.hostname, isRestarting: isRestarting, onCancel: { showRestartSheet = false }, onConfirm: { guard let onRestart else { return } showRestartSheet = false Task { let feedback = await onRestart() await MainActor.run { restartFeedback = feedback } } } ) } } private var resolvedBinding: Binding { if showPlaceholder { return .constant(placeholderServer()) } return $server } private func placeholderServer() -> Server { Server(id: server.id, hostname: server.hostname, info: .placeholder, pingable: server.pingable) } } #Preview { ServerDetailView( server: .constant(Server(id: UUID(), hostname: "preview.example.com", info: ServerInfo.placeholder)), isFetching: false, canRestart: true ) } private struct LoadingBadge: View { var body: some View { HStack(spacing: 6) { ProgressView() .scaleEffect(0.5) Text("Fetching latest data…") .font(.caption) } .padding(8) .background(.ultraThinMaterial, in: Capsule()) } } private struct RestartConfirmationSheet: View { let hostname: String let isRestarting: Bool let onCancel: () -> Void let onConfirm: () -> Void var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Reboot this server?") .font(.title3.weight(.semibold)) Text("This will send a reboot command to \(hostname).") .foregroundColor(.secondary) HStack { Spacer() Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) .disabled(isRestarting) Button("Reboot", role: .destructive) { onConfirm() } .keyboardShortcut(.defaultAction) .disabled(isRestarting) } } .padding(24) .frame(width: 420) } } private struct RestartFeedbackBanner: View { let feedback: ServerActionFeedback let onDismiss: () -> Void var body: some View { VStack(alignment: .leading, spacing: 12) { Text(feedback.title) .font(.headline) Text(feedback.message) .font(.subheadline) .foregroundColor(.secondary) HStack { Spacer() Button("OK") { onDismiss() } .keyboardShortcut(.defaultAction) } } .frame(maxWidth: 360, alignment: .leading) .padding(24) .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) .shadow(radius: 12) } }