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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; };
|
||||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.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 */; };
|
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BB2AB4983500064C43 /* CategoryCardView.swift */; };
|
||||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; };
|
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; };
|
||||||
A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.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 */; };
|
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C82AB4CBB400064C43 /* OnboardingView.swift */; };
|
||||||
A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; };
|
A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; };
|
||||||
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -128,6 +132,7 @@
|
|||||||
A70171BA2AB4980100064C43 /* Views */,
|
A70171BA2AB4980100064C43 /* Views */,
|
||||||
A70171B72AB2445700064C43 /* ViewModels */,
|
A70171B72AB2445700064C43 /* ViewModels */,
|
||||||
A70171B22AB211F000064C43 /* Network */,
|
A70171B22AB211F000064C43 /* Network */,
|
||||||
|
A703226B2ABAF60D00D7C4ED /* Extensions */,
|
||||||
A70171852AA8E71F00064C43 /* Assets.xcassets */,
|
A70171852AA8E71F00064C43 /* Assets.xcassets */,
|
||||||
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
|
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
|
||||||
A70171882AA8E71F00064C43 /* Preview Content */,
|
A70171882AA8E71F00064C43 /* Preview Content */,
|
||||||
@@ -163,9 +168,9 @@
|
|||||||
A70171B22AB211F000064C43 /* Network */ = {
|
A70171B22AB211F000064C43 /* Network */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A70171B82AB399FB00064C43 /* Extensions.swift */,
|
A703226C2ABAF90D00D7C4ED /* APIInterface.swift */,
|
||||||
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
|
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
|
||||||
A70171AE2AB2116B00064C43 /* NetworkController.swift */,
|
A70171AE2AB2116B00064C43 /* NetworkHandler.swift */,
|
||||||
A70171B02AB211DF00064C43 /* CustomError.swift */,
|
A70171B02AB211DF00064C43 /* CustomError.swift */,
|
||||||
);
|
);
|
||||||
path = Network;
|
path = Network;
|
||||||
@@ -203,6 +208,15 @@
|
|||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A703226B2ABAF60D00D7C4ED /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */,
|
||||||
|
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -335,9 +349,9 @@
|
|||||||
files = (
|
files = (
|
||||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
||||||
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
|
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
|
||||||
A70171AF2AB2116B00064C43 /* NetworkController.swift in Sources */,
|
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
||||||
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
|
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
|
||||||
A70171B92AB399FB00064C43 /* Extensions.swift in Sources */,
|
A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */,
|
||||||
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */,
|
A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */,
|
||||||
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
|
A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */,
|
||||||
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
||||||
@@ -346,6 +360,8 @@
|
|||||||
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
|
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */,
|
||||||
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
||||||
A70171CB2AB4CD1700064C43 /* UserDefaults.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 */,
|
A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */,
|
||||||
A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */,
|
A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */,
|
||||||
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */,
|
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */,
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ struct RecipeDetail: Codable {
|
|||||||
keywords: "",
|
keywords: "",
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
dateModified: "",
|
dateModified: "",
|
||||||
imageUrl: "", id: "",
|
imageUrl: "",
|
||||||
|
id: "",
|
||||||
prepTime: "",
|
prepTime: "",
|
||||||
cookTime: "",
|
cookTime: "",
|
||||||
totalTime: "",
|
totalTime: "",
|
||||||
@@ -68,4 +69,18 @@ struct RecipeImage {
|
|||||||
var full: UIImage?
|
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
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 14.09.23.
|
// 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."
|
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
|
import Foundation
|
||||||
|
|
||||||
enum RequestMethod: String {
|
enum RequestMethod: String {
|
||||||
case GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE"
|
case GET = "GET",
|
||||||
|
POST = "POST",
|
||||||
|
PUT = "PUT",
|
||||||
|
DELETE = "DELETE"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RequestPath: String {
|
enum RequestPath {
|
||||||
case GET_CATEGORIES = "categories"
|
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 {
|
enum ContentType: String {
|
||||||
case JSON = "application/json", IMAGE = "image/jpeg"
|
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 {
|
struct RequestWrapper {
|
||||||
let method: RequestMethod
|
private let _method: RequestMethod
|
||||||
var path: String
|
private let _path: RequestPath
|
||||||
let accept: AcceptHeader
|
private let _headerFields: [HeaderField]
|
||||||
let body: Codable?
|
private let _body: Data?
|
||||||
|
private let _authenticate: Bool = true
|
||||||
|
|
||||||
init(method: RequestMethod, path: String, body: Codable? = nil, accept: AcceptHeader = .JSON) {
|
private init(
|
||||||
self.method = method
|
method: RequestMethod,
|
||||||
self.path = path
|
path: RequestPath,
|
||||||
self.body = body
|
headerFields: [HeaderField] = [],
|
||||||
self.accept = accept
|
body: Data? = nil,
|
||||||
|
authenticate: Bool = true
|
||||||
|
) {
|
||||||
|
self._method = method
|
||||||
|
self._path = path
|
||||||
|
self._headerFields = headerFields
|
||||||
|
self._body = body
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepend(cookBookPath: String) -> String {
|
func getMethod() -> String {
|
||||||
return cookBookPath + self.path
|
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 {
|
extension RequestWrapper {
|
||||||
let poll: LoginV2Poll
|
static func customRequest(
|
||||||
let login: String
|
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
|
@main
|
||||||
struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
||||||
@StateObject var userSettings = UserSettings()
|
@StateObject var userSettings = UserSettings()
|
||||||
|
@StateObject var mainViewModel = MainViewModel()
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
MainView(userSettings: userSettings)
|
ZStack {
|
||||||
.fullScreenCover(isPresented: $userSettings.onboarding) {
|
if userSettings.onboarding {
|
||||||
OnboardingView(userSettings: userSettings)
|
OnboardingView(userSettings: userSettings)
|
||||||
|
} else {
|
||||||
|
MainView(userSettings: userSettings, viewModel: mainViewModel)
|
||||||
}
|
}
|
||||||
|
}.transition(.slide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,19 @@ import UIKit
|
|||||||
private var imageCache: [Int: RecipeImage] = [:]
|
private var imageCache: [Int: RecipeImage] = [:]
|
||||||
|
|
||||||
let dataStore: DataStore
|
let dataStore: DataStore
|
||||||
let networkController: NetworkController
|
var apiInterface: APIInterface? = nil
|
||||||
|
|
||||||
/// The path of an image in storage
|
/// The path of an image in storage
|
||||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, full in
|
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||||
return "image\(recipeId)_\(full ? "full" : "thumb")"
|
return "image\(recipeId)_\(thumb ? "thumb" : "full")"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The path of an image on the server
|
/// The path of an image on the server
|
||||||
private var networkImagePath: (Int, Bool) -> (String) = { recipeId, full in
|
private var networkImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||||
return "recipes/\(recipeId)/image?size=\(full ? "full" : "thumb")"
|
return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.networkController = NetworkController()
|
|
||||||
self.dataStore = DataStore()
|
self.dataStore = DataStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +35,11 @@ import UIKit
|
|||||||
/// - Parameters
|
/// - Parameters
|
||||||
/// - needsUpdate: If true, the recipe will be loaded from the server directly, otherwise it will be loaded from store first.
|
/// - 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 {
|
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
|
self.categories = categoryList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +49,11 @@ import UIKit
|
|||||||
/// - categoryName: The name of the category containing the requested list of recipes.
|
/// - 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.
|
/// - 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 {
|
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
|
recipes[categoryName] = recipeList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +69,11 @@ import UIKit
|
|||||||
return recipeDetail
|
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
|
recipeDetails[recipeId] = recipeDetail
|
||||||
return recipeDetail
|
return recipeDetail
|
||||||
}
|
}
|
||||||
@@ -75,7 +86,7 @@ import UIKit
|
|||||||
guard let recipeList = recipes[category.name] else { continue }
|
guard let recipeList = recipes[category.name] else { continue }
|
||||||
for recipe in recipeList {
|
for recipe in recipeList {
|
||||||
let _ = await loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
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.
|
/// - 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.
|
/// - 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.
|
/// - Returns: The image if found locally or on the server, otherwise nil.
|
||||||
func loadImage(recipeId: Int, full: Bool, needsUpdate: Bool = false) async -> UIImage? {
|
func loadImage(recipeId: Int, thumb: Bool, needsUpdate: Bool = false) async -> UIImage? {
|
||||||
print("loadImage(recipeId: \(recipeId), full: \(full), needsUpdate: \(needsUpdate))")
|
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 the image needs an update, request it from the server and overwrite the stored image
|
||||||
if needsUpdate {
|
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 {
|
guard let image = UIImage(data: data) else {
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full))
|
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
|
||||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||||
return image
|
return image
|
||||||
} else {
|
} else {
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||||
@@ -126,30 +138,31 @@ import UIKit
|
|||||||
|
|
||||||
// Try to load image from cache
|
// Try to load image from cache
|
||||||
print("Attempting 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.")
|
print("Image found in cache.")
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from store
|
// Try to load from store
|
||||||
print("Attempting to load image from local storage ...")
|
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.")
|
print("Image found in local storage.")
|
||||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from the server. Store if successfull.
|
// Try to load from the server. Store if successfull.
|
||||||
print("Attempting to load image from server ...")
|
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.")
|
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.
|
// 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 {
|
guard let image = UIImage(data: data) else {
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, full))
|
await dataStore.save(data: data.base64EncodedString(), toPath: localImagePath(recipeId, thumb))
|
||||||
imageToCache(image: image, recipeId: recipeId, full: full)
|
imageToCache(image: image, recipeId: recipeId, thumb: thumb)
|
||||||
return image
|
return image
|
||||||
}
|
}
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: false)
|
imageCache[recipeId] = RecipeImage(imageExists: false)
|
||||||
@@ -170,14 +183,15 @@ import UIKit
|
|||||||
|
|
||||||
|
|
||||||
extension MainViewModel {
|
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 {
|
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.")
|
print("Data found locally.")
|
||||||
return data
|
return data
|
||||||
} else {
|
} else {
|
||||||
let request = RequestWrapper(method: .GET, path: networkPath)
|
guard let apiInterface = apiInterface else { return nil }
|
||||||
let (data, error): (D?, Error?) = await networkController.sendDataRequest(request)
|
let request = RequestWrapper.jsonGetRequest(path: networkPath)
|
||||||
|
let (data, error): (T?, Error?) = await apiInterface.sendDataRequest(request)
|
||||||
print(error as Any)
|
print(error as Any)
|
||||||
if let data = data {
|
if let data = data {
|
||||||
await dataStore.save(data: data, toPath: localPath)
|
await dataStore.save(data: data, toPath: localPath)
|
||||||
@@ -190,33 +204,33 @@ extension MainViewModel {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func imageToCache(image: UIImage, recipeId: Int, full: Bool) {
|
private func imageToCache(image: UIImage, recipeId: Int, thumb: Bool) {
|
||||||
if imageCache[recipeId] == nil {
|
if imageCache[recipeId] == nil {
|
||||||
imageCache[recipeId] = RecipeImage(imageExists: true)
|
imageCache[recipeId] = RecipeImage(imageExists: true)
|
||||||
}
|
}
|
||||||
if full {
|
if thumb {
|
||||||
imageCache[recipeId]!.imageExists = true
|
|
||||||
imageCache[recipeId]!.full = image
|
|
||||||
} else {
|
|
||||||
imageCache[recipeId]!.imageExists = true
|
imageCache[recipeId]!.imageExists = true
|
||||||
imageCache[recipeId]!.thumb = image
|
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 imageCache[recipeId] != nil {
|
||||||
if full {
|
if thumb {
|
||||||
return imageCache[recipeId]!.full
|
|
||||||
} else {
|
|
||||||
return imageCache[recipeId]!.thumb
|
return imageCache[recipeId]!.thumb
|
||||||
|
} else {
|
||||||
|
return imageCache[recipeId]!.full
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func imageFromStore(recipeId: Int, full: Bool) async -> UIImage? {
|
private func imageFromStore(recipeId: Int, thumb: Bool) async -> UIImage? {
|
||||||
do {
|
do {
|
||||||
let localPath = localImagePath(recipeId, full)
|
let localPath = localImagePath(recipeId, thumb)
|
||||||
if let data: String = try await dataStore.load(fromPath: localPath) {
|
if let data: String = try await dataStore.load(fromPath: localPath) {
|
||||||
guard let dataDecoded = Data(base64Encoded: data) else { return nil }
|
guard let dataDecoded = Data(base64Encoded: data) else { return nil }
|
||||||
let image = UIImage(data: dataDecoded)
|
let image = UIImage(data: dataDecoded)
|
||||||
@@ -228,22 +242,6 @@ extension MainViewModel {
|
|||||||
}
|
}
|
||||||
return nil
|
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 {
|
for recipe in recipes {
|
||||||
Task {
|
Task {
|
||||||
let _ = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
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
|
import SwiftUI
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@StateObject var viewModel = MainViewModel()
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@StateObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
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 {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
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)
|
.font(.subheadline)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
.tint(.white)
|
.tint(.white)
|
||||||
@@ -152,10 +152,13 @@ struct LoginTab: View {
|
|||||||
// fetch login v2 response
|
// fetch login v2 response
|
||||||
Task {
|
Task {
|
||||||
guard let res = await fetchLoginV2Response() else { return }
|
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: {
|
} label: {
|
||||||
Text("Done")
|
Text("Validate")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding()
|
.padding()
|
||||||
@@ -188,24 +191,65 @@ struct LoginTab: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendLoginV2Request() async {
|
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,
|
method: .POST,
|
||||||
path: "https://\(userSettings.serverAddress)/index.php/login/v2"
|
path: .LOGINV2REQ,
|
||||||
|
headerFields: headerFields
|
||||||
)
|
)
|
||||||
let (loginReq, _): (LoginV2Request?, Error?) = await NetworkHandler.sendDataRequest(request)
|
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
|
self.loginRequest = loginReq
|
||||||
|
} catch {
|
||||||
|
print("Could not establish communication with the server.")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchLoginV2Response() async -> LoginV2Response? {
|
func fetchLoginV2Response() async -> LoginV2Response? {
|
||||||
guard let loginRequest = loginRequest else { return nil }
|
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,
|
method: .POST,
|
||||||
path: loginRequest.poll.endpoint,
|
path: .NONE,
|
||||||
body: "token=\(loginRequest.poll.token)"
|
headerFields: headerFields,
|
||||||
|
body: "token=\(loginRequest.poll.token)".data(using: .utf8),
|
||||||
|
authenticate: false
|
||||||
)
|
)
|
||||||
let (loginRes, _): (LoginV2Response?, Error?) = await NetworkHandler.sendDataRequest(request)
|
|
||||||
|
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
|
return loginRes
|
||||||
}
|
}
|
||||||
|
print("Could not decode.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoginLabel: View {
|
struct LoginLabel: View {
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ struct RecipeCardView: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.task {
|
.task {
|
||||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false)
|
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true)
|
||||||
}
|
}
|
||||||
.refreshable {
|
.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 : "")
|
.navigationTitle(showTitle ? recipe.name : "")
|
||||||
.task {
|
.task {
|
||||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id)
|
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 {
|
.refreshable {
|
||||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
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