Nextcloud login flow v2, Network code rewrite
This commit is contained in:
@@ -15,10 +15,10 @@
|
||||
A701719E2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A701719D2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift */; };
|
||||
A70171A02AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */; };
|
||||
A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AC2AA8EF4700064C43 /* MainViewModel.swift */; };
|
||||
A70171AF2AB2116B00064C43 /* NetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AE2AB2116B00064C43 /* NetworkController.swift */; };
|
||||
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AE2AB2116B00064C43 /* NetworkHandler.swift */; };
|
||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; };
|
||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.swift */; };
|
||||
A70171B92AB399FB00064C43 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* Extensions.swift */; };
|
||||
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */; };
|
||||
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BB2AB4983500064C43 /* CategoryCardView.swift */; };
|
||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; };
|
||||
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.swift */; };
|
||||
@@ -28,6 +28,8 @@
|
||||
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C82AB4CBB400064C43 /* OnboardingView.swift */; };
|
||||
A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; };
|
||||
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; };
|
||||
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; };
|
||||
A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIInterface.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -60,10 +62,10 @@
|
||||
A701719D2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITests.swift; sourceTree = "<group>"; };
|
||||
A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
A70171AC2AA8EF4700064C43 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
|
||||
A70171AE2AB2116B00064C43 /* NetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkController.swift; sourceTree = "<group>"; };
|
||||
A70171AE2AB2116B00064C43 /* NetworkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHandler.swift; sourceTree = "<group>"; };
|
||||
A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = "<group>"; };
|
||||
A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = "<group>"; };
|
||||
A70171B82AB399FB00064C43 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
|
||||
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterExtension.swift; sourceTree = "<group>"; };
|
||||
A70171BB2AB4983500064C43 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = "<group>"; };
|
||||
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = "<group>"; };
|
||||
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = "<group>"; };
|
||||
@@ -73,6 +75,8 @@
|
||||
A70171C82AB4CBB400064C43 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
A70171CA2AB4CD1700064C43 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
|
||||
A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCoderExtension.swift; sourceTree = "<group>"; };
|
||||
A703226C2ABAF90D00D7C4ED /* APIInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIInterface.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -128,6 +132,7 @@
|
||||
A70171BA2AB4980100064C43 /* Views */,
|
||||
A70171B72AB2445700064C43 /* ViewModels */,
|
||||
A70171B22AB211F000064C43 /* Network */,
|
||||
A703226B2ABAF60D00D7C4ED /* Extensions */,
|
||||
A70171852AA8E71F00064C43 /* Assets.xcassets */,
|
||||
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
|
||||
A70171882AA8E71F00064C43 /* Preview Content */,
|
||||
@@ -163,9 +168,9 @@
|
||||
A70171B22AB211F000064C43 /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A70171B82AB399FB00064C43 /* Extensions.swift */,
|
||||
A703226C2ABAF90D00D7C4ED /* APIInterface.swift */,
|
||||
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
|
||||
A70171AE2AB2116B00064C43 /* NetworkController.swift */,
|
||||
A70171AE2AB2116B00064C43 /* NetworkHandler.swift */,
|
||||
A70171B02AB211DF00064C43 /* CustomError.swift */,
|
||||
);
|
||||
path = Network;
|
||||
@@ -203,6 +208,15 @@
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A703226B2ABAF60D00D7C4ED /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */,
|
||||
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -335,9 +349,9 @@
|
||||
files = (
|
||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
||||
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
|
||||
A70171AF2AB2116B00064C43 /* NetworkController.swift in Sources */,
|
||||
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
|
||||
A70171B92AB399FB00064C43 /* Extensions.swift in Sources */,
|
||||
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */,
|
||||
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */,
|
||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
|
||||
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
||||
@@ -346,6 +360,8 @@
|
||||
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
|
||||
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
||||
A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */,
|
||||
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */,
|
||||
A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */,
|
||||
A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */,
|
||||
A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */,
|
||||
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */,
|
||||
|
||||
@@ -47,7 +47,8 @@ struct RecipeDetail: Codable {
|
||||
keywords: "",
|
||||
dateCreated: "",
|
||||
dateModified: "",
|
||||
imageUrl: "", id: "",
|
||||
imageUrl: "",
|
||||
id: "",
|
||||
prepTime: "",
|
||||
cookTime: "",
|
||||
totalTime: "",
|
||||
@@ -68,4 +69,18 @@ struct RecipeImage {
|
||||
var full: UIImage?
|
||||
}
|
||||
|
||||
struct LoginV2Request: Codable {
|
||||
let poll: LoginV2Poll
|
||||
let login: String
|
||||
}
|
||||
|
||||
struct LoginV2Poll: Codable {
|
||||
let token: String
|
||||
let endpoint: String
|
||||
}
|
||||
|
||||
struct LoginV2Response: Codable {
|
||||
let server: String
|
||||
let loginName: String
|
||||
let appPassword: String
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Extensions.swift
|
||||
// DateFormatterExtension.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 14.09.23.
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// JSONCoderExtension.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 20.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension JSONDecoder {
|
||||
static func safeDecode<T: Decodable>(_ data: Data) -> T? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
print("Decoding type ", T.self, " ...")
|
||||
return try decoder.decode(T.self, from: data)
|
||||
} catch (let error) {
|
||||
print("JSONDecoder - safeDecode(): Failed to decode data.")
|
||||
print("Error: ", error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONEncoder {
|
||||
static func safeEncode<T: Encodable>(_ object: T) -> Data? {
|
||||
do {
|
||||
return try JSONEncoder().encode(object)
|
||||
} catch {
|
||||
print("JSONDecoder - safeEncode(): Could not encode object \(T.self)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
78
Nextcloud Cookbook iOS Client/Network/APIInterface.swift
Normal file
78
Nextcloud Cookbook iOS Client/Network/APIInterface.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// APIInterface.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 20.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class APIInterface {
|
||||
var userSettings: UserSettings
|
||||
|
||||
var apiPath: String
|
||||
var authString: String
|
||||
let apiVersion = "1"
|
||||
|
||||
init(userSettings: UserSettings) {
|
||||
print("Initializing NetworkController.")
|
||||
self.userSettings = userSettings
|
||||
|
||||
self.apiPath = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/"
|
||||
|
||||
let loginString = "\(userSettings.username):\(userSettings.token)"
|
||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||
self.authString = loginData.base64EncodedString()
|
||||
}
|
||||
|
||||
func imageDataFromServer(recipeId: Int, thumb: Bool) async -> Data? {
|
||||
do {
|
||||
let request = RequestWrapper.imageRequest(path: .IMAGE(recipeId: recipeId, thumb: thumb))
|
||||
let (data, _): (Data?, Error?) = try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: apiPath,
|
||||
authString: authString
|
||||
)
|
||||
guard let data = data else {
|
||||
print("Error receiving or decoding data.")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
} catch {
|
||||
print("Could not load image from server.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension APIInterface {
|
||||
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
||||
do {
|
||||
let (data, error) = try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: apiPath,
|
||||
authString: authString
|
||||
)
|
||||
if let data = data {
|
||||
return (JSONDecoder.safeDecode(data), error)
|
||||
}
|
||||
return (nil, error)
|
||||
} catch {
|
||||
print("An unknown network error occured.")
|
||||
}
|
||||
return (nil, NetworkError.unknownError)
|
||||
}
|
||||
|
||||
func sendRequest(_ request: RequestWrapper) async -> Error? {
|
||||
do {
|
||||
return try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: apiPath,
|
||||
authString: authString
|
||||
).1
|
||||
} catch {
|
||||
print("An unknown network error occured.")
|
||||
}
|
||||
return NetworkError.unknownError
|
||||
}
|
||||
}
|
||||
@@ -13,3 +13,15 @@ public enum NotImplementedError: Error, CustomStringConvertible {
|
||||
return "Function not implemented."
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkError: String, Error {
|
||||
case missingUrl = "Missing URL."
|
||||
case parametersNil = "Parameters are nil."
|
||||
case encodingFailed = "Parameter encoding failed."
|
||||
case redirectionError = "Redirection error"
|
||||
case clientError = "Client error"
|
||||
case serverError = "Server error"
|
||||
case invalidRequest = "Invalid request"
|
||||
case unknownError = "Unknown error"
|
||||
case dataError = "Invalid data error."
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
//
|
||||
// NetworkController.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum NetworkError: String, Error {
|
||||
case missingUrl = "Missing URL."
|
||||
case parametersNil = "Parameters are nil."
|
||||
case encodingFailed = "Parameter encoding failed."
|
||||
case redirectionError = "Redirection error"
|
||||
case clientError = "Client error"
|
||||
case serverError = "Server error"
|
||||
case invalidRequest = "Invalid request"
|
||||
case unknownError = "Unknown error"
|
||||
case dataError = "Invalid data error."
|
||||
}
|
||||
|
||||
class NetworkController {
|
||||
var userSettings: UserSettings
|
||||
var authString: String
|
||||
var cookBookUrlString: String
|
||||
|
||||
let apiVersion = "1"
|
||||
|
||||
init() {
|
||||
print("Initializing NetworkController.")
|
||||
self.userSettings = UserSettings()
|
||||
self.cookBookUrlString = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/"
|
||||
|
||||
let loginString = "\(userSettings.username):\(userSettings.token)"
|
||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||
self.authString = loginData.base64EncodedString()
|
||||
}
|
||||
|
||||
func fetchData(path: String) async throws -> Data? {
|
||||
|
||||
let url = URL(string: "\(cookBookUrlString)/\(path)")!
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
request.httpMethod = "GET"
|
||||
request.setValue(
|
||||
"true",
|
||||
forHTTPHeaderField: "OCS-APIRequest"
|
||||
)
|
||||
request.setValue(
|
||||
"Basic \(authString)",
|
||||
forHTTPHeaderField: "Authorization"
|
||||
)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
return data
|
||||
} catch {
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func sendHTTPRequest(_ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) {
|
||||
print("Sending \(requestWrapper.method.rawValue) request (path: \(requestWrapper.prepend(cookBookPath: cookBookUrlString))) ...")
|
||||
let urlStringSanitized = requestWrapper.prepend(cookBookPath: cookBookUrlString).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
let url = URL(string: urlStringSanitized!)!
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(
|
||||
"true",
|
||||
forHTTPHeaderField: "OCS-APIRequest"
|
||||
)
|
||||
request.setValue(
|
||||
"Basic \(authString)",
|
||||
forHTTPHeaderField: "Authorization"
|
||||
)
|
||||
|
||||
request.setValue(
|
||||
requestWrapper.accept.rawValue,
|
||||
forHTTPHeaderField: "Accept"
|
||||
)
|
||||
|
||||
request.httpMethod = requestWrapper.method.rawValue
|
||||
|
||||
switch requestWrapper.method {
|
||||
case .GET: break
|
||||
case .POST, .PUT:
|
||||
guard let httpBody = requestWrapper.body else { return (nil, nil) }
|
||||
do {
|
||||
print("Encoding request ...")
|
||||
request.httpBody = try JSONEncoder().encode(httpBody)
|
||||
print("Request body: \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "nil")")
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
case .DELETE: throw NotImplementedError.notImplemented
|
||||
}
|
||||
|
||||
var data: Data? = nil
|
||||
var response: URLResponse? = nil
|
||||
do {
|
||||
(data, response) = try await URLSession.shared.data(for: request)
|
||||
print("Response: ", response)
|
||||
return (data, nil)
|
||||
} catch {
|
||||
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
||||
}
|
||||
}
|
||||
|
||||
private func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? {
|
||||
guard let response = response else {
|
||||
return NetworkError.unknownError
|
||||
}
|
||||
switch response.statusCode {
|
||||
case 200...299: return (nil)
|
||||
case 300...399: return (NetworkError.redirectionError)
|
||||
case 400...499: return (NetworkError.clientError)
|
||||
case 500...599: return (NetworkError.serverError)
|
||||
case 600: return (NetworkError.invalidRequest)
|
||||
default: return (NetworkError.unknownError)
|
||||
}
|
||||
}
|
||||
|
||||
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
||||
do {
|
||||
let (data, error) = try await sendHTTPRequest(request)
|
||||
if let data = data {
|
||||
return (decodeData(data), error)
|
||||
}
|
||||
return (nil, error)
|
||||
} catch {
|
||||
print("An unknown network error occured.")
|
||||
}
|
||||
return (nil, NetworkError.unknownError)
|
||||
}
|
||||
|
||||
func sendRequest(_ request: RequestWrapper) async -> Error? {
|
||||
do {
|
||||
return try await sendHTTPRequest(request).1
|
||||
} catch {
|
||||
print("An unknown network error occured.")
|
||||
}
|
||||
return NetworkError.unknownError
|
||||
}
|
||||
|
||||
private func decodeData<D: Decodable>(_ data: Data) -> D? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
print("Decoding type ", D.self, " ...")
|
||||
return try decoder.decode(D.self, from: data)
|
||||
} catch (let error) {
|
||||
print("DataController - decodeData(): Failed to decode data.")
|
||||
print("Error: ", error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct NetworkHandler {
|
||||
static func sendHTTPRequest(_ requestWrapper: RequestWrapper, authString: String? = nil) async throws -> (Data?, NetworkError?) {
|
||||
print("Sending \(requestWrapper.method.rawValue) request (path: \(requestWrapper.path)) ...")
|
||||
let urlStringSanitized = requestWrapper.path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
let url = URL(string: urlStringSanitized!)!
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue(
|
||||
"true",
|
||||
forHTTPHeaderField: "OCS-APIRequest"
|
||||
)
|
||||
if let authString = authString {
|
||||
request.setValue(
|
||||
"Basic \(authString)",
|
||||
forHTTPHeaderField: "Authorization"
|
||||
)
|
||||
}
|
||||
request.setValue(
|
||||
requestWrapper.accept.rawValue,
|
||||
forHTTPHeaderField: "Accept"
|
||||
)
|
||||
|
||||
request.httpMethod = requestWrapper.method.rawValue
|
||||
|
||||
switch requestWrapper.method {
|
||||
case .GET: break
|
||||
case .POST, .PUT:
|
||||
guard let httpBody = requestWrapper.body else { break }
|
||||
do {
|
||||
print("Encoding request ...")
|
||||
request.httpBody = try JSONEncoder().encode(httpBody)
|
||||
print("Request body: \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "nil")")
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
case .DELETE: throw NotImplementedError.notImplemented
|
||||
}
|
||||
|
||||
var data: Data? = nil
|
||||
var response: URLResponse? = nil
|
||||
do {
|
||||
(data, response) = try await URLSession.shared.data(for: request)
|
||||
print("Response: ", response)
|
||||
return (data, nil)
|
||||
} catch {
|
||||
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
||||
}
|
||||
}
|
||||
|
||||
static func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? {
|
||||
guard let response = response else {
|
||||
return NetworkError.unknownError
|
||||
}
|
||||
switch response.statusCode {
|
||||
case 200...299: return (nil)
|
||||
case 300...399: return (NetworkError.redirectionError)
|
||||
case 400...499: return (NetworkError.clientError)
|
||||
case 500...599: return (NetworkError.serverError)
|
||||
case 600: return (NetworkError.invalidRequest)
|
||||
default: return (NetworkError.unknownError)
|
||||
}
|
||||
}
|
||||
|
||||
static func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
||||
do {
|
||||
let (data, error) = try await sendHTTPRequest(request)
|
||||
if let data = data {
|
||||
print(String(data: data, encoding: .utf8))
|
||||
return (decodeData(data), error)
|
||||
}
|
||||
return (nil, error)
|
||||
} catch {
|
||||
print("An unknown network error occured.")
|
||||
}
|
||||
return (nil, NetworkError.unknownError)
|
||||
}
|
||||
|
||||
private static func decodeData<D: Decodable>(_ data: Data) -> D? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
print("Decoding type ", D.self, " ...")
|
||||
return try decoder.decode(D.self, from: data)
|
||||
} catch (let error) {
|
||||
print("DataController - decodeData(): Failed to decode data.")
|
||||
print("Error: ", error)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
79
Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift
Normal file
79
Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// NetworkHandler.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 13.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
|
||||
struct NetworkHandler {
|
||||
static func sendHTTPRequest(
|
||||
_ requestWrapper: RequestWrapper,
|
||||
hostPath: String,
|
||||
authString: String?
|
||||
) async throws -> (Data?, NetworkError?) {
|
||||
print("Sending \(requestWrapper.getMethod()) request (path: \(requestWrapper.getPath())) ...")
|
||||
|
||||
// Prepare URL
|
||||
let urlString = hostPath + requestWrapper.getPath()
|
||||
let urlStringSanitized = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
let url = URL(string: urlStringSanitized!)!
|
||||
|
||||
// Create URL request
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
// Set URL method
|
||||
request.httpMethod = requestWrapper.getMethod()
|
||||
|
||||
// Set authentication string, if needed
|
||||
if let authString = authString {
|
||||
request.setValue(
|
||||
"Basic \(authString)",
|
||||
forHTTPHeaderField: "Authorization"
|
||||
)
|
||||
}
|
||||
|
||||
// Set other header fields
|
||||
for headerField in requestWrapper.getHeaderFields() {
|
||||
request.setValue(
|
||||
headerField.getValue(),
|
||||
forHTTPHeaderField: headerField.getField()
|
||||
)
|
||||
}
|
||||
|
||||
// Set http body
|
||||
if let body = requestWrapper.getBody() {
|
||||
request.httpBody = body
|
||||
}
|
||||
|
||||
print("Request:\nMethod: \(request.httpMethod)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)")
|
||||
|
||||
// Wait for and return data and (decoded) response
|
||||
var data: Data? = nil
|
||||
var response: URLResponse? = nil
|
||||
do {
|
||||
(data, response) = try await URLSession.shared.data(for: request)
|
||||
print("Response: ", response)
|
||||
return (data, nil)
|
||||
} catch {
|
||||
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
||||
}
|
||||
}
|
||||
|
||||
private static func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? {
|
||||
guard let response = response else {
|
||||
return NetworkError.unknownError
|
||||
}
|
||||
switch response.statusCode {
|
||||
case 200...299: return (nil)
|
||||
case 300...399: return (NetworkError.redirectionError)
|
||||
case 400...499: return (NetworkError.clientError)
|
||||
case 500...599: return (NetworkError.serverError)
|
||||
case 600: return (NetworkError.invalidRequest)
|
||||
default: return (NetworkError.unknownError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,47 +8,153 @@
|
||||
import Foundation
|
||||
|
||||
enum RequestMethod: String {
|
||||
case GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE"
|
||||
case GET = "GET",
|
||||
POST = "POST",
|
||||
PUT = "PUT",
|
||||
DELETE = "DELETE"
|
||||
}
|
||||
|
||||
enum RequestPath: String {
|
||||
case GET_CATEGORIES = "categories"
|
||||
enum RequestPath {
|
||||
case CATEGORIES,
|
||||
RECIPE_LIST(categoryName: String),
|
||||
RECIPE_DETAIL(recipeId: Int),
|
||||
IMAGE(recipeId: Int, thumb: Bool)
|
||||
|
||||
case LOGINV2REQ,
|
||||
CUSTOM(path: String),
|
||||
NONE
|
||||
|
||||
var stringValue: String {
|
||||
switch self {
|
||||
case .CATEGORIES: return "categories"
|
||||
case .RECIPE_LIST(categoryName: let name): return "category/\(name)"
|
||||
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)"
|
||||
case .IMAGE(recipeId: let recipeId, thumb: let thumb): return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
|
||||
|
||||
case .LOGINV2REQ: return "/index.php/login/v2"
|
||||
case .CUSTOM(path: let path): return path
|
||||
case .NONE: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AcceptHeader: String {
|
||||
case JSON = "application/json", IMAGE = "image/jpeg"
|
||||
enum ContentType: String {
|
||||
case JSON = "application/json",
|
||||
IMAGE = "image/jpeg",
|
||||
FORM = "application/x-www-form-urlencoded"
|
||||
}
|
||||
|
||||
struct HeaderField {
|
||||
private let _field: String
|
||||
private let _value: String
|
||||
|
||||
func getField() -> String {
|
||||
return _field
|
||||
}
|
||||
|
||||
func getValue() -> String {
|
||||
return _value
|
||||
}
|
||||
|
||||
static func accept(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "accept", _value: value.rawValue)
|
||||
}
|
||||
|
||||
static func ocsRequest(value: Bool) -> HeaderField {
|
||||
return HeaderField(_field: "OCS-APIRequest", _value: value ? "true" : "false")
|
||||
}
|
||||
|
||||
static func contentType(value: ContentType) -> HeaderField {
|
||||
return HeaderField(_field: "Content-Type", _value: value.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestWrapper {
|
||||
let method: RequestMethod
|
||||
var path: String
|
||||
let accept: AcceptHeader
|
||||
let body: Codable?
|
||||
private let _method: RequestMethod
|
||||
private let _path: RequestPath
|
||||
private let _headerFields: [HeaderField]
|
||||
private let _body: Data?
|
||||
private let _authenticate: Bool = true
|
||||
|
||||
init(method: RequestMethod, path: String, body: Codable? = nil, accept: AcceptHeader = .JSON) {
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.body = body
|
||||
self.accept = accept
|
||||
private init(
|
||||
method: RequestMethod,
|
||||
path: RequestPath,
|
||||
headerFields: [HeaderField] = [],
|
||||
body: Data? = nil,
|
||||
authenticate: Bool = true
|
||||
) {
|
||||
self._method = method
|
||||
self._path = path
|
||||
self._headerFields = headerFields
|
||||
self._body = body
|
||||
}
|
||||
|
||||
func prepend(cookBookPath: String) -> String {
|
||||
return cookBookPath + self.path
|
||||
func getMethod() -> String {
|
||||
return self._method.rawValue
|
||||
}
|
||||
|
||||
func getPath() -> String {
|
||||
return self._path.stringValue
|
||||
}
|
||||
|
||||
func getHeaderFields() -> [HeaderField] {
|
||||
return self._headerFields
|
||||
}
|
||||
|
||||
func getBody() -> Data? {
|
||||
return _body
|
||||
}
|
||||
|
||||
func needsAuth() -> Bool {
|
||||
return _authenticate
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginV2Request: Codable {
|
||||
let poll: LoginV2Poll
|
||||
let login: String
|
||||
extension RequestWrapper {
|
||||
static func customRequest(
|
||||
method: RequestMethod,
|
||||
path: RequestPath,
|
||||
headerFields: [HeaderField] = [],
|
||||
body: Data? = nil,
|
||||
authenticate: Bool = true
|
||||
) -> RequestWrapper {
|
||||
let request = RequestWrapper(
|
||||
method: method,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
body: body,
|
||||
authenticate: authenticate
|
||||
)
|
||||
return request
|
||||
}
|
||||
|
||||
static func jsonGetRequest(path: RequestPath) -> RequestWrapper {
|
||||
let headerFields = [
|
||||
HeaderField.ocsRequest(value: true),
|
||||
HeaderField.accept(value: .JSON)
|
||||
]
|
||||
let request = RequestWrapper(
|
||||
method: .GET,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
authenticate: true
|
||||
)
|
||||
return request
|
||||
}
|
||||
|
||||
static func imageRequest(path: RequestPath) -> RequestWrapper {
|
||||
let headerFields = [
|
||||
HeaderField.ocsRequest(value: true),
|
||||
HeaderField.accept(value: .IMAGE)
|
||||
]
|
||||
let request = RequestWrapper(
|
||||
method: .GET,
|
||||
path: path,
|
||||
headerFields: headerFields,
|
||||
authenticate: true
|
||||
)
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginV2Poll: Codable {
|
||||
let token: String
|
||||
let endpoint: String
|
||||
}
|
||||
|
||||
struct LoginV2Response: Codable {
|
||||
let server: String
|
||||
let loginName: String
|
||||
let appPassword: String
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@ import SwiftUI
|
||||
@main
|
||||
struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
||||
@StateObject var userSettings = UserSettings()
|
||||
@StateObject var mainViewModel = MainViewModel()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView(userSettings: userSettings)
|
||||
.fullScreenCover(isPresented: $userSettings.onboarding) {
|
||||
ZStack {
|
||||
if userSettings.onboarding {
|
||||
OnboardingView(userSettings: userSettings)
|
||||
} else {
|
||||
MainView(userSettings: userSettings, viewModel: mainViewModel)
|
||||
}
|
||||
}.transition(.slide)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,20 +15,19 @@ import UIKit
|
||||
private var imageCache: [Int: RecipeImage] = [:]
|
||||
|
||||
let dataStore: DataStore
|
||||
let networkController: NetworkController
|
||||
var apiInterface: APIInterface? = nil
|
||||
|
||||
/// The path of an image in storage
|
||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, full in
|
||||
return "image\(recipeId)_\(full ? "full" : "thumb")"
|
||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||
return "image\(recipeId)_\(thumb ? "thumb" : "full")"
|
||||
}
|
||||
|
||||
/// The path of an image on the server
|
||||
private var networkImagePath: (Int, Bool) -> (String) = { recipeId, full in
|
||||
return "recipes/\(recipeId)/image?size=\(full ? "full" : "thumb")"
|
||||
private var networkImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||
return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
|
||||
}
|
||||
|
||||
init() {
|
||||
self.networkController = NetworkController()
|
||||
self.dataStore = DataStore()
|
||||
}
|
||||
|
||||
@@ -36,7 +35,11 @@ import UIKit
|
||||
/// - Parameters
|
||||
/// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from store first.
|
||||
func loadCategoryList(needsUpdate: Bool = false) async {
|
||||
if let categoryList: [Category] = await load(localPath: "categories.data", networkPath: "categories", needsUpdate: needsUpdate) {
|
||||
if let categoryList: [Category] = await loadObject(
|
||||
localPath: "categories.data",
|
||||
networkPath: .CATEGORIES,
|
||||
needsUpdate: needsUpdate
|
||||
) {
|
||||
self.categories = categoryList
|
||||
}
|
||||
}
|
||||
@@ -46,7 +49,11 @@ import UIKit
|
||||
/// - categoryName: The name of the category containing the requested list of recipes.
|
||||
/// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from store first.
|
||||
func loadRecipeList(categoryName: String, needsUpdate: Bool = false) async {
|
||||
if let recipeList: [Recipe] = await load(localPath: "category_\(categoryName).data", networkPath: "category/\(categoryName)", needsUpdate: needsUpdate) {
|
||||
if let recipeList: [Recipe] = await loadObject(
|
||||
localPath: "category_\(categoryName).data",
|
||||
networkPath: .RECIPE_LIST(categoryName: categoryName),
|
||||
needsUpdate: needsUpdate
|
||||
) {
|
||||
recipes[categoryName] = recipeList
|
||||
}
|
||||
}
|
||||
@@ -62,7 +69,11 @@ import UIKit
|
||||
return recipeDetail
|
||||
}
|
||||
}
|
||||
if let recipeDetail: RecipeDetail = await load(localPath: "recipe\(recipeId).data", networkPath: "recipes/\(recipeId)", needsUpdate: needsUpdate) {
|
||||
if let recipeDetail: RecipeDetail = await loadObject(
|
||||
localPath: "recipe\(recipeId).data",
|
||||
networkPath: .RECIPE_DETAIL(recipeId: recipeId),
|
||||
needsUpdate: needsUpdate
|
||||
) {
|
||||
recipeDetails[recipeId] = recipeDetail
|
||||
return recipeDetail
|
||||
}
|
||||
@@ -75,7 +86,7 @@ import UIKit
|
||||
guard let recipeList = recipes[category.name] else { continue }
|
||||
for recipe in recipeList {
|
||||
let _ = await loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||
let _ = await loadImage(recipeId: recipe.recipe_id, full: false)
|
||||
let _ = await loadImage(recipeId: recipe.recipe_id, thumb: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,17 +111,18 @@ import UIKit
|
||||
/// - full: If true, load the full resolution image. Otherwise, load a thumbnail-sized image.
|
||||
/// - needsUpdate: Determines wether the image should be loaded directly from the server, or if it should be loaded from cache/store first.
|
||||
/// - Returns: The image if found locally or on the server, otherwise nil.
|
||||
func loadImage(recipeId: Int, full: Bool, needsUpdate: Bool = false) async -> UIImage? {
|
||||
print("loadImage(recipeId: \(recipeId), full: \(full), needsUpdate: \(needsUpdate))")
|
||||
func loadImage(recipeId: Int, thumb: Bool, needsUpdate: Bool = false) async -> UIImage? {
|
||||
print("loadImage(recipeId: \(recipeId), thumb: \(thumb), needsUpdate: \(needsUpdate))")
|
||||
// If the image needs an update, request it from the server and overwrite the stored image
|
||||
if needsUpdate {
|
||||
if let data = await imageDataFromServer(recipeId: recipeId, full: full) {
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
guard let image = UIImage(data: data) else {
|
||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||
return nil
|
||||
}
|
||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full))
|
||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
|
||||
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||
return image
|
||||
} else {
|
||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||
@@ -126,30 +138,31 @@ import UIKit
|
||||
|
||||
// Try to load image from cache
|
||||
print("Attempting to load image from cache ...")
|
||||
if let image = imageFromCache(recipeId: recipeId, full: full) {
|
||||
if let image = imageFromCache(recipeId: recipeId, thumb: thumb) {
|
||||
print("Image found in cache.")
|
||||
return image
|
||||
}
|
||||
|
||||
// Try to load from store
|
||||
print("Attempting to load image from local storage ...")
|
||||
if let image = await imageFromStore(recipeId: recipeId, full: full) {
|
||||
if let image = await imageFromStore(recipeId: recipeId, thumb: thumb) {
|
||||
print("Image found in local storage.")
|
||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
||||
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||
return image
|
||||
}
|
||||
|
||||
// Try to load from the server. Store if successfull.
|
||||
print("Attempting to load image from server ...")
|
||||
if let data = await imageDataFromServer(recipeId: recipeId, full: full) {
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
if let data = await apiInterface.imageDataFromServer(recipeId: recipeId, thumb: thumb) {
|
||||
print("Image data received.")
|
||||
// Create empty RecipeImage for each recipe even if no image found, so that further server requests are only sent if explicitly requested.
|
||||
guard let image = UIImage(data: data) else {
|
||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||
return nil
|
||||
}
|
||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full))
|
||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
|
||||
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||
return image
|
||||
}
|
||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||
@@ -170,14 +183,15 @@ import UIKit
|
||||
|
||||
|
||||
extension MainViewModel {
|
||||
private func load<D: Codable>(localPath: String, networkPath: String, needsUpdate: Bool = false) async -> D? {
|
||||
private func loadObject<T: Codable>(localPath: String, networkPath: RequestPath, needsUpdate: Bool = false) async -> T? {
|
||||
do {
|
||||
if !needsUpdate, let data: D = try await dataStore.load(fromPath: localPath) {
|
||||
if !needsUpdate, let data: T = try await dataStore.load(fromPath: localPath) {
|
||||
print("Data found locally.")
|
||||
return data
|
||||
} else {
|
||||
let request = RequestWrapper(method: .GET, path: networkPath)
|
||||
let (data, error): (D?, Error?) = await networkController.sendDataRequest(request)
|
||||
guard let apiInterface = apiInterface else { return nil }
|
||||
let request = RequestWrapper.jsonGetRequest(path: networkPath)
|
||||
let (data, error): (T?, Error?) = await apiInterface.sendDataRequest(request)
|
||||
print(error as Any)
|
||||
if let data = data {
|
||||
await dataStore.save(data: data, toPath: localPath)
|
||||
@@ -190,33 +204,33 @@ extension MainViewModel {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func imageToCache(image: UIImage, recipeId: Int, full: Bool) {
|
||||
private func imageToCache(image: UIImage, recipeId: Int, thumb: Bool) {
|
||||
if imageCache[recipeId] == nil {
|
||||
imageCache[recipeId] = RecipeImage(imageExists: true)
|
||||
}
|
||||
if full {
|
||||
imageCache[recipeId]!.imageExists = true
|
||||
imageCache[recipeId]!.full = image
|
||||
} else {
|
||||
if thumb {
|
||||
imageCache[recipeId]!.imageExists = true
|
||||
imageCache[recipeId]!.thumb = image
|
||||
} else {
|
||||
imageCache[recipeId]!.imageExists = true
|
||||
imageCache[recipeId]!.full = image
|
||||
}
|
||||
}
|
||||
|
||||
private func imageFromCache(recipeId: Int, full: Bool) -> UIImage? {
|
||||
private func imageFromCache(recipeId: Int, thumb: Bool) -> UIImage? {
|
||||
if imageCache[recipeId] != nil {
|
||||
if full {
|
||||
return imageCache[recipeId]!.full
|
||||
} else {
|
||||
if thumb {
|
||||
return imageCache[recipeId]!.thumb
|
||||
} else {
|
||||
return imageCache[recipeId]!.full
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func imageFromStore(recipeId: Int, full: Bool) async -> UIImage? {
|
||||
private func imageFromStore(recipeId: Int, thumb: Bool) async -> UIImage? {
|
||||
do {
|
||||
let localPath = localImagePath(recipeId, full)
|
||||
let localPath = localImagePath(recipeId, thumb)
|
||||
if let data: String = try await dataStore.load(fromPath: localPath) {
|
||||
guard let dataDecoded = Data(base64Encoded: data) else { return nil }
|
||||
let image = UIImage(data: dataDecoded)
|
||||
@@ -228,22 +242,6 @@ extension MainViewModel {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func imageDataFromServer(recipeId: Int, full: Bool) async -> Data? {
|
||||
do {
|
||||
let networkPath = networkImagePath(recipeId, full)
|
||||
let request = RequestWrapper(method: .GET, path: networkPath, accept: .IMAGE)
|
||||
let (data, _): (Data?, Error?) = try await networkController.sendHTTPRequest(request)
|
||||
guard let data = data else {
|
||||
print("Error receiving or decoding data.")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
} catch {
|
||||
print("Could not load image from server.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ struct RecipeBookView: View {
|
||||
for recipe in recipes {
|
||||
Task {
|
||||
let _ = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
||||
let _ = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true)
|
||||
let _ = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@StateObject var viewModel = MainViewModel()
|
||||
@StateObject var userSettings: UserSettings
|
||||
@ObservedObject var viewModel: MainViewModel
|
||||
@ObservedObject var userSettings: UserSettings
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||
|
||||
init(userSettings: UserSettings, viewModel: MainViewModel) {
|
||||
self.userSettings = userSettings
|
||||
self.viewModel = viewModel
|
||||
self.viewModel.apiInterface = APIInterface(userSettings: userSettings)
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
@@ -60,11 +68,6 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct MainView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainView(userSettings: UserSettings())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ struct LoginTab: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Submitting will open a web browser. Please follow the login instructions provided there.\nAfter a successfull login, return to this application and press 'Done'.")
|
||||
Text("Submitting will open a web browser. Please follow the login instructions provided there.\nAfter a successfull login, return to this application and press 'Validate'.")
|
||||
.font(.subheadline)
|
||||
.padding(.bottom)
|
||||
.tint(.white)
|
||||
@@ -152,10 +152,13 @@ struct LoginTab: View {
|
||||
// fetch login v2 response
|
||||
Task {
|
||||
guard let res = await fetchLoginV2Response() else { return }
|
||||
print(res.loginName)
|
||||
print("Login successfull for user \(res.loginName)!")
|
||||
userSettings.username = res.loginName
|
||||
userSettings.token = res.appPassword
|
||||
userSettings.onboarding = false
|
||||
}
|
||||
} label: {
|
||||
Text("Done")
|
||||
Text("Validate")
|
||||
.foregroundColor(.white)
|
||||
.font(.headline)
|
||||
.padding()
|
||||
@@ -188,23 +191,64 @@ struct LoginTab: View {
|
||||
}
|
||||
|
||||
func sendLoginV2Request() async {
|
||||
let request = RequestWrapper(
|
||||
let hostPath = "https://\(userSettings.serverAddress)"
|
||||
let headerFields: [HeaderField] = [
|
||||
//HeaderField.ocsRequest(value: true),
|
||||
//HeaderField.accept(value: .JSON)
|
||||
]
|
||||
let request = RequestWrapper.customRequest(
|
||||
method: .POST,
|
||||
path: "https://\(userSettings.serverAddress)/index.php/login/v2"
|
||||
path: .LOGINV2REQ,
|
||||
headerFields: headerFields
|
||||
)
|
||||
let (loginReq, _): (LoginV2Request?, Error?) = await NetworkHandler.sendDataRequest(request)
|
||||
self.loginRequest = loginReq
|
||||
do {
|
||||
let (data, _): (Data?, Error?) = try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: hostPath,
|
||||
authString: nil
|
||||
)
|
||||
|
||||
guard let data = data else { return }
|
||||
print("Data: \(data)")
|
||||
let loginReq: LoginV2Request? = JSONDecoder.safeDecode(data)
|
||||
self.loginRequest = loginReq
|
||||
} catch {
|
||||
print("Could not establish communication with the server.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func fetchLoginV2Response() async -> LoginV2Response? {
|
||||
guard let loginRequest = loginRequest else { return nil }
|
||||
let request = RequestWrapper(
|
||||
let headerFields = [
|
||||
HeaderField.ocsRequest(value: true),
|
||||
HeaderField.accept(value: .JSON),
|
||||
HeaderField.contentType(value: .FORM)
|
||||
]
|
||||
let request = RequestWrapper.customRequest(
|
||||
method: .POST,
|
||||
path: loginRequest.poll.endpoint,
|
||||
body: "token=\(loginRequest.poll.token)"
|
||||
path: .NONE,
|
||||
headerFields: headerFields,
|
||||
body: "token=\(loginRequest.poll.token)".data(using: .utf8),
|
||||
authenticate: false
|
||||
)
|
||||
let (loginRes, _): (LoginV2Response?, Error?) = await NetworkHandler.sendDataRequest(request)
|
||||
return loginRes
|
||||
|
||||
var (data, error): (Data?, Error?) = (nil, nil)
|
||||
do {
|
||||
(data, error) = try await NetworkHandler.sendHTTPRequest(
|
||||
request,
|
||||
hostPath: loginRequest.poll.endpoint,
|
||||
authString: nil
|
||||
)
|
||||
} catch {
|
||||
print("Error: ", error)
|
||||
}
|
||||
guard let data = data else { return nil }
|
||||
if let loginRes: LoginV2Response = JSONDecoder.safeDecode(data) {
|
||||
return loginRes
|
||||
}
|
||||
print("Could not decode.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ struct RecipeCardView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.padding(.horizontal)
|
||||
.task {
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false)
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true)
|
||||
}
|
||||
.refreshable {
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false, needsUpdate: true)
|
||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,11 @@ struct RecipeDetailView: View {
|
||||
.navigationTitle(showTitle ? recipe.name : "")
|
||||
.task {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false)
|
||||
}
|
||||
.refreshable {
|
||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true, needsUpdate: true)
|
||||
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false, needsUpdate: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user