diff --git a/CrystalFetch.xcodeproj/project.pbxproj b/CrystalFetch.xcodeproj/project.pbxproj index 1a45214..7838b91 100644 --- a/CrystalFetch.xcodeproj/project.pbxproj +++ b/CrystalFetch.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 63E2EBFA2AD1658800A32758 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E2EBF72AD1651700A32758 /* GeneralSettingsView.swift */; }; 844A8EFB2A860F91009A389C /* ESDCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8EFA2A860F91009A389C /* ESDCatalog.swift */; }; 844A8EFF2A86CA8C009A389C /* SimpleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8EFE2A86CA8C009A389C /* SimpleContentView.swift */; }; 844A8F032A86E86F009A389C /* EULAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A8F022A86E86F009A389C /* EULAView.swift */; }; @@ -494,6 +495,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 63E2EBF72AD1651700A32758 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; 844A8EFA2A860F91009A389C /* ESDCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESDCatalog.swift; sourceTree = ""; }; 844A8EFE2A86CA8C009A389C /* SimpleContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleContentView.swift; sourceTree = ""; }; 844A8F022A86E86F009A389C /* EULAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EULAView.swift; sourceTree = ""; }; @@ -976,6 +978,7 @@ CEC09F3E2A6F151800980857 /* CrystalFetch-Info.plist */, FFB1B5282A929CEC00B95D56 /* CrystalFetch-InfoPlist.strings */, CEC8BC522A7B55D80042878F /* Localizable.strings */, + 63E2EBF72AD1651700A32758 /* GeneralSettingsView.swift */, ); path = Source; sourceTree = ""; @@ -1595,6 +1598,7 @@ CEC09F372A6E5B7600980857 /* BuildDetails.swift in Sources */, CEC09F3B2A6E629F00980857 /* PrettyString.swift in Sources */, CEC09F3D2A6EECC700980857 /* Downloader.swift in Sources */, + 63E2EBFA2AD1658800A32758 /* GeneralSettingsView.swift in Sources */, CEC09F2D2A6DC60500980857 /* UUPDetails.swift in Sources */, CEC09F352A6DF33C00980857 /* BuildsListView.swift in Sources */, 84EB35722A870EA7004F252E /* ShowWindowButtonView.swift in Sources */, diff --git a/Source/GeneralSettingsView.swift b/Source/GeneralSettingsView.swift new file mode 100644 index 0000000..97784b7 --- /dev/null +++ b/Source/GeneralSettingsView.swift @@ -0,0 +1,40 @@ +// +// Copyright © 2023 Turing Software, LLC. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +let defaultUUPDumpJsonApiUrl = "https://uupdump.net/json-api/" + +struct GeneralSettingsView: View { + @AppStorage("uupDumpJsonApiUrl") private var uupDumpJsonApiUrl = defaultUUPDumpJsonApiUrl + + var body: some View { + Form { + TextField("UUP Dump JSON API URL", text: Binding( + get: { uupDumpJsonApiUrl }, + set: { uupDumpJsonApiUrl = $0.isBlank ? defaultUUPDumpJsonApiUrl : $0 }) + ).autocorrectionDisabled() + } + .padding(20) + .frame(width: 500, height: 100) + } +} + +extension String { + var isBlank: Bool { + return allSatisfy({ $0.isWhitespace }) + } +} diff --git a/Source/Main.swift b/Source/Main.swift index 0fdb9a7..e318015 100644 --- a/Source/Main.swift +++ b/Source/Main.swift @@ -32,5 +32,9 @@ struct Main: App { }.commands { SidebarCommands() }.handlesExternalEvents(matching: Set(["UUPDump"])) + + Settings { + GeneralSettingsView() + } } } diff --git a/Source/UUPDump/UUPDumpAPI.swift b/Source/UUPDump/UUPDumpAPI.swift index 2106e52..f2861d0 100644 --- a/Source/UUPDump/UUPDumpAPI.swift +++ b/Source/UUPDump/UUPDumpAPI.swift @@ -20,7 +20,6 @@ actor UUPDumpAPI { // Server has a rate limit of 10us so we don't make more than one request per 100us private let kTimeoutSec = TimeInterval(0.0001) private let kNsInSec = TimeInterval(1000000000) - private let uupDumpEndpointBase = URL(string: "https://uupdump.net/json-api/")! private var session = URLSession.shared private var lastRequestTime: Date? @@ -31,6 +30,10 @@ actor UUPDumpAPI { try Task.checkCancellation() } lastRequestTime = Date.now + let uupDumpEndpointRaw = UserDefaults.standard.string(forKey: "uupDumpJsonApiUrl")! + guard let uupDumpEndpointBase = URL(string: uupDumpEndpointRaw) else { + throw UUPDumpAPIError.errorRequest(uupDumpEndpointRaw, "Unable to parse URL") + } var components = URLComponents() components.scheme = uupDumpEndpointBase.scheme components.host = uupDumpEndpointBase.host @@ -44,7 +47,13 @@ actor UUPDumpAPI { return [] } } - let (data, _) = try await session.data(from: components.url!) + let data: Data + do { + (data, _) = try await session.data(from: components.url!) + } catch { + throw UUPDumpAPIError.errorRequest(uupDumpEndpointRaw, error.localizedDescription) + } + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { if let response = json["response"] as? [String: Any] { if let error = response["error"] as? String { @@ -55,6 +64,7 @@ actor UUPDumpAPI { } } } + NSLog("Invalid response: %@", String(data: data, encoding: .utf8) ?? "") throw UUPDumpAPIError.responseNotFound } @@ -77,6 +87,7 @@ actor UUPDumpAPI { enum UUPDumpAPIError: Error { case responseNotFound + case errorRequest(String, String) case errorResponse(String) } @@ -84,7 +95,9 @@ extension UUPDumpAPIError: LocalizedError { var errorDescription: String? { switch self { case .responseNotFound: return NSLocalizedString("Cannot find data from the server response.", comment: "UUPDumpAPI") + case .errorRequest(let url, let message): return String.localizedStringWithFormat(NSLocalizedString("Error calling server: %@ (%@)", comment: "UUPDumpAPI"), message, url) case .errorResponse(let message): return String.localizedStringWithFormat(NSLocalizedString("Error returned from server: %@", comment: "UUPDumpAPI"), message) + } } }