New Recipe Edit View

This commit is contained in:
VincentMeilinger
2024-02-18 12:25:18 +01:00
parent e88f1d329d
commit d3e0366ce6
23 changed files with 1261 additions and 1086 deletions

View File

@@ -14,9 +14,7 @@ struct ApiRequest {
let authString: String?
let headerFields: [HeaderField]
let body: Data?
/// The path to the Cookbook application on the nextcloud server.
init(
path: String,
method: RequestMethod,

View File

@@ -1,70 +0,0 @@
//
// CustomError.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 13.09.23.
//
import Foundation
import SwiftUI
public enum NotImplementedError: Error, CustomStringConvertible {
case notImplemented
public var description: String {
return "Function not implemented."
}
}
public enum NetworkError: String, Error {
case missingUrl = "Missing URL."
case parametersNil = "Parameters are nil."
case encodingFailed = "Parameter encoding failed."
case decodingFailed = "Data decoding 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."
}
public enum ServerError: Error {
case unknownError, missingRequestBody, duplicateRecipe, noImage, missingRecipeName, recipeNotFound, deleteFailed, requestUnsuccessful
static func decodeFromURLResponse(response: HTTPURLResponse?) -> ServerError? {
guard let response = response else {
return ServerError.unknownError
}
print("Status code: ", response.statusCode)
switch response.statusCode {
case 200...299: return nil
case 400: return .missingRequestBody
case 404: return .recipeNotFound
case 409: return .duplicateRecipe
case 406: return .noImage
case 422: return .missingRecipeName
case 500: return .requestUnsuccessful
case 502: return .deleteFailed
default: return ServerError.unknownError
}
}
var localizedDescription: LocalizedStringKey {
switch self {
case .noImage: return "The recipe has no image whose MIME type matches the Accept header"
case .missingRecipeName: return "There was no name in the request given for the recipe. Cannot save the recipe."
default: return "An unknown server error occured."
}
}
var localizedTitle: LocalizedStringKey {
switch self {
case .missingRequestBody: return "Missing Request Body"
case .duplicateRecipe: return "Duplicate Recipe"
case .noImage: return "Image MIME Error"
case .missingRecipeName: return "Missing Name"
default: return "Error"
}
}
}

View File

@@ -0,0 +1,23 @@
//
// CustomError.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 decodingFailed = "Data decoding 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."
}

View File

@@ -1,80 +0,0 @@
//
// 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)\nPath: \(request.url?.absoluteString)\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)
print("Data: ", data?.description, data, String(data: data ?? Data(), encoding: .utf8))
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)
}
}
}

View File

@@ -1,169 +0,0 @@
//
// NetworkRequests.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 13.09.23.
//
import Foundation
enum RequestMethod: String {
case GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
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)
}
}
enum RequestPath {
case CATEGORIES,
RECIPE_LIST(categoryName: String),
RECIPE_DETAIL(recipeId: Int),
NEW_RECIPE,
IMAGE(recipeId: Int, thumb: Bool),
CONFIG,
KEYWORDS
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 .NEW_RECIPE: return "recipes"
case .CONFIG: return "config"
case .KEYWORDS: return "keywords"
case .LOGINV2REQ: return "/index.php/login/v2"
case .CUSTOM(path: let path): return path
case .NONE: return ""
}
}
}
struct RequestWrapper {
private let _method: RequestMethod
private let _path: RequestPath
private let _headerFields: [HeaderField]
private let _body: Data?
private let _authenticate: Bool = true
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 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
}
}
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
}
}

View File

@@ -0,0 +1,50 @@
//
// NetworkRequests.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 13.09.23.
//
import Foundation
enum RequestMethod: String {
case GET = "GET",
POST = "POST",
PUT = "PUT",
DELETE = "DELETE"
}
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 RecipeImportRequest: Codable {
let url: String
}