Files
iKeyMon/Views/MainView.swift

257 lines
9.6 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// MainView.swift
// iKeyMon
//
// Created by tracer on 30.03.25.
//
import SwiftUI
struct MainView: View {
private static let serverOrderKeyStatic = "serverOrder"
private static let storedServersKeyStatic = "storedServers"
@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 = MainView.serverOrderKeyStatic
private let storedServersKey = MainView.storedServersKeyStatic
@State private var servers: [Server] = MainView.loadStoredServers()
// @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
print("⏰ [MainView] Refresh timer fired with \(servers.count) servers")
for server in servers {
print("⏰ [MainView] Triggering fetchServerInfo for: \(server.hostname)")
fetchServerInfo(for: server.id)
}
}
.onAppear {
print("👀 [MainView] onAppear - servers in memory: \(servers.count)")
if let storedID = UserDefaults.standard.string(forKey: "selectedServerID"),
let uuid = UUID(uuidString: storedID),
servers.contains(where: { $0.id == uuid }) {
print("✅ [MainView] Restored selected server \(uuid)")
selectedServerID = uuid
} else if selectedServerID == nil, let first = servers.first {
print("✅ [MainView] Selecting first server \(first.hostname)")
selectedServerID = first.id
} else {
print(" [MainView] No stored selection")
}
pingAllServers()
pingTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in
print("📡 [MainView] Ping timer firing")
pingAllServers()
}
}
.frame(minWidth: 800, minHeight: 450)
}
private func fetchServerInfo(for id: UUID) {
guard let server = servers.first(where: { $0.id == id }) else {
print("❌ [MainView] fetchServerInfo: server not found for id \(id)")
return
}
guard let api = ServerAPI(server: server) else {
print("❌ [MainView] fetchServerInfo: could not create API for \(server.hostname)")
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 = info
servers[index] = updated
print("✅ [MainView] Updated server info for \(updated.hostname)")
}
} 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)
print("💾 [MainView] Saved server order with \(ids.count) entries")
}
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
print("📶 [MainView] Ping \(server.hostname): \(pingable ? "online" : "offline")")
}
}
}
}
private static func loadStoredServers() -> [Server] {
let defaults = UserDefaults.standard
guard let data = defaults.data(forKey: storedServersKeyStatic) else {
print(" [MainView] No storedServers data found")
return []
}
do {
let saved = try JSONDecoder().decode([Server].self, from: data)
print("📦 [MainView] Loaded \(saved.count) servers from UserDefaults")
if let order = defaults.stringArray(forKey: serverOrderKeyStatic) {
let idMap = order.compactMap(UUID.init)
let sorted = 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 sorted
}
return saved
} catch {
print("❌ [MainView] Failed to decode stored servers: \(error)")
return []
}
}
#Preview {
MainView()
}