refactored code structure
This commit is contained in:
221
Sources/Views/MainView.swift
Normal file
221
Sources/Views/MainView.swift
Normal file
@@ -0,0 +1,221 @@
|
||||
//
|
||||
// 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
|
||||
for server in servers {
|
||||
fetchServerInfo(for: server.id)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
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
|
||||
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 apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!apiKey.isEmpty else {
|
||||
print("❌ [MainView] fetchServerInfo: missing API key for \(server.hostname)")
|
||||
return
|
||||
}
|
||||
guard let baseURL = URL(string: "https://\(server.hostname)") else {
|
||||
print("❌ [MainView] Invalid base URL for \(server.hostname)")
|
||||
return
|
||||
}
|
||||
|
||||
isFetchingInfo = true
|
||||
|
||||
Task {
|
||||
defer { isFetchingInfo = false }
|
||||
do {
|
||||
let api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey)
|
||||
let info = try await api.fetchServerSummary(apiKey: apiKey)
|
||||
await MainActor.run {
|
||||
if let index = servers.firstIndex(where: { $0.id == id }) {
|
||||
var updated = servers[index]
|
||||
updated.info = info
|
||||
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)
|
||||
print("💾 [MainView] Saved server order with \(ids.count) entries")
|
||||
}
|
||||
|
||||
private struct PingResponse: Codable {
|
||||
let response: String
|
||||
}
|
||||
|
||||
func pingAllServers() {
|
||||
for (index, server) in servers.enumerated() {
|
||||
Task {
|
||||
let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let pingable = await PingService.ping(hostname: server.hostname, apiKey: apiKey)
|
||||
await MainActor.run {
|
||||
servers[index].pingable = pingable
|
||||
}
|
||||
if !pingable {
|
||||
print("📶 [MainView] Ping \(server.hostname): 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()
|
||||
}
|
||||
Reference in New Issue
Block a user