import SwiftUI struct SettingsSectionProviderView: 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 { Form { Section(header: Text("Provider Selection")) { 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)) } } } Section(header: Text("Moebooru Feed")) { Toggle("Show Held Posts", isOn: $settings.showHeldMoebooruPosts) } Section(header: Text("API Credentials")) { SecureField( "API Key", text: Binding( get: { settings.providerAPIKeys[settings.preferredBooru] ?? "" }, set: { updateCredentials(apiKey: $0) } ) ) .autocorrectionDisabled(true) TextField( isDanbooruProvider ? "Login" : "User ID", text: Binding( get: { if isDanbooruProvider { return settings.providerLogins[settings.preferredBooru] ?? "" } let userID = settings.providerUserIDs[settings.preferredBooru] ?? 0 return userID == 0 ? "" : String(userID) }, set: { newValue in if isDanbooruProvider { updateCredentials(login: newValue.trimmingCharacters(in: .whitespacesAndNewlines)) return } let userID = Int(newValue) ?? 0 updateCredentials(userID: userID) } ) ) .autocorrectionDisabled(true) #if os(iOS) .keyboardType(isDanbooruProvider ? .default : .numberPad) #endif } Section(header: Text("Custom Providers")) { Button("Add Custom Provider") { showingCustomBooruSheet = true } .trailingFrame() if case .custom(let provider) = settings.preferredBooru { Button("Remove Custom Provider") { settings.customProviders.removeAll { $0.id == provider.id } settings.preferredBooru = .safebooru } .disabled(!settings.customProviders.contains { $0.id == provider.id }) } Button("Remove All Custom Providers") { if case .custom = settings.preferredBooru { settings.preferredBooru = .safebooru } if !settings.customProviders.isEmpty { settings.customProviders.removeAll() } } .trailingFrame() } } #if os(macOS) .formStyle(.grouped) #endif .navigationTitle("Provider") #if !os(macOS) .navigationBarTitleDisplayMode(.large) #endif .sheet(isPresented: $showingCustomBooruSheet) { NavigationStack { Form { Section(header: Text("Provider Details")) { TextField("Domain", text: $newDomain) .autocorrectionDisabled(true) Picker("Provider Type", selection: $newFlavor) { ForEach(BooruProviderFlavor.allCases, id: \.self) { flavor in Text(flavor.rawValue).tag(flavor) } } } } #if os(macOS) .formStyle(.grouped) #endif .navigationTitle("Add Custom Provider") #if !os(macOS) .navigationBarTitleDisplayMode(.inline) #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) } } } } .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 var isDanbooruProvider: Bool { BooruProviderFlavor(provider: settings.preferredBooru) == .danbooru } private func updateCredentials(apiKey: String? = nil, userID: Int? = nil, login: String? = 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, login: login ?? credentials.login, id: credentials.id ) } else { allCredentials.append( BooruProviderCredentials( provider: settings.preferredBooru, apiKey: apiKey ?? "", userID: userID ?? 0, login: login ?? "" ) ) } settings.providerCredentials = allCredentials } } #Preview { NavigationStack { SettingsSectionProviderView() .environmentObject(SettingsManager()) } }