import Foundation import Observation import OrgSocialKit @Observable @MainActor final class EditProfileViewModel { var nick = "" var title = "" var description = "" var avatar = "" var location = "" var birthday = "" var language = "" var linksText = "" var contactsText = "" var isSaving = false var errorMessage: String? var saved = false private let pw = ProfileWriter() var isLoading = false init() { loadFromUserDefaults() } func load() async { guard let url = ownFeedURL else { return } isLoading = true defer { isLoading = false } // bypassCache so the edit form reflects the real feed, not a stale CDN copy. guard let content = try? await FeedFetcher().fetch(from: url, bypassCache: true) else { return } loadFromContent(content) } func save() async { guard let feedURL = ownFeedURL, let uploader = makeUploader() else { errorMessage = "Configure your feed in Settings before editing your profile." return } isSaving = true errorMessage = nil defer { isSaving = false } do { // bypassCache so we patch the latest authoritative content, not a stale cache. var content = try await FeedFetcher().fetch(from: feedURL, bypassCache: true) content = pw.setKeyword("NICK", value: nick, in: content) content = pw.setKeyword("TITLE", value: title, in: content) if description.isEmpty { content = pw.removeKeyword("DESCRIPTION", from: content) } else { content = pw.setKeyword("DESCRIPTION", value: description, in: content) } if avatar.isEmpty { content = pw.removeKeyword("AVATAR", from: content) } else { content = pw.setKeyword("AVATAR", value: avatar, in: content) } if location.isEmpty { content = pw.removeKeyword("LOCATION", from: content) } else { content = pw.setKeyword("LOCATION", value: location, in: content) } if birthday.isEmpty { content = pw.removeKeyword("BIRTHDAY", from: content) } else { content = pw.setKeyword("BIRTHDAY", value: birthday, in: content) } if language.isEmpty { content = pw.removeKeyword("LANGUAGE", from: content) } else { content = pw.setKeyword("LANGUAGE", value: language, in: content) } let links = linksText.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } content = pw.setMultiKeyword("LINK", values: links, in: content) let contacts = contactsText.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } content = pw.setMultiKeyword("CONTACT", values: contacts, in: content) try await uploader.upload(content: content) FollowCoordinator.shared.updateCachedContent(content) saved = true } catch { errorMessage = error.localizedDescription } } // MARK: - Private helpers private func loadFromUserDefaults() { nick = UserDefaults.standard.string(forKey: "nick") ?? "" } private func loadFromContent(_ content: String) { // Quick parse: read header keyword values for line in content.components(separatedBy: "\n") { let t = line.trimmingCharacters(in: .whitespaces) if t == "* Posts" { break } guard t.hasPrefix("#+"), let colonIdx = t.firstIndex(of: ":") else { continue } let kw = String(t[t.index(t.startIndex, offsetBy: 2).. (any FeedUploader)? { UploaderFactory.makeUploader() } }