import Foundation import Observation import OrgSocialKit @Observable @MainActor final class SettingsViewModel { // Common var uploadMethod: UploadMethod var publicFeedURL: String var relayURL: String var maxPostAgeDays: Int var useRelay: Bool var showAllRelayFeeds: Bool // Sharing — preview service used by the per-post Share button. var previewServiceEnabled: Bool var previewServiceURL: String // VFile var vfileURL: String // GitHub var githubToken: String var githubOwner: String var githubRepo: String var githubPath: String var githubBranch: String // Codeberg var codebergToken: String var codebergOwner: String var codebergRepo: String var codebergPath: String var codebergBranch: String var codebergInstance: String // WebDAV var webdavURL: String var webdavUsername: String var webdavPassword: String private let d = UserDefaults.standard init() { uploadMethod = UploadMethod(rawValue: UserDefaults.standard.string(forKey: "uploadMethod") ?? "") ?? .vfile publicFeedURL = UserDefaults.standard.string(forKey: "publicFeedURL") ?? "" relayURL = UserDefaults.standard.string(forKey: "relayURL") ?? "https://relay.org-social.org" maxPostAgeDays = UserDefaults.standard.integer(forKey: "maxPostAgeDays").nonZero ?? 7 useRelay = UserDefaults.standard.object(forKey: "useRelay") as? Bool ?? true showAllRelayFeeds = UserDefaults.standard.object(forKey: "showAllRelayFeeds") as? Bool ?? false previewServiceEnabled = UserDefaults.standard.object(forKey: "previewServiceEnabled") as? Bool ?? true previewServiceURL = UserDefaults.standard.string(forKey: "previewServiceURL") ?? "https://preview.org-social.org/" vfileURL = UserDefaults.standard.string(forKey: "vfileURL") ?? "" githubToken = UserDefaults.standard.string(forKey: "githubToken") ?? "" githubOwner = UserDefaults.standard.string(forKey: "githubOwner") ?? "" githubRepo = UserDefaults.standard.string(forKey: "githubRepo") ?? "" githubPath = UserDefaults.standard.string(forKey: "githubPath").nilIfEmpty ?? "social.org" githubBranch = UserDefaults.standard.string(forKey: "githubBranch").nilIfEmpty ?? "main" codebergToken = UserDefaults.standard.string(forKey: "codebergToken") ?? "" codebergOwner = UserDefaults.standard.string(forKey: "codebergOwner") ?? "" codebergRepo = UserDefaults.standard.string(forKey: "codebergRepo") ?? "" codebergPath = UserDefaults.standard.string(forKey: "codebergPath").nilIfEmpty ?? "social.org" codebergBranch = UserDefaults.standard.string(forKey: "codebergBranch").nilIfEmpty ?? "main" codebergInstance = UserDefaults.standard.string(forKey: "codebergInstance").nilIfEmpty ?? "https://codeberg.org" webdavURL = UserDefaults.standard.string(forKey: "webdavURL") ?? "" webdavUsername = UserDefaults.standard.string(forKey: "webdavUsername") ?? "" webdavPassword = UserDefaults.standard.string(forKey: "webdavPassword") ?? "" } // MARK: - Validation var publicFeedURLValid: Bool { isHTTPURL(publicFeedURL) } var relayURLValid: Bool { isHTTPURL(relayURL) } var uploadConfigValid: Bool { switch uploadMethod { case .vfile: return isHTTPURL(vfileURL) case .github: return !githubToken.isEmpty && !githubOwner.isEmpty && !githubRepo.isEmpty case .codeberg: return !codebergToken.isEmpty && !codebergOwner.isEmpty && !codebergRepo.isEmpty case .webdav: return isHTTPURL(webdavURL) } } /// When the preview toggle is off the URL is irrelevant (the field is /// disabled), so we only require validity in the on state. var previewServiceValid: Bool { !previewServiceEnabled || isHTTPURL(previewServiceURL) } var canSave: Bool { publicFeedURLValid && relayURLValid && uploadConfigValid && previewServiceValid } // MARK: - Derived public URL hints var githubDerivedPublicURL: String { guard !githubOwner.isEmpty, !githubRepo.isEmpty else { return "" } let p = githubPath.isEmpty ? "social.org" : githubPath let b = githubBranch.isEmpty ? "main" : githubBranch return "https://raw.githubusercontent.com/\(githubOwner)/\(githubRepo)/\(b)/\(p)" } var codebergDerivedPublicURL: String { guard !codebergOwner.isEmpty, !codebergRepo.isEmpty else { return "" } let inst = codebergInstance.isEmpty ? "https://codeberg.org" : codebergInstance let p = codebergPath.isEmpty ? "social.org" : codebergPath let b = codebergBranch.isEmpty ? "main" : codebergBranch return "\(inst)/\(codebergOwner)/\(codebergRepo)/raw/branch/\(b)/\(p)" } // MARK: - Save func saveIfValid() { guard canSave else { return } d.set(uploadMethod.rawValue, forKey: "uploadMethod") d.set(publicFeedURL, forKey: "publicFeedURL") d.set(relayURL, forKey: "relayURL") d.set(maxPostAgeDays, forKey: "maxPostAgeDays") d.set(useRelay, forKey: "useRelay") d.set(showAllRelayFeeds, forKey: "showAllRelayFeeds") d.set(previewServiceEnabled, forKey: "previewServiceEnabled") d.set(previewServiceURL, forKey: "previewServiceURL") d.set(vfileURL, forKey: "vfileURL") d.set(githubToken, forKey: "githubToken") d.set(githubOwner, forKey: "githubOwner") d.set(githubRepo, forKey: "githubRepo") d.set(githubPath, forKey: "githubPath") d.set(githubBranch, forKey: "githubBranch") d.set(codebergToken, forKey: "codebergToken") d.set(codebergOwner, forKey: "codebergOwner") d.set(codebergRepo, forKey: "codebergRepo") d.set(codebergPath, forKey: "codebergPath") d.set(codebergBranch, forKey: "codebergBranch") d.set(codebergInstance, forKey: "codebergInstance") d.set(webdavURL, forKey: "webdavURL") d.set(webdavUsername, forKey: "webdavUsername") d.set(webdavPassword, forKey: "webdavPassword") } private func isHTTPURL(_ raw: String) -> Bool { guard let url = URL(string: raw) else { return false } return url.scheme?.hasPrefix("http") == true && url.host != nil } } private extension Int { var nonZero: Int? { self == 0 ? nil : self } } private extension String? { var nilIfEmpty: String? { self?.isEmpty == false ? self : nil } }