Basic Edit View and components
This commit is contained in:
@@ -29,7 +29,10 @@
|
|||||||
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 */; };
|
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; };
|
||||||
A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIInterface.swift */; };
|
A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIController.swift */; };
|
||||||
|
A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; };
|
||||||
|
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; };
|
||||||
|
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -76,7 +79,10 @@
|
|||||||
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>"; };
|
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>"; };
|
A703226C2ABAF90D00D7C4ED /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = "<group>"; };
|
||||||
|
A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = "<group>"; };
|
||||||
|
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = "<group>"; };
|
||||||
|
A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
A70171B22AB211F000064C43 /* Network */ = {
|
A70171B22AB211F000064C43 /* Network */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A703226C2ABAF90D00D7C4ED /* APIInterface.swift */,
|
A703226C2ABAF90D00D7C4ED /* APIController.swift */,
|
||||||
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
|
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
|
||||||
A70171AE2AB2116B00064C43 /* NetworkHandler.swift */,
|
A70171AE2AB2116B00064C43 /* NetworkHandler.swift */,
|
||||||
A70171B02AB211DF00064C43 /* CustomError.swift */,
|
A70171B02AB211DF00064C43 /* CustomError.swift */,
|
||||||
@@ -192,6 +198,7 @@
|
|||||||
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */,
|
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */,
|
||||||
A70171C12AB498C600064C43 /* RecipeCardView.swift */,
|
A70171C12AB498C600064C43 /* RecipeCardView.swift */,
|
||||||
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */,
|
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */,
|
||||||
|
A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */,
|
||||||
A70171C82AB4CBB400064C43 /* OnboardingView.swift */,
|
A70171C82AB4CBB400064C43 /* OnboardingView.swift */,
|
||||||
A70171CC2AB501B100064C43 /* SettingsView.swift */,
|
A70171CC2AB501B100064C43 /* SettingsView.swift */,
|
||||||
);
|
);
|
||||||
@@ -212,7 +219,9 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */,
|
A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */,
|
||||||
|
A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */,
|
||||||
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */,
|
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */,
|
||||||
|
A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -347,6 +356,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */,
|
||||||
|
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */,
|
||||||
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
|
||||||
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
|
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
|
||||||
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
|
||||||
@@ -361,10 +372,11 @@
|
|||||||
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 */,
|
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */,
|
||||||
A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */,
|
A703226D2ABAF90D00D7C4ED /* APIController.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 */,
|
||||||
|
A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -516,6 +528,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
|
||||||
@@ -539,7 +552,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
@@ -556,6 +569,7 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\"";
|
||||||
@@ -579,7 +593,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = auto;
|
SDKROOT = auto;
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"colors" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "logo-white.png",
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -24,22 +24,60 @@ struct Recipe: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct RecipeDetail: Codable {
|
struct RecipeDetail: Codable {
|
||||||
let name: String
|
var name: String
|
||||||
let keywords: String
|
var keywords: String
|
||||||
let dateCreated: String
|
var dateCreated: String
|
||||||
let dateModified: String
|
var dateModified: String
|
||||||
let imageUrl: String
|
var imageUrl: String
|
||||||
let id: String
|
var id: String
|
||||||
let prepTime: String?
|
var prepTime: String?
|
||||||
let cookTime: String?
|
var cookTime: String?
|
||||||
let totalTime: String?
|
var totalTime: String?
|
||||||
let description: String
|
var description: String
|
||||||
let url: String
|
var url: String
|
||||||
let recipeYield: Int
|
var recipeYield: Int
|
||||||
let recipeCategory: String
|
var recipeCategory: String
|
||||||
let tool: [String]
|
var tool: [String]
|
||||||
let recipeIngredient: [String]
|
var recipeIngredient: [String]
|
||||||
let recipeInstructions: [String]
|
var recipeInstructions: [String]
|
||||||
|
|
||||||
|
init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String]) {
|
||||||
|
self.name = name
|
||||||
|
self.keywords = keywords
|
||||||
|
self.dateCreated = dateCreated
|
||||||
|
self.dateModified = dateModified
|
||||||
|
self.imageUrl = imageUrl
|
||||||
|
self.id = id
|
||||||
|
self.prepTime = prepTime
|
||||||
|
self.cookTime = cookTime
|
||||||
|
self.totalTime = totalTime
|
||||||
|
self.description = description
|
||||||
|
self.url = url
|
||||||
|
self.recipeYield = recipeYield
|
||||||
|
self.recipeCategory = recipeCategory
|
||||||
|
self.tool = tool
|
||||||
|
self.recipeIngredient = recipeIngredient
|
||||||
|
self.recipeInstructions = recipeInstructions
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
name = ""
|
||||||
|
keywords = ""
|
||||||
|
dateCreated = ""
|
||||||
|
dateModified = ""
|
||||||
|
imageUrl = ""
|
||||||
|
id = ""
|
||||||
|
prepTime = ""
|
||||||
|
cookTime = ""
|
||||||
|
totalTime = ""
|
||||||
|
description = ""
|
||||||
|
url = ""
|
||||||
|
recipeYield = 0
|
||||||
|
recipeCategory = ""
|
||||||
|
tool = []
|
||||||
|
recipeIngredient = []
|
||||||
|
recipeInstructions = []
|
||||||
|
}
|
||||||
|
|
||||||
static func error() -> RecipeDetail {
|
static func error() -> RecipeDetail {
|
||||||
return RecipeDetail(
|
return RecipeDetail(
|
||||||
@@ -69,6 +107,11 @@ struct RecipeImage {
|
|||||||
var full: UIImage?
|
var full: UIImage?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Login flow
|
||||||
|
|
||||||
struct LoginV2Request: Codable {
|
struct LoginV2Request: Codable {
|
||||||
let poll: LoginV2Poll
|
let poll: LoginV2Poll
|
||||||
let login: String
|
let login: String
|
||||||
@@ -84,3 +127,16 @@ struct LoginV2Response: Codable {
|
|||||||
let loginName: String
|
let loginName: String
|
||||||
let appPassword: String
|
let appPassword: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LoginValidation: Codable {
|
||||||
|
let ocs: Ocs
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Ocs: Codable {
|
||||||
|
let meta: MetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MetaData: Codable {
|
||||||
|
let status: String
|
||||||
|
let statuscode: Int
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// ColorExtension.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 20.09.23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
public static var nextcloudBlue: Color {
|
||||||
|
return Color("ncblue")
|
||||||
|
}
|
||||||
|
public static var backgroundHighlight: Color {
|
||||||
|
return Color("backgroundHighlight")
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift
Normal file
21
Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// DateExtension.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 29.09.23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
static var zero: Date {
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.dateFormat = "HH:mm"
|
||||||
|
|
||||||
|
if let date = dateFormatter.date(from:"00:00") {
|
||||||
|
return date
|
||||||
|
} else {
|
||||||
|
return Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// APIInterface.swift
|
// APIController.swift
|
||||||
// Nextcloud Cookbook iOS Client
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 20.09.23.
|
// Created by Vincent Meilinger on 20.09.23.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class APIInterface {
|
class APIController {
|
||||||
var userSettings: UserSettings
|
var userSettings: UserSettings
|
||||||
|
|
||||||
var apiPath: String
|
var apiPath: String
|
||||||
@@ -15,7 +15,7 @@ class APIInterface {
|
|||||||
let apiVersion = "1"
|
let apiVersion = "1"
|
||||||
|
|
||||||
init(userSettings: UserSettings) {
|
init(userSettings: UserSettings) {
|
||||||
print("Initializing NetworkController.")
|
print("Initializing APIController.")
|
||||||
self.userSettings = userSettings
|
self.userSettings = userSettings
|
||||||
|
|
||||||
self.apiPath = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/"
|
self.apiPath = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/"
|
||||||
@@ -24,7 +24,11 @@ class APIInterface {
|
|||||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||||
self.authString = loginData.base64EncodedString()
|
self.authString = loginData.base64EncodedString()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension APIController {
|
||||||
func imageDataFromServer(recipeId: Int, thumb: Bool) async -> Data? {
|
func imageDataFromServer(recipeId: Int, thumb: Bool) async -> Data? {
|
||||||
do {
|
do {
|
||||||
let request = RequestWrapper.imageRequest(path: .IMAGE(recipeId: recipeId, thumb: thumb))
|
let request = RequestWrapper.imageRequest(path: .IMAGE(recipeId: recipeId, thumb: thumb))
|
||||||
@@ -43,9 +47,7 @@ class APIInterface {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension APIInterface {
|
|
||||||
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
||||||
do {
|
do {
|
||||||
let (data, error) = try await NetworkHandler.sendHTTPRequest(
|
let (data, error) = try await NetworkHandler.sendHTTPRequest(
|
||||||
@@ -49,7 +49,7 @@ struct NetworkHandler {
|
|||||||
request.httpBody = body
|
request.httpBody = body
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Request:\nMethod: \(request.httpMethod)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)")
|
print("Request:\nMethod: \(request.httpMethod)\nPath: \(request.url?.absoluteString)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)")
|
||||||
|
|
||||||
// Wait for and return data and (decoded) response
|
// Wait for and return data and (decoded) response
|
||||||
var data: Data? = nil
|
var data: Data? = nil
|
||||||
@@ -57,6 +57,7 @@ struct NetworkHandler {
|
|||||||
do {
|
do {
|
||||||
(data, response) = try await URLSession.shared.data(for: request)
|
(data, response) = try await URLSession.shared.data(for: request)
|
||||||
print("Response: ", response)
|
print("Response: ", response)
|
||||||
|
print("Data: ", data?.description, data, String(data: data ?? Data(), encoding: .utf8))
|
||||||
return (data, nil)
|
return (data, nil)
|
||||||
} catch {
|
} catch {
|
||||||
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.user-selected.read-only</key>
|
<key>com.apple.security.files.user-selected.read-only</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
|||||||
if userSettings.onboarding {
|
if userSettings.onboarding {
|
||||||
OnboardingView(userSettings: userSettings)
|
OnboardingView(userSettings: userSettings)
|
||||||
} else {
|
} else {
|
||||||
MainView(userSettings: userSettings, viewModel: mainViewModel)
|
MainView(viewModel: mainViewModel, userSettings: userSettings)
|
||||||
|
.onAppear {
|
||||||
|
mainViewModel.apiInterface = APIController(userSettings: userSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.transition(.slide)
|
}.transition(.slide)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor class MainViewModel: ObservableObject {
|
@MainActor class MainViewModel: ObservableObject {
|
||||||
@Published var categories: [Category] = []
|
@Published var categories: [Category] = []
|
||||||
@@ -15,7 +16,7 @@ import UIKit
|
|||||||
private var imageCache: [Int: RecipeImage] = [:]
|
private var imageCache: [Int: RecipeImage] = [:]
|
||||||
|
|
||||||
let dataStore: DataStore
|
let dataStore: DataStore
|
||||||
var apiInterface: APIInterface? = nil
|
var apiInterface: APIController? = nil
|
||||||
|
|
||||||
/// The path of an image in storage
|
/// The path of an image in storage
|
||||||
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct CategoryCardView: View {
|
|||||||
.ultraThickMaterial
|
.ultraThickMaterial
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
Text(category.name)
|
Text(category.name == "*" ? "Other" : category.name)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
)
|
)
|
||||||
.frame(maxHeight: 25)
|
.frame(maxHeight: 25)
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ struct RecipeBookView: View {
|
|||||||
if let recipes = viewModel.recipes[categoryName] {
|
if let recipes = viewModel.recipes[categoryName] {
|
||||||
ForEach(recipes, id: \.recipe_id) { recipe in
|
ForEach(recipes, id: \.recipe_id) { recipe in
|
||||||
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
|
NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) {
|
||||||
RecipeCardView(viewModel: viewModel, recipe: recipe, isDownloaded: viewModel.recipeDetailExists(recipeId: recipe.recipe_id))
|
RecipeCardView(viewModel: viewModel, recipe: recipe)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(categoryName)
|
.navigationTitle(categoryName == "*" ? "Other" : categoryName)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Menu {
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -10,33 +10,30 @@ import SwiftUI
|
|||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@ObservedObject var viewModel: MainViewModel
|
@ObservedObject var viewModel: MainViewModel
|
||||||
@ObservedObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
|
||||||
|
|
||||||
init(userSettings: UserSettings, viewModel: MainViewModel) {
|
@State var showEditView: Bool = false
|
||||||
self.userSettings = userSettings
|
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
|
||||||
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) {
|
||||||
LazyVGrid(columns: columns, spacing: 0) {
|
LazyVGrid(columns: columns, spacing: 0) {
|
||||||
ForEach(viewModel.categories, id: \.name) { category in
|
ForEach(viewModel.categories, id: \.name) { category in
|
||||||
NavigationLink(
|
if category.recipe_count != 0 {
|
||||||
destination: RecipeBookView(
|
NavigationLink(
|
||||||
categoryName: category.name,
|
destination: RecipeBookView(
|
||||||
viewModel: viewModel)
|
categoryName: category.name,
|
||||||
) {
|
viewModel: viewModel
|
||||||
CategoryCardView(category: category)
|
)
|
||||||
|
) {
|
||||||
|
CategoryCardView(category: category)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.navigationTitle("CookBook")
|
.navigationTitle("Cookbooks")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Menu {
|
Menu {
|
||||||
Button {
|
Button {
|
||||||
@@ -51,14 +48,27 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showEditView = true
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Create new recipe")
|
||||||
|
Image(systemName: "plus.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis.circle")
|
Image(systemName: "ellipsis.circle")
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink( destination: SettingsView(userSettings: userSettings, viewModel: viewModel)) {
|
NavigationLink( destination: SettingsView(userSettings: userSettings, viewModel: viewModel)) {
|
||||||
Image(systemName: "gearshape")
|
Image(systemName: "gearshape")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
.tint(.nextcloudBlue)
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadCategoryList()
|
await viewModel.loadCategoryList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct OnboardingView: View {
|
|||||||
}
|
}
|
||||||
.tabViewStyle(.page)
|
.tabViewStyle(.page)
|
||||||
.background(
|
.background(
|
||||||
selectedTab == 1 ? Color("ncblue").ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea()
|
selectedTab == 1 ? Color.nextcloudBlue.ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea()
|
||||||
)
|
)
|
||||||
.animation(.easeInOut, value: selectedTab)
|
.animation(.easeInOut, value: selectedTab)
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,13 @@ struct WelcomeTab: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 120, height: 120)
|
.frame(width: 120, height: 120)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
Text("Tank you for downloading")
|
Text("Tank you for downloading the")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
Text("Nextcloud")
|
Text("Cookbook Client")
|
||||||
.font(.largeTitle)
|
|
||||||
.bold()
|
|
||||||
Text("Cookbook")
|
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.bold()
|
.bold()
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.\n\nCurrently, only app token login is supported. You can create an app token in the nextcloud security settings.")
|
Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.")
|
||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -54,12 +51,18 @@ struct WelcomeTab: View {
|
|||||||
struct LoginTab: View {
|
struct LoginTab: View {
|
||||||
@ObservedObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
|
|
||||||
|
// Login flow
|
||||||
enum LoginMethod {
|
enum LoginMethod {
|
||||||
case v2, token
|
case v2, token
|
||||||
}
|
}
|
||||||
@State var selectedLoginMethod: LoginMethod = .v2
|
@State var selectedLoginMethod: LoginMethod = .v2
|
||||||
@State var loginRequest: LoginV2Request? = nil
|
@State var loginRequest: LoginV2Request? = nil
|
||||||
|
|
||||||
|
// Login error alert
|
||||||
|
@State var showAlert: Bool = false
|
||||||
|
@State var alertMessage: String = "Error: Could not connect to server."
|
||||||
|
|
||||||
|
// TextField handling
|
||||||
enum Field {
|
enum Field {
|
||||||
case server
|
case server
|
||||||
case username
|
case username
|
||||||
@@ -70,15 +73,7 @@ struct LoginTab: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
Spacer()
|
||||||
Spacer()
|
|
||||||
Image("nc-logo-white")
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fit)
|
|
||||||
.frame(maxHeight: 150)
|
|
||||||
.padding()
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Picker("Login Method", selection: $selectedLoginMethod) {
|
Picker("Login Method", selection: $selectedLoginMethod) {
|
||||||
Text("Nextcloud Login").tag(LoginMethod.v2)
|
Text("Nextcloud Login").tag(LoginMethod.v2)
|
||||||
Text("App Token Login").tag(LoginMethod.token)
|
Text("App Token Login").tag(LoginMethod.token)
|
||||||
@@ -109,7 +104,11 @@ struct LoginTab: View {
|
|||||||
HStack{
|
HStack{
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
userSettings.onboarding = false
|
Task {
|
||||||
|
if await loginCheck(nextcloudLogin: false) {
|
||||||
|
userSettings.onboarding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text("Submit")
|
Text("Submit")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
@@ -138,20 +137,43 @@ struct LoginTab: View {
|
|||||||
await sendLoginV2Request()
|
await sendLoginV2Request()
|
||||||
if let loginRequest = loginRequest {
|
if let loginRequest = loginRequest {
|
||||||
await UIApplication.shared.open(URL(string: loginRequest.login)!)
|
await UIApplication.shared.open(URL(string: loginRequest.login)!)
|
||||||
|
} else {
|
||||||
|
alertMessage = "Unable to reach server. Please check your server address and internet connection."
|
||||||
|
showAlert = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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'.")
|
Text("Entering the server address will open a web browser. Please follow the login instructions provided there. If the browser does not open, click the link 'Open in browser'\nAfter a successfull login, return to this application and press 'Validate'.")
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
.tint(.white)
|
.foregroundStyle(.white)
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await sendLoginV2Request()
|
||||||
|
if let loginRequest = loginRequest {
|
||||||
|
await UIApplication.shared.open(URL(string: loginRequest.login)!)
|
||||||
|
} else {
|
||||||
|
alertMessage = "Unable to reach server. Please check your server address and internet connection."
|
||||||
|
showAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Open in browser")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
|
||||||
HStack{
|
HStack{
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
// fetch login v2 response
|
// fetch login v2 response
|
||||||
Task {
|
Task {
|
||||||
guard let res = await fetchLoginV2Response() else { return }
|
guard let res = await fetchLoginV2Response() else {
|
||||||
|
alertMessage = "Login failed. Please login via the browser and try again."
|
||||||
|
showAlert = true
|
||||||
|
return
|
||||||
|
}
|
||||||
print("Login successfull for user \(res.loginName)!")
|
print("Login successfull for user \(res.loginName)!")
|
||||||
userSettings.username = res.loginName
|
userSettings.username = res.loginName
|
||||||
userSettings.token = res.appPassword
|
userSettings.token = res.appPassword
|
||||||
@@ -187,6 +209,9 @@ struct LoginTab: View {
|
|||||||
}
|
}
|
||||||
.fontDesign(.rounded)
|
.fontDesign(.rounded)
|
||||||
.padding()
|
.padding()
|
||||||
|
.alert(alertMessage, isPresented: $showAlert) {
|
||||||
|
Button("Ok", role: .cancel) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +275,53 @@ struct LoginTab: View {
|
|||||||
print("Could not decode.")
|
print("Could not decode.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loginCheck(nextcloudLogin: Bool) async -> Bool {
|
||||||
|
if userSettings.serverAddress == "" {
|
||||||
|
alertMessage = "Please enter a server address!"
|
||||||
|
showAlert = true
|
||||||
|
return false
|
||||||
|
} else if !nextcloudLogin && (userSettings.username == "" || userSettings.token == "") {
|
||||||
|
alertMessage = "Please enter a user name and app token!"
|
||||||
|
showAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let headerFields = [
|
||||||
|
HeaderField.ocsRequest(value: true),
|
||||||
|
]
|
||||||
|
let request = RequestWrapper.customRequest(
|
||||||
|
method: .GET,
|
||||||
|
path: .CATEGORIES,
|
||||||
|
headerFields: headerFields,
|
||||||
|
authenticate: true
|
||||||
|
)
|
||||||
|
|
||||||
|
var (data, error): (Data?, Error?) = (nil, nil)
|
||||||
|
do {
|
||||||
|
let loginString = "\(userSettings.username):\(userSettings.token)"
|
||||||
|
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||||
|
let authString = loginData.base64EncodedString()
|
||||||
|
(data, error) = try await NetworkHandler.sendHTTPRequest(
|
||||||
|
request,
|
||||||
|
hostPath: "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v1/",
|
||||||
|
authString: authString
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
print("Error: ", error)
|
||||||
|
}
|
||||||
|
guard let data = data else {
|
||||||
|
alertMessage = "Login failed. Please check your inputs."
|
||||||
|
showAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if let testRequest: [Category] = JSONDecoder.safeDecode(data) {
|
||||||
|
print("validationResponse: \(testRequest)")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
alertMessage = "Login failed. Please check your inputs and internet connection."
|
||||||
|
showAlert = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoginLabel: View {
|
struct LoginLabel: View {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct RecipeCardView: View {
|
|||||||
@State var viewModel: MainViewModel
|
@State var viewModel: MainViewModel
|
||||||
@State var recipe: Recipe
|
@State var recipe: Recipe
|
||||||
@State var recipeThumb: UIImage?
|
@State var recipeThumb: UIImage?
|
||||||
@State var isDownloaded: Bool
|
@State var isDownloaded: Bool? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -25,18 +25,21 @@ struct RecipeCardView: View {
|
|||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack {
|
if let isDownloaded = isDownloaded {
|
||||||
Image(systemName: isDownloaded ? "checkmark.icloud" : "icloud.and.arrow.down")
|
VStack {
|
||||||
.foregroundColor(.secondary)
|
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
||||||
.padding()
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(.ultraThickMaterial)
|
.background(Color.backgroundHighlight)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.task {
|
.task {
|
||||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true)
|
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true)
|
||||||
|
self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id)
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true)
|
recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true)
|
||||||
|
|||||||
@@ -15,35 +15,41 @@ struct RecipeDetailView: View {
|
|||||||
@State var recipeDetail: RecipeDetail?
|
@State var recipeDetail: RecipeDetail?
|
||||||
@State var recipeImage: UIImage?
|
@State var recipeImage: UIImage?
|
||||||
@State var showTitle: Bool = false
|
@State var showTitle: Bool = false
|
||||||
|
@State var isDownloaded: Bool? = nil
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(showsIndicators: false) {
|
ScrollView(showsIndicators: false) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let recipeImage = recipeImage {
|
if let recipeImage = recipeImage {
|
||||||
Image(uiImage: recipeImage)
|
Image(uiImage: recipeImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.scaledToFill()
|
||||||
.frame(height: 300)
|
.frame(maxHeight: 300)
|
||||||
.clipped()
|
.clipped()
|
||||||
} else {
|
|
||||||
Color("ncblue")
|
|
||||||
.frame(height: 150)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let recipeDetail = recipeDetail {
|
if let recipeDetail = recipeDetail {
|
||||||
LazyVStack (alignment: .leading) {
|
LazyVStack (alignment: .leading) {
|
||||||
Divider()
|
Divider()
|
||||||
Text(recipeDetail.name)
|
HStack {
|
||||||
.font(.title)
|
Text(recipeDetail.name)
|
||||||
.bold()
|
.font(.title)
|
||||||
.padding()
|
.bold()
|
||||||
.onDisappear {
|
.padding()
|
||||||
showTitle = true
|
.onDisappear {
|
||||||
}
|
showTitle = true
|
||||||
.onAppear {
|
}
|
||||||
showTitle = false
|
.onAppear {
|
||||||
|
showTitle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if let isDownloaded = isDownloaded {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Divider()
|
Divider()
|
||||||
RecipeYieldSection(recipeDetail: recipeDetail)
|
|
||||||
RecipeDurationSection(recipeDetail: recipeDetail)
|
RecipeDurationSection(recipeDetail: recipeDetail)
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
if(!recipeDetail.recipeIngredient.isEmpty) {
|
if(!recipeDetail.recipeIngredient.isEmpty) {
|
||||||
@@ -59,13 +65,14 @@ struct RecipeDetailView: View {
|
|||||||
}.padding(.horizontal, 5)
|
}.padding(.horizontal, 5)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}.animation(.easeInOut, value: recipeImage)
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.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, thumb: false)
|
recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false)
|
||||||
|
self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id)
|
||||||
}
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true)
|
||||||
@@ -75,31 +82,18 @@ struct RecipeDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RecipeYieldSection: View {
|
|
||||||
@State var recipeDetail: RecipeDetail
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
Text("Servings: \(recipeDetail.recipeYield)")
|
|
||||||
Spacer()
|
|
||||||
}.padding()
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RecipeDurationSection: View {
|
struct RecipeDurationSection: View {
|
||||||
@State var recipeDetail: RecipeDetail
|
@State var recipeDetail: RecipeDetail
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack(alignment: .center) {
|
||||||
if let prepTime = recipeDetail.prepTime {
|
if let prepTime = recipeDetail.prepTime {
|
||||||
VStack {
|
VStack {
|
||||||
SecondaryLabel(text: "Prep time")
|
SecondaryLabel(text: "Prep time")
|
||||||
Text(formatDate(duration: prepTime))
|
Text(formatDate(duration: prepTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color("accent"))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let cookTime = recipeDetail.cookTime {
|
if let cookTime = recipeDetail.cookTime {
|
||||||
@@ -108,9 +102,6 @@ struct RecipeDurationSection: View {
|
|||||||
Text(formatDate(duration: cookTime))
|
Text(formatDate(duration: cookTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color("accent"))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let totalTime = recipeDetail.totalTime {
|
if let totalTime = recipeDetail.totalTime {
|
||||||
@@ -119,9 +110,6 @@ struct RecipeDurationSection: View {
|
|||||||
Text(formatDate(duration: totalTime))
|
Text(formatDate(duration: totalTime))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.padding()
|
}.padding()
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color("accent"))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +122,13 @@ struct RecipeIngredientSection: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
SecondaryLabel(text: "Ingredients")
|
if recipeDetail.recipeYield == 0 {
|
||||||
|
SecondaryLabel(text: "Ingredients")
|
||||||
|
} else if recipeDetail.recipeYield == 1 {
|
||||||
|
SecondaryLabel(text: "Ingredients per serving")
|
||||||
|
} else {
|
||||||
|
SecondaryLabel(text: "Ingredients for \(recipeDetail.recipeYield) servings")
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
ForEach(recipeDetail.recipeIngredient, id: \.self) { ingredient in
|
ForEach(recipeDetail.recipeIngredient, id: \.self) { ingredient in
|
||||||
|
|||||||
88
Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift
Normal file
88
Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// RecipeEditView.swift
|
||||||
|
// Nextcloud Cookbook iOS Client
|
||||||
|
//
|
||||||
|
// Created by Vincent Meilinger on 29.09.23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
struct RecipeEditView: View {
|
||||||
|
@State var recipe: RecipeDetail
|
||||||
|
|
||||||
|
@State var times = [Date.zero, Date.zero, Date.zero]
|
||||||
|
|
||||||
|
init(recipe: RecipeDetail? = nil) {
|
||||||
|
|
||||||
|
self.recipe = recipe ?? RecipeDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
TextField("Title", text: $recipe.name)
|
||||||
|
Section() {
|
||||||
|
DatePicker("Prep time:", selection: $times[0], displayedComponents: .hourAndMinute)
|
||||||
|
DatePicker("Cook time:", selection: $times[1], displayedComponents: .hourAndMinute)
|
||||||
|
DatePicker("Total time:", selection: $times[2], displayedComponents: .hourAndMinute)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section() {
|
||||||
|
|
||||||
|
List {
|
||||||
|
ForEach(recipe.recipeInstructions.indices, id: \.self) { ix in
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
Text("\(ix+1).")
|
||||||
|
TextEditor(text: $recipe.recipeInstructions[ix])
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onMove { indexSet, offset in
|
||||||
|
recipe.recipeInstructions.move(fromOffsets: indexSet, toOffset: offset)
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
recipe.recipeInstructions.remove(atOffsets: indexSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Add instruction")
|
||||||
|
Button() {
|
||||||
|
recipe.recipeInstructions.append("")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Ingredients")
|
||||||
|
Spacer()
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct TimePicker: View {
|
||||||
|
@Binding var hours: Int
|
||||||
|
@Binding var minutes: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Picker("", selection: $hours){
|
||||||
|
ForEach(0..<99, id: \.self) { i in
|
||||||
|
Text("\(i) hours").tag(i)
|
||||||
|
}
|
||||||
|
}.pickerStyle(.wheel)
|
||||||
|
Picker("", selection: $minutes){
|
||||||
|
ForEach(0..<60, id: \.self) { i in
|
||||||
|
Text("\(i) min").tag(i)
|
||||||
|
}
|
||||||
|
}.pickerStyle(.wheel)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,72 +8,98 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
fileprivate enum SettingsAlert {
|
||||||
|
case LOG_OUT,
|
||||||
|
DELETE_CACHE,
|
||||||
|
NONE
|
||||||
|
|
||||||
|
func getTitle() -> String {
|
||||||
|
switch self {
|
||||||
|
case .LOG_OUT: return "Log out"
|
||||||
|
case .DELETE_CACHE: return "Delete local data"
|
||||||
|
default: return "Please confirm your action."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMessage() -> String {
|
||||||
|
switch self {
|
||||||
|
case .LOG_OUT: return "Are you sure that you want to log out of your account?"
|
||||||
|
case .DELETE_CACHE: return "Are you sure that you want to delete the downloaded recipes? This action will not affect any recipes stored on your server."
|
||||||
|
default: return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@ObservedObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
@ObservedObject var viewModel: MainViewModel
|
@ObservedObject var viewModel: MainViewModel
|
||||||
|
|
||||||
|
@State fileprivate var alertType: SettingsAlert = .NONE
|
||||||
|
@State var showAlert: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
Form {
|
||||||
SettingsSection(title: "Language", description: "Language settings coming soon.")
|
Section() {
|
||||||
SettingsSection(title: "Accent Color", description: "The accent color setting will be released in a future update.")
|
Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!)
|
||||||
SettingsSection(title: "Log out", description: "Log out of your Nextcloud account in this app. Your recipes will be removed from local storage.")
|
} header: {
|
||||||
{
|
Text("About")
|
||||||
|
} footer: {
|
||||||
|
Text("If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section() {
|
||||||
|
Link("Get support", destination: URL(string: "https://vincentmeilinger.github.io/Nextcloud-Cookbook-Client-Support/")!)
|
||||||
|
} header: {
|
||||||
|
Text("Support")
|
||||||
|
} footer: {
|
||||||
|
Text("If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information.")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section() {
|
||||||
Button("Log out") {
|
Button("Log out") {
|
||||||
print("Log out.")
|
print("Log out.")
|
||||||
userSettings.serverAddress = ""
|
alertType = .LOG_OUT
|
||||||
userSettings.username = ""
|
showAlert = true
|
||||||
userSettings.token = ""
|
|
||||||
userSettings.onboarding = true
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.tint(.red)
|
||||||
.accentColor(.red)
|
|
||||||
.padding()
|
Button("Delete local data.") {
|
||||||
}
|
|
||||||
|
|
||||||
SettingsSection(title: "Clear local data", description: "Your recipes will be removed from local storage.")
|
|
||||||
{
|
|
||||||
Button("Clear Cache") {
|
|
||||||
print("Clear cache.")
|
print("Clear cache.")
|
||||||
viewModel.deleteAllData()
|
alertType = .DELETE_CACHE
|
||||||
|
showAlert = true
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.tint(.red)
|
||||||
.accentColor(.red)
|
|
||||||
.padding()
|
} header: {
|
||||||
|
Text("Danger Zone")
|
||||||
}
|
}
|
||||||
|
|
||||||
}.navigationTitle("Settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
struct SettingsSection<Content: View>: View {
|
|
||||||
let title: String
|
|
||||||
let description: String
|
|
||||||
@ViewBuilder let content: () -> Content
|
|
||||||
|
|
||||||
init(title: String, description: String, content: @escaping () -> Content) {
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.content = content
|
|
||||||
}
|
|
||||||
|
|
||||||
init(title: String, description: String) where Content == EmptyView {
|
|
||||||
self.title = title
|
|
||||||
self.description = description
|
|
||||||
self.content = { EmptyView() }
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(title)
|
|
||||||
.font(.headline)
|
|
||||||
Text(description)
|
|
||||||
.font(.caption)
|
|
||||||
}.padding()
|
|
||||||
Spacer()
|
|
||||||
content()
|
|
||||||
}
|
}
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
.alert(alertType.getTitle(), isPresented: $showAlert) {
|
||||||
|
Button("Cancel", role: .cancel) { }
|
||||||
|
if alertType == .LOG_OUT {
|
||||||
|
Button("Log out", role: .destructive) { logOut() }
|
||||||
|
} else if alertType == .DELETE_CACHE {
|
||||||
|
Button("Delete", role: .destructive) { deleteCache() }
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(alertType.getMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func logOut() {
|
||||||
|
userSettings.serverAddress = ""
|
||||||
|
userSettings.username = ""
|
||||||
|
userSettings.token = ""
|
||||||
|
viewModel.deleteAllData()
|
||||||
|
userSettings.onboarding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteCache() {
|
||||||
|
//viewModel.deleteAllData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user