Files
iKeyMon/Views/MainView.swift
2025-11-15 19:49:28 +01:00

230 lines
8.1 KiB
Swift

//
// MainView.swift
// iKeyMon
//
// Created by tracer on 30.03.25.
//
import SwiftUI
struct MainView: View {
@State var showAddServerSheet: Bool = false
@State private var serverBeingEdited: Server?
@State private var serverToDelete: Server?
@State private var showDeleteConfirmation = false
@State private var isFetchingInfo: Bool = false
@State private var refreshTimer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
@State private var progress: Double = 0
@State private var lastRefresh = Date()
@State private var pingTimer: Timer?
private let serverOrderKey = "serverOrder"
@State private var servers: [Server] = {
if let data = UserDefaults.standard.data(forKey: "storedServers"),
let saved = try? JSONDecoder().decode([Server].self, from: data) {
if let idStrings = UserDefaults.standard.stringArray(forKey: "serverOrder") {
let idMap = idStrings.compactMap(UUID.init)
return saved.sorted { a, b in
guard
let i1 = idMap.firstIndex(of: a.id),
let i2 = idMap.firstIndex(of: b.id)
else { return false }
return i1 < i2
}
}
return saved
}
return []
}()
// @State private var selectedServer: Server?
@State private var selectedServerID: UUID?
var body: some View {
NavigationSplitView {
List(selection: $selectedServerID) {
ForEach(servers) { server in
HStack {
Image(systemName: "dot.circle.fill")
.foregroundColor(server.pingable ? .green : .red)
Text(server.hostname)
}
.tag(server)
.contextMenu {
Button("Edit") {
print("Editing:", server.hostname)
serverBeingEdited = server
}
Divider()
Button("Delete", role: .destructive) {
serverToDelete = server
showDeleteConfirmation = true
}
}
}
.onMove(perform: moveServer)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showAddServerSheet = true }) {
Image(systemName: "plus")
}
.help("Add Host")
}
}
.navigationTitle("Servers")
.onChange(of: selectedServerID) {
if let selectedServerID {
UserDefaults.standard.set(selectedServerID.uuidString, forKey: "selectedServerID")
fetchServerInfo(for: selectedServerID)
}
}
} detail: {
if let selectedServerID,
let index = servers.firstIndex(where: { selectedServerID == $0.id }) {
ServerDetailView(server: $servers[index], isFetching: isFetchingInfo)
} else {
ContentUnavailableView("No Server Selected", systemImage: "server.rack")
}
}
.sheet(isPresented: $showAddServerSheet) {
ServerFormView(
mode: .add,
servers: $servers,
dismiss: { showAddServerSheet = false }
)
}
.sheet(item: $serverBeingEdited) { server in
ServerFormView(
mode: .edit(server),
servers: $servers,
dismiss: { serverBeingEdited = nil }
)
}
.alert("Are you sure you want to delete this server?", isPresented: $showDeleteConfirmation, presenting: serverToDelete) { server in
Button("Delete", role: .destructive) {
ServerFormView.delete(server: server, from: &servers)
}
Button("Cancel", role: .cancel) {}
}
.onReceive(refreshTimer) { _ in
for server in servers {
print("fetching server: \(server.hostname)")
fetchServerInfo(for: server.id)
}
}
.onAppear {
if let storedID = UserDefaults.standard.string(forKey: "selectedServerID"),
let uuid = UUID(uuidString: storedID),
servers.contains(where: { $0.id == uuid }) {
selectedServerID = uuid
} else if selectedServerID == nil, let first = servers.first {
selectedServerID = first.id
}
pingAllServers()
pingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
pingAllServers()
}
}
.frame(minWidth: 800, minHeight: 450)
}
private func fetchServerInfo(for id: UUID) {
guard let server = servers.first(where: { $0.id == id }),
let api = ServerAPI(server: server) else {
return
}
isFetchingInfo = true
Task {
defer { isFetchingInfo = false }
do {
let info = try await api.fetchServerInfo()
if let index = servers.firstIndex(where: { $0.id == id }) {
var updated = servers[index]
updated.info = try ServerInfo(from: info as! Decoder)
servers[index] = updated
}
} catch {
print("❌ Failed to fetch server data: \(error)")
}
}
}
private func moveServer(from source: IndexSet, to destination: Int) {
servers.move(fromOffsets: source, toOffset: destination)
saveServerOrder()
}
private func saveServerOrder() {
let ids = servers.map { $0.id.uuidString }
UserDefaults.standard.set(ids, forKey: serverOrderKey)
}
private struct PingResponse: Codable {
let response: String
}
// func pingServer(_ server: Server) async -> Bool {
// let hostname = server.hostname
// guard let url = URL(string: "https://\(hostname)/api/v2/ping") else {
// return false
// }
//
// var request = URLRequest(url: url)
// request.httpMethod = "GET"
// request.timeoutInterval = 5
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
//
// let apiKey = KeychainHelper.loadApiKey(for: hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
// request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
//
// do {
// let (data, response) = try await URLSession.shared.data(for: request)
// if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
// do {
// let decoded = try JSONDecoder().decode(PingResponse.self, from: data)
// if decoded.response == "pong" {
// return true
// } else {
// print(" Unexpected response: \(decoded.response)")
// return false
// }
// } catch {
// print(" Failed to decode JSON: \(error)")
// return false
// }
// } else {
// return false
// }
// } catch {
// print("[Ping] \(server.hostname): \(error.localizedDescription)")
// return false
// }
// }
func pingAllServers() {
for (index, server) in servers.enumerated() {
Task {
let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let api = ServerAPI(hostname: server.hostname, apiKey: apiKey)
let pingable = await api.ping()
servers[index].pingable = pingable
}
}
}
}
#Preview {
MainView()
}