import SwiftUI struct SettingsProviderView: View { @EnvironmentObject var settings: SettingsManager @State private var showingCustomBooruSheet = false @State private var newDomain: String = "" @State private var newFlavor: BooruProviderFlavor = .danbooru @State private var domainError: String? var body: some View { Group { Picker("Provider", selection: $settings.preferredBooru) { ForEach(BooruProvider.allCases, id: \.self) { type in Text(type.rawValue).tag(type) } ForEach(settings.customProviders, id: \.id) { provider in Text(provider.domain) .tag(BooruProvider.custom(provider)) } } SecureField( "API Key", text: Binding( get: { settings.providerAPIKeys[settings.preferredBooru] ?? "" }, set: { updateCredentials(apiKey: $0) } ) ) .autocorrectionDisabled(true) TextField( "User ID", text: Binding( get: { String(settings.providerUserIDs[settings.preferredBooru] ?? 0) }, set: { newValue in let userID = Int(newValue) ?? 0 updateCredentials(userID: userID) } ) ) .autocorrectionDisabled(true) #if os(iOS) .keyboardType(.numberPad) #endif Button("Add Custom Provider") { showingCustomBooruSheet = true } .trailingFrame() .sheet(isPresented: $showingCustomBooruSheet) { Form { TextField("Domain", text: $newDomain) .autocorrectionDisabled(true) Picker("Flavor", selection: $newFlavor) { ForEach(BooruProviderFlavor.allCases, id: \.self) { flavor in Text(flavor.rawValue).tag(flavor) } } } #if os(macOS) .formStyle(.grouped) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { showingCustomBooruSheet = false resetForm() } } ToolbarItem(placement: .confirmationAction) { Button("Add") { if validateDomain() { addCustomProvider() showingCustomBooruSheet = false } } .disabled(newDomain.isEmpty || domainError != nil) } } } if case .custom(let provider) = settings.preferredBooru { removeCustomProviderButtonContent(provider) } Button("Remove All Custom Providers") { if case .custom = settings.preferredBooru { settings.preferredBooru = .safebooru } if !settings.customProviders.isEmpty { settings.customProviders.removeAll() } } .trailingFrame() } .alert( "Invalid Domain", isPresented: Binding( get: { domainError != nil }, set: { if !$0 { domainError = nil } } ) ) { Button("OK", role: .cancel) { () } } message: { Text(domainError ?? "An unknown error occurred while validating the domain.") } } private func addCustomProvider() { let customProvider = BooruProviderCustom( domain: newDomain.lowercased(), flavor: newFlavor ) settings.customProviders.append(customProvider) resetForm() } private func resetForm() { newDomain = "" newFlavor = .danbooru domainError = nil } @discardableResult private func validateDomain() -> Bool { guard !newDomain.isEmpty else { domainError = nil return false } let domain = newDomain.lowercased().trimmingCharacters(in: .whitespaces) let domainRegex = "^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\\.[a-z]{2,}$" guard NSPredicate(format: "SELF MATCHES %@", domainRegex).evaluate(with: domain) else { domainError = "Please enter a valid domain name, such as yande.re." return false } guard !domain.contains("://"), !domain.contains("/"), !domain.contains("?") else { domainError = "Only enter the domain name—leave out 'http://' or extra details." return false } guard domain.count <= 253 else { // RFC 1035 domainError = "This domain name is too long. It must be 253 characters or fewer." return false } let labels = domain.split(separator: ".") guard labels.allSatisfy({ $0.count <= 63 }) else { domainError = "Each section of the domain name must be 63 characters or fewer." return false } domainError = nil return true } private func updateCredentials(apiKey: String? = nil, userID: Int? = nil) { var allCredentials = settings.providerCredentials if let index = allCredentials.firstIndex(where: { $0.provider == settings.preferredBooru }) { let credentials = allCredentials[index] allCredentials[index] = BooruProviderCredentials( provider: credentials.provider, apiKey: apiKey ?? credentials.apiKey, userID: userID ?? credentials.userID, id: credentials.id ) } else { allCredentials.append( BooruProviderCredentials( provider: settings.preferredBooru, apiKey: apiKey ?? "", userID: userID ?? 0 ) ) } settings.providerCredentials = allCredentials } private func removeCustomProviderButtonContent(_ provider: BooruProviderCustom) -> some View { Button("Remove Custom Provider") { settings.customProviders.removeAll { $0.id == provider.id } settings.preferredBooru = .safebooru } .disabled(!settings.customProviders.contains { $0.id == provider.id }) .trailingFrame() } }