diff options
| author | Fuwn <[email protected]> | 2025-03-05 03:53:34 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-03-05 03:53:34 -0800 |
| commit | d10f51d93e35c11f883df536b13b7bb6382d463c (patch) | |
| tree | 59b786a352ad3adb16a61ca1d858e5e75807db90 | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-d10f51d93e35c11f883df536b13b7bb6382d463c.tar.xz sora-testing-d10f51d93e35c11f883df536b13b7bb6382d463c.zip | |
feat: Development commit
| -rw-r--r-- | Localizable.xcstrings | 24 | ||||
| -rw-r--r-- | Sora/Data/Booru/Provider/BooruProvider.swift | 10 | ||||
| -rw-r--r-- | Sora/Data/Booru/Provider/BooruProviderCustom.swift | 6 | ||||
| -rw-r--r-- | Sora/Data/Settings/SettingsManager.swift | 22 | ||||
| -rw-r--r-- | Sora/Views/Settings/Section/SettingsProviderView.swift | 148 |
5 files changed, 199 insertions, 11 deletions
diff --git a/Localizable.xcstrings b/Localizable.xcstrings index df67de2..1b601f2 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4,6 +4,12 @@ "%lld" : { }, + "Add" : { + + }, + "Add Custom Provider" : { + + }, "Add Dummy Bookmarks" : { }, @@ -51,6 +57,9 @@ "BookmarksAddToView" : { }, + "Cancel" : { + + }, "Clear Cached Tags (%@)" : { }, @@ -103,6 +112,9 @@ } } }, + "Domain" : { + + }, "Enable \"Share Image\" Shortcut" : { }, @@ -112,6 +124,9 @@ "Export Failed" : { }, + "Flavor" : { + + }, "Image Quality" : { }, @@ -124,6 +139,9 @@ "Import Failed" : { }, + "Invalid Domain" : { + + }, "Loading %@…" : { }, @@ -226,9 +244,15 @@ "Remove All Bookmarks" : { }, + "Remove All Custom Providers" : { + + }, "Remove All Searches" : { }, + "Remove Custom Provider" : { + + }, "Reset to Defaults" : { "localizations" : { "ja" : { diff --git a/Sora/Data/Booru/Provider/BooruProvider.swift b/Sora/Data/Booru/Provider/BooruProvider.swift index 84b76c8..49bf18a 100644 --- a/Sora/Data/Booru/Provider/BooruProvider.swift +++ b/Sora/Data/Booru/Provider/BooruProvider.swift @@ -9,7 +9,13 @@ enum BooruProvider: CaseIterable, Codable, Hashable, Equatable { // MARK: - Computed Properties var domain: String { - Self.domains[self] ?? rawValue.lowercased() + switch self { + case .custom(let provider): + provider.self.domain + + default: + Self.domains[self] ?? rawValue.lowercased() + } } var asFileNameComponent: String { @@ -45,7 +51,7 @@ enum BooruProvider: CaseIterable, Codable, Hashable, Equatable { "yande.re" case .custom(let provider): - provider.baseURL + provider.domain } } diff --git a/Sora/Data/Booru/Provider/BooruProviderCustom.swift b/Sora/Data/Booru/Provider/BooruProviderCustom.swift index a2be323..7d8cc28 100644 --- a/Sora/Data/Booru/Provider/BooruProviderCustom.swift +++ b/Sora/Data/Booru/Provider/BooruProviderCustom.swift @@ -2,12 +2,12 @@ import Foundation struct BooruProviderCustom: Identifiable, Codable, Hashable { let id: UUID - var baseURL: String + var domain: String var flavor: BooruProviderFlavor - init(baseURL: String, flavor: BooruProviderFlavor, id: UUID = UUID()) { + init(domain: String, flavor: BooruProviderFlavor, id: UUID = UUID()) { self.id = id - self.baseURL = baseURL + self.domain = domain self.flavor = flavor } } diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index f4273ef..984ffbe 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -14,9 +14,6 @@ class SettingsManager: ObservableObject { @AppStorage("thumbnailGridColumns") var thumbnailGridColumns = 2 - @AppStorage("preferredBooru") - var preferredBooruData = Data() - @AppStorage("enableShareShortcut") var enableShareShortcut = false @@ -44,6 +41,12 @@ class SettingsManager: ObservableObject { @AppStorage("searchHistory") private var searchHistoryData = Data() + @AppStorage("preferredBooru") + private var preferredBooruData = Data() + + @AppStorage("customProviders") + private var customProvidersData = Data() + // MARK: - Computed Properties var bookmarks: [SettingsBookmark] { get { @@ -85,6 +88,19 @@ class SettingsManager: ObservableObject { set { preferredBooruData = Self.encode(newValue) ?? preferredBooruData } } + var customProviders: [BooruProviderCustom] { + get { + Self.decode( + [BooruProviderCustom].self, + from: customProvidersData + ) ?? [] + } + + set { + customProvidersData = Self.encode(newValue) ?? customProvidersData + } + } + // MARK: - Private Helpers private static func encode<T: Encodable>(_ value: T) -> Data? { try? JSONEncoder().encode(value) diff --git a/Sora/Views/Settings/Section/SettingsProviderView.swift b/Sora/Views/Settings/Section/SettingsProviderView.swift index b561bc3..eb3fb1b 100644 --- a/Sora/Views/Settings/Section/SettingsProviderView.swift +++ b/Sora/Views/Settings/Section/SettingsProviderView.swift @@ -2,12 +2,154 @@ 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 { - Picker("Provider", selection: $settings.preferredBooru) { - ForEach(BooruProvider.allCases, id: \.self) { type in - Text(type.rawValue).tag(type) + 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)) + } + } + + Button("Add Custom Provider") { + showingCustomBooruSheet = true + } + .sheet(isPresented: $showingCustomBooruSheet) { + NavigationView { + Form { + TextField("Domain", text: $newDomain) + .autocorrectionDisabled(true) + + Picker("Flavor", selection: $newFlavor) { + ForEach(BooruProviderFlavor.allCases, id: \.self) { flavor in + Text(flavor.rawValue).tag(flavor) + } + } + } + .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 os(macOS) + .navigationTitle("Add Custom Provider") + #endif + } + } + + 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() + } } } + .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 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 }) } } |