import Foundation @MainActor class WebDAVClient: ObservableObject { private let settings: WebDAVSettings @Published var notes: [DenoteNote] = [] @Published var isLoading = false @Published var isPrefetching = false @Published var prefetchProgress: Double = 0 @Published var errorMessage: String? private var prefetchTask: Task? init(settings: WebDAVSettings) { self.settings = settings } private var config: WebDAVConfig { settings.config } func loadNotes() async { prefetchTask?.cancel() isLoading = true errorMessage = nil do { notes = try await fetchNotesList() prefetchTask = Task { await prefetchAllContent() } } catch { errorMessage = error.localizedDescription } isLoading = false } func loadContent(for index: Int) async { guard notes.indices.contains(index), notes[index].content == nil else { return } do { let content = try await fetchContent(for: notes[index]) guard notes.indices.contains(index) else { return } notes[index].content = content } catch {} } func saveContent(_ content: String, for note: DenoteNote) async throws { let c = config guard let url = URL(string: c.url + note.path) else { throw URLError(.badURL) } var request = URLRequest(url: url) request.httpMethod = "PUT" request.setValue(c.authHeader, forHTTPHeaderField: "Authorization") request.setValue("text/plain; charset=utf-8", forHTTPHeaderField: "Content-Type") request.httpBody = Data(content.utf8) let (_, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { throw URLError(.badServerResponse) } if let idx = notes.firstIndex(where: { $0.id == note.id }) { notes[idx].content = content } } func fetchContent(for note: DenoteNote) async throws -> String { let c = config guard let url = URL(string: c.url + note.path) else { throw URLError(.badURL) } var request = URLRequest(url: url) request.setValue(c.authHeader, forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) return String(data: data, encoding: .utf8) ?? String(data: data, encoding: .isoLatin1) ?? "" } private func fetchNotesList() async throws -> [DenoteNote] { let c = config guard let url = URL(string: c.url + c.notesPath) else { throw URLError(.badURL) } var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("1", forHTTPHeaderField: "Depth") request.setValue(c.authHeader, forHTTPHeaderField: "Authorization") request.setValue("application/xml", forHTTPHeaderField: "Content-Type") let (data, _) = try await URLSession.shared.data(for: request) return PropfindParser(data: data).parse() .sorted { ($0.modifiedDate ?? .distantPast) > ($1.modifiedDate ?? .distantPast) } } private func prefetchAllContent() async { isPrefetching = true let count = notes.count for i in 0.. Int { guard let url = URL(string: config.url + config.notesPath) else { throw URLError(.badURL) } var request = URLRequest(url: url) request.httpMethod = "PROPFIND" request.setValue("1", forHTTPHeaderField: "Depth") request.setValue(config.authHeader, forHTTPHeaderField: "Authorization") request.setValue("application/xml", forHTTPHeaderField: "Content-Type") let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } if http.statusCode == 401 { throw URLError(.userAuthenticationRequired) } guard (200...299).contains(http.statusCode) else { throw URLError(.badServerResponse) } return PropfindParser(data: data).parse().count } }