Added category 'All' recipes

This commit is contained in:
Vicnet
2023-10-22 20:28:51 +02:00
parent 05c30a2cff
commit 8f32946e27
17 changed files with 574 additions and 243 deletions

View File

@@ -25,13 +25,15 @@
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; }; A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; };
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; }; A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; };
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 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.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 /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIController.swift */; }; A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIController.swift */; };
A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; }; A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; };
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; }; A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; };
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; }; A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; };
A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; };
A76B8A712AE002AE00096CEC /* AlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* AlertHandler.swift */; };
A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; }; A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; };
A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; }; A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; };
A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; }; A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; };
@@ -77,13 +79,15 @@
A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; }; A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; }; A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
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 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.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 /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.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>"; }; 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>"; }; 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>"; }; A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = "<group>"; };
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = "<group>"; };
A76B8A702AE002AE00096CEC /* AlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertHandler.swift; sourceTree = "<group>"; };
A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; }; A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; }; A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = "<group>"; };
A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = "<group>"; }; A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = "<group>"; };
@@ -138,13 +142,15 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */, A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */,
A76B8A702AE002AE00096CEC /* AlertHandler.swift */,
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */,
A70171C72AB4C4A100064C43 /* Data */, A70171C72AB4C4A100064C43 /* Data */,
A70171BA2AB4980100064C43 /* Views */, A70171BA2AB4980100064C43 /* Views */,
A70171B72AB2445700064C43 /* ViewModels */, A70171B72AB2445700064C43 /* ViewModels */,
A70171B22AB211F000064C43 /* Network */, A70171B22AB211F000064C43 /* Network */,
A703226B2ABAF60D00D7C4ED /* Extensions */, A703226B2ABAF60D00D7C4ED /* Extensions */,
A70171852AA8E71F00064C43 /* Assets.xcassets */,
A7AEAE632AD5521400135378 /* Localizable.xcstrings */, A7AEAE632AD5521400135378 /* Localizable.xcstrings */,
A70171852AA8E71F00064C43 /* Assets.xcassets */,
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */, A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
A70171882AA8E71F00064C43 /* Preview Content */, A70171882AA8E71F00064C43 /* Preview Content */,
); );
@@ -216,7 +222,7 @@
children = ( children = (
A70171C32AB4A31200064C43 /* DataStore.swift */, A70171C32AB4A31200064C43 /* DataStore.swift */,
A70171C52AB4C43A00064C43 /* DataModels.swift */, A70171C52AB4C43A00064C43 /* DataModels.swift */,
A70171CA2AB4CD1700064C43 /* UserDefaults.swift */, A70171CA2AB4CD1700064C43 /* UserSettings.swift */,
); );
path = Data; path = Data;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -368,6 +374,7 @@
A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */, A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */,
A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */, A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */,
A70171B12AB211DF00064C43 /* CustomError.swift in Sources */, A70171B12AB211DF00064C43 /* CustomError.swift in Sources */,
A76B8A712AE002AE00096CEC /* AlertHandler.swift in Sources */,
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A70171C42AB4A31200064C43 /* DataStore.swift in Sources */,
A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */, A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */,
A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */, A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */,
@@ -380,11 +387,12 @@
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */, A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */,
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 /* UserSettings.swift in Sources */,
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */, A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */,
A703226D2ABAF90D00D7C4ED /* APIController.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 */,
A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */,
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */, A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */,
A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */, A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */,
); );
@@ -562,7 +570,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.2; MARKETING_VERSION = 1.3;
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;
@@ -603,7 +611,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.2; MARKETING_VERSION = 1.3;
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;

View File

@@ -0,0 +1,99 @@
//
// AlertHandler.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 18.10.23.
//
import Foundation
import SwiftUI
class AlertHandler: ObservableObject {
@Published var presentAlert: Bool = false
var alert: AlertType = .GENERIC
var alertAction: () -> () = {}
func present(alert: AlertType, onConfirm: @escaping () -> () = {}) {
self.alert = alert
self.alertAction = onConfirm
self.presentAlert = true
}
func dismiss() {
self.alertAction = {}
self.alert = .GENERIC
}
}
enum AlertButton: LocalizedStringKey, Identifiable {
var id: Self {
return self
}
case OK = "Ok", DELETE = "Delete", CANCEL = "Cancel"
}
enum AlertType: Error {
case NO_TITLE,
DUPLICATE,
UPLOAD_ERROR,
CONFIRM_DELETE,
LOGIN_FAILED,
GENERIC,
CUSTOM(title: LocalizedStringKey, description: LocalizedStringKey)
var localizedDescription: LocalizedStringKey {
switch self {
case .NO_TITLE:
return "Please enter a recipe name."
case .DUPLICATE:
return "A recipe with that name already exists."
case .UPLOAD_ERROR:
return "Unable to upload your recipe. Please check your internet connection."
case .CONFIRM_DELETE:
return "This action is not reversible!"
case .LOGIN_FAILED:
return "Please check your credentials and internet connection."
case .CUSTOM(title: _, description: let description):
return description
default:
return "An unknown error occured."
}
}
var localizedTitle: LocalizedStringKey {
switch self {
case .NO_TITLE:
return "Missing recipe name."
case .DUPLICATE:
return "Duplicate recipe."
case .UPLOAD_ERROR:
return "Network error."
case .CONFIRM_DELETE:
return "Delete recipe?"
case .LOGIN_FAILED:
return "Login failed."
case .CUSTOM(title: let title, description: _):
return title
default:
return "Error."
}
}
var alertButtons: [AlertButton] {
switch self {
case .CONFIRM_DELETE:
return [.CANCEL, .DELETE]
default:
return [.OK]
}
}
}

View File

@@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "cookbook-category.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -1,21 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "cookbook-recipe.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,5 +1,5 @@
// //
// UserDefaults.swift // UserSettings.swift
// Nextcloud Cookbook iOS Client // Nextcloud Cookbook iOS Client
// //
// Created by Vincent Meilinger on 15.09.23. // Created by Vincent Meilinger on 15.09.23.
@@ -46,12 +46,19 @@ class UserSettings: ObservableObject {
} }
} }
@Published var downloadRecipes: Bool {
didSet {
UserDefaults.standard.set(downloadRecipes, forKey: "downloadRecipes")
}
}
init() { init() {
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? "" self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
self.serverAddress = UserDefaults.standard.object(forKey: "serverAddress") as? String ?? "" self.serverAddress = UserDefaults.standard.object(forKey: "serverAddress") as? String ?? ""
self.onboarding = UserDefaults.standard.object(forKey: "onboarding") as? Bool ?? true self.onboarding = UserDefaults.standard.object(forKey: "onboarding") as? Bool ?? true
self.defaultCategory = UserDefaults.standard.object(forKey: "defaultCategory") as? String ?? "" self.defaultCategory = UserDefaults.standard.object(forKey: "defaultCategory") as? String ?? ""
self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.EN.rawValue self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.DEVICE.rawValue
self.downloadRecipes = UserDefaults.standard.object(forKey: "downloadRecipes") as? Bool ?? false
} }
} }

View File

@@ -15,4 +15,7 @@ extension Color {
public static var backgroundHighlight: Color { public static var backgroundHighlight: Color {
return Color("backgroundHighlight") return Color("backgroundHighlight")
} }
public static var background: Color {
return Color(UIColor.systemBackground)
}
} }

View File

@@ -177,6 +177,22 @@
} }
} }
}, },
"All" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alle"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Todas"
}
}
}
},
"An unknown error occured." : { "An unknown error occured." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -257,18 +273,18 @@
} }
} }
}, },
"Cook time" : { "Connected to server." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Kochen" "value" : "Verbindung mit dem Server hergestellt."
} }
}, },
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Duración de cocción" "value" : "Conexión al servidor establecida."
} }
} }
} }
@@ -305,6 +321,22 @@
} }
} }
}, },
"Cooking" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kochen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cocción"
}
}
}
},
"Cooking duration:" : { "Cooking duration:" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -545,6 +577,22 @@
} }
} }
}, },
"If 'Same as Device' is selected and your device language is not supported yet, this option will default to english." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Wenn \"Systemsprache\" ausgewählt ist und Ihre Systemsprache noch nicht unterstützt wird, wird standardmäßig Englisch verwendet."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Si se selecciona 'Idioma del sistema' y el idioma de su dispositivo no es compatible aún, esta opción se establecerá por defecto en inglés."
}
}
}
},
"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." : { "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." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -689,6 +737,22 @@
} }
} }
}, },
"Login failed." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Login fehlgeschlagen."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Inicio de sesión fallido"
}
}
}
},
"Login Method" : { "Login Method" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -833,6 +897,22 @@
} }
} }
}, },
"Please check your credentials and internet connection." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bitte überprüfen Sie Ihre Anmeldedaten oder Ihre Internetverbindung."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Por favor, comprueba tus credenciales de inicio de sesión y la conexión a Internet."
}
}
}
},
"Please enter a recipe name." : { "Please enter a recipe name." : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -849,7 +929,7 @@
} }
} }
}, },
"Prep time" : { "Preparation" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
@@ -860,7 +940,7 @@
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Duración de preparación" "value" : "Preparación"
} }
} }
} }
@@ -881,6 +961,38 @@
} }
} }
}, },
"Same as Device" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Systemsprache"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Idioma del sistema"
}
}
}
},
"Search recipe" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Rezept suchen"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Buscar receta"
}
}
}
},
"Search recipes" : { "Search recipes" : {
"localizations" : { "localizations" : {
"de" : { "de" : {
@@ -1121,6 +1233,22 @@
} }
} }
}, },
"Unable to connect to server." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verbindung mit dem Server fehlgeschlagen."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "No se puede conectar al servidor."
}
}
}
},
"Unable to upload your recipe. Please check your internet connection." : { "Unable to upload your recipe. Please check your internet connection." : {
"localizations" : { "localizations" : {
"de" : { "de" : {

View File

@@ -20,6 +20,7 @@ enum RequestPath {
RECIPE_DETAIL(recipeId: Int), RECIPE_DETAIL(recipeId: Int),
NEW_RECIPE, NEW_RECIPE,
IMAGE(recipeId: Int, thumb: Bool), IMAGE(recipeId: Int, thumb: Bool),
CONFIG,
KEYWORDS KEYWORDS
case LOGINV2REQ, case LOGINV2REQ,
@@ -33,6 +34,7 @@ enum RequestPath {
case .RECIPE_DETAIL(recipeId: let recipeId): return "recipes/\(recipeId)" 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 .IMAGE(recipeId: let recipeId, thumb: let thumb): return "recipes/\(recipeId)/image?size=\(thumb ? "thumb" : "full")"
case .NEW_RECIPE: return "recipes" case .NEW_RECIPE: return "recipes"
case .CONFIG: return "config"
case .KEYWORDS: return "keywords" case .KEYWORDS: return "keywords"
case .LOGINV2REQ: return "/index.php/login/v2" case .LOGINV2REQ: return "/index.php/login/v2"

View File

@@ -11,6 +11,7 @@ import SwiftUI
struct Nextcloud_Cookbook_iOS_ClientApp: App { struct Nextcloud_Cookbook_iOS_ClientApp: App {
@StateObject var userSettings = UserSettings() @StateObject var userSettings = UserSettings()
@StateObject var mainViewModel = MainViewModel() @StateObject var mainViewModel = MainViewModel()
@StateObject var alertHandler = AlertHandler()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
@@ -25,8 +26,12 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
} }
} }
.transition(.slide) .transition(.slide)
.environment(\.locale, .init(identifier: userSettings.language)) .environment(
} \.locale,
.init(identifier: userSettings.language ==
SupportedLanguage.DEVICE.rawValue ? (Locale.current.language.languageCode?.identifier ?? "en") : userSettings.language)
)
.environmentObject(alertHandler)
}
} }
} }

View File

@@ -0,0 +1,31 @@
//
// SupportedLanguage.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 18.10.23.
//
import Foundation
enum SupportedLanguage: String, Codable {
case DEVICE = "device",
EN = "en",
DE = "de",
ES = "es"
func descriptor() -> String {
switch self {
case .DEVICE:
return String(localized: "Same as Device")
case .EN:
return "English"
case .DE:
return "Deutsch"
case .ES:
return "Español"
}
}
static let allValues = [DEVICE, EN, DE, ES]
}

View File

@@ -64,6 +64,19 @@ import SwiftUI
} }
func getAllRecipes() async -> [Recipe] {
var allRecipes: [Recipe] = []
for category in categories {
await loadRecipeList(categoryName: category.name)
if let recipeArray = recipes[category.name] {
allRecipes.append(contentsOf: recipeArray)
}
}
return allRecipes.sorted(by: {
$0.name < $1.name
})
}
/// Try to load the recipe details from cache. If not found, try to load from store or the server. /// Try to load the recipe details from cache. If not found, try to load from store or the server.
/// - Parameters /// - Parameters
/// - recipeId: The id of the recipe. /// - recipeId: The id of the recipe.
@@ -204,6 +217,22 @@ import SwiftUI
}) })
recipeDetails.removeValue(forKey: id) recipeDetails.removeValue(forKey: id)
} }
func checkServerConnection() async -> Bool {
guard let apiController = apiController else { return false }
let req = RequestWrapper.customRequest(
method: .GET,
path: .CONFIG,
headerFields: [
.ocsRequest(value: true),
.accept(value: .JSON)
]
)
if let error = await apiController.sendRequest(req) {
return false
}
return true
}
} }

View File

@@ -14,13 +14,31 @@ struct MainView: View {
@State private var selectedCategory: Category? = nil @State private var selectedCategory: Category? = nil
@State private var showEditView: Bool = false @State private var showEditView: Bool = false
@State private var showSearchView: Bool = false
@State private var showSettingsView: Bool = false @State private var showSettingsView: Bool = false
@State private var serverConnection: Bool = false
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
var body: some View { var body: some View {
NavigationSplitView { NavigationSplitView {
List(viewModel.categories, selection: $selectedCategory) { category in List(selection: $selectedCategory) {
// All recipes
NavigationLink {
RecipeSearchView(viewModel: viewModel)
} label: {
HStack(alignment: .center) {
Image(systemName: "book.closed.fill")
Text("All")
.font(.system(size: 20, weight: .light, design: .serif))
.italic()
}
.padding(7)
}
// Categories
ForEach(viewModel.categories) { category in
if category.recipe_count != 0 { if category.recipe_count != 0 {
NavigationLink(value: category) { NavigationLink(value: category) {
HStack(alignment: .center) { HStack(alignment: .center) {
@@ -32,11 +50,74 @@ struct MainView: View {
} }
} }
} }
}
.navigationTitle("Cookbooks") .navigationTitle("Cookbooks")
.navigationDestination(isPresented: $showSettingsView) { .navigationDestination(isPresented: $showSettingsView) {
SettingsView(userSettings: userSettings, viewModel: viewModel) SettingsView(userSettings: userSettings, viewModel: viewModel)
} }
.navigationDestination(isPresented: $showSearchView) {
RecipeSearchView(viewModel: viewModel)
}
.toolbar { .toolbar {
MainViewToolBar(
viewModel: viewModel,
showEditView: $showEditView,
showSettingsView: $showSettingsView,
serverConnection: $serverConnection
)
}
} detail: {
NavigationStack {
if let category = selectedCategory {
CategoryDetailView(
categoryName: category.name,
viewModel: viewModel,
showEditView: $showEditView
)
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
}
}
}
.tint(.nextcloudBlue)
.sheet(isPresented: $showEditView) {
RecipeEditView(viewModel: viewModel, isPresented: $showEditView)
}
.task {
self.serverConnection = await viewModel.checkServerConnection()
await viewModel.loadCategoryList()
// Open detail view for default category
if userSettings.defaultCategory != "" {
if let cat = viewModel.categories.first(where: { c in
if c.name == userSettings.defaultCategory {
return true
}
return false
}) {
self.selectedCategory = cat
}
}
}
.refreshable {
self.serverConnection = await viewModel.checkServerConnection()
await viewModel.loadCategoryList(needsUpdate: true)
}
}
}
struct MainViewToolBar: ToolbarContent {
@ObservedObject var viewModel: MainViewModel
@Binding var showEditView: Bool
@Binding var showSettingsView: Bool
@Binding var serverConnection: Bool
@State private var presentPopover: Bool = false
var body: some ToolbarContent {
// Top left menu toolbar item
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Menu { Menu {
Button { Button {
@@ -61,57 +142,75 @@ struct MainView: View {
Image(systemName: "ellipsis.circle") Image(systemName: "ellipsis.circle")
} }
} }
// Server connection indicator
ToolbarItem(placement: .topBarTrailing) {
Button {
print("Check server connection")
presentPopover = true
} label: {
if serverConnection {
Image(systemName: "checkmark.icloud")
} else {
Image(systemName: "xmark.icloud")
}
}.popover(isPresented: $presentPopover) {
Text(serverConnection ? LocalizedStringKey("Connected to server.") : LocalizedStringKey("Unable to connect to server."))
.bold()
.padding()
.presentationCompactAdaptation(.popover)
}
}
// Create new recipes
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button { Button {
print("Add new recipe") print("Add new recipe")
showEditView = true showEditView = true
} label: { } label: {
HStack {
Image(systemName: "plus.circle.fill") Image(systemName: "plus.circle.fill")
} }
} }
} }
} }
} detail: {
struct RecipeSearchView: View {
@ObservedObject var viewModel: MainViewModel
@State var searchText: String = ""
@State var allRecipes: [Recipe] = []
var body: some View {
NavigationStack { NavigationStack {
if let category = selectedCategory { VStack {
CategoryDetailView( ScrollView(showsIndicators: false) {
categoryName: category.name, LazyVStack {
viewModel: viewModel, ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
showEditView: $showEditView NavigationLink(value: recipe) {
) RecipeCardView(viewModel: viewModel, recipe: recipe)
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes }
.buttonStyle(.plain)
} }
} }
} }
.tint(.nextcloudBlue) .navigationDestination(for: Recipe.self) { recipe in
.sheet(isPresented: $showEditView) { RecipeDetailView(viewModel: viewModel, recipe: recipe)
RecipeEditView(viewModel: viewModel, isPresented: $showEditView) }
.searchable(text: $searchText, prompt: "Search recipes")
}
.navigationTitle("Search recipe")
} }
.task { .task {
await viewModel.loadCategoryList() allRecipes = await viewModel.getAllRecipes()
if userSettings.defaultCategory != "" {
if let cat = viewModel.categories.first(where: { c in
if c.name == userSettings.defaultCategory {
return true
} }
return false }
}) {
self.selectedCategory = cat func recipesFiltered() -> [Recipe] {
guard searchText != "" else { return allRecipes }
return allRecipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased())
} }
} }
} }
.refreshable {
await viewModel.loadCategoryList(needsUpdate: true)
}
}
}

View File

@@ -16,11 +16,24 @@ struct RecipeCardView: View {
var body: some View { var body: some View {
HStack { HStack {
Image(uiImage: recipeThumb ?? UIImage(named: "cookbook-recipe")!) if let recipeThumb = recipeThumb {
Image(uiImage: recipeThumb)
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80) .frame(width: 80, height: 80)
.clipped() .clipped()
} else {
ZStack {
Image(systemName: "square.text.square")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(Color.white)
.padding(10)
}
.background(Color("ncblue"))
.frame(width: 80, height: 80)
}
Text(recipe.name) Text(recipe.name)
.font(.headline) .font(.headline)

View File

@@ -12,6 +12,7 @@ import PhotosUI
struct RecipeEditView: View { struct RecipeEditView: View {
@EnvironmentObject var alertHandler: AlertHandler
@ObservedObject var viewModel: MainViewModel @ObservedObject var viewModel: MainViewModel
@State var recipe: RecipeDetail = RecipeDetail() @State var recipe: RecipeDetail = RecipeDetail()
@Binding var isPresented: Bool @Binding var isPresented: Bool
@@ -25,8 +26,6 @@ struct RecipeEditView: View {
@State private var keywords: [String] = [] @State private var keywords: [String] = []
@State private var keywordSuggestions: [String] = [] @State private var keywordSuggestions: [String] = []
@State private var alertMessage: ErrorMessages = .GENERIC
@State private var presentAlert: Bool = false
@State private var waitingForUpload: Bool = false @State private var waitingForUpload: Bool = false
var body: some View { var body: some View {
@@ -43,8 +42,9 @@ struct RecipeEditView: View {
Menu { Menu {
Button { Button {
print("Delete recipe.") print("Delete recipe.")
alertMessage = .CONFIRM_DELETE alertHandler.present(alert: .CONFIRM_DELETE) {
presentAlert = true deleteRecipe()
}
} label: { } label: {
Image(systemName: "trash") Image(systemName: "trash")
.foregroundStyle(.red) .foregroundStyle(.red)
@@ -152,18 +152,24 @@ struct RecipeEditView: View {
keywords.append(keyword) keywords.append(keyword)
} }
} }
.alert(alertMessage.localizedTitle, isPresented: $presentAlert) { .alert(alertHandler.alert.localizedTitle, isPresented: $alertHandler.presentAlert) {
switch alertMessage { ForEach(alertHandler.alert.alertButtons) { buttonType in
case .CONFIRM_DELETE: if buttonType == .OK {
Button("Cancel", role: .cancel) { } Button(AlertButton.OK.rawValue, role: .cancel) {
Button("Delete", role: .destructive) { alertHandler.alertAction()
deleteRecipe() alertHandler.dismiss()
}
} else if buttonType == .CANCEL {
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
} else if buttonType == .DELETE {
Button(AlertButton.DELETE.rawValue, role: .destructive) {
alertHandler.alertAction()
alertHandler.dismiss()
}
} }
default:
Button("Ok", role: .cancel) { }
} }
} message: { } message: {
Text(alertMessage.localizedDescription) Text(alertHandler.alert.localizedDescription)
} }
} }
@@ -179,9 +185,8 @@ struct RecipeEditView: View {
func recipeValid() -> Bool { func recipeValid() -> Bool {
// Check if the recipe has a name // Check if the recipe has a name
if recipe.name == "" { if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
self.alertMessage = .NO_TITLE alertHandler.present(alert: .NO_TITLE)
self.presentAlert = true
return false return false
} }
// Check if the recipe has a unique name // Check if the recipe has a unique name
@@ -194,8 +199,7 @@ struct RecipeEditView: View {
.replacingOccurrences(of: " ", with: "") .replacingOccurrences(of: " ", with: "")
.lowercased() .lowercased()
{ {
self.alertMessage = .DUPLICATE alertHandler.present(alert: .DUPLICATE)
self.presentAlert = true
return false return false
} }
} }
@@ -266,11 +270,7 @@ struct RecipeEditView: View {
guard let data = data else { return } guard let data = data else { return }
do { do {
let error = try JSONDecoder().decode(ServerMessage.self, from: data) let error = try JSONDecoder().decode(ServerMessage.self, from: data)
DispatchQueue.main.sync { // TODO: Better error handling (Show error to user!)
alertMessage = .CUSTOM(title: "Error.", description: LocalizedStringKey(stringLiteral: error.msg))
presentAlert = true
return
}
} catch { } catch {
} }
@@ -410,46 +410,3 @@ fileprivate class Duration: ObservableObject {
fileprivate enum ErrorMessages: Error {
case NO_TITLE,
DUPLICATE,
UPLOAD_ERROR,
CONFIRM_DELETE,
GENERIC,
CUSTOM(title: LocalizedStringKey, description: LocalizedStringKey)
var localizedDescription: LocalizedStringKey {
switch self {
case .NO_TITLE:
return "Please enter a recipe name."
case .DUPLICATE:
return "A recipe with that name already exists."
case .UPLOAD_ERROR:
return "Unable to upload your recipe. Please check your internet connection."
case .CONFIRM_DELETE:
return "This action is not reversible!"
case .CUSTOM(title: _, description: let description):
return description
default:
return "An unknown error occured."
}
}
var localizedTitle: LocalizedStringKey {
switch self {
case .NO_TITLE:
return "Missing recipe name."
case .DUPLICATE:
return "Duplicate recipe."
case .UPLOAD_ERROR:
return "Network error."
case .CONFIRM_DELETE:
return "Delete recipe?"
case .CUSTOM(title: let title, description: _):
return title
default:
return "Error."
}
}
}

View File

@@ -8,45 +8,7 @@
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 ""
}
}
}
enum SupportedLanguage: String, Codable {
case EN = "en",
DE = "de",
ES = "es"
func descriptor() -> String {
switch self {
case .EN:
return "English"
case .DE:
return "Deutsch"
case .ES:
return "Español"
}
}
static let allValues = [EN, DE, ES]
}
struct SettingsView: View { struct SettingsView: View {
@ObservedObject var userSettings: UserSettings @ObservedObject var userSettings: UserSettings
@@ -58,23 +20,32 @@ struct SettingsView: View {
var body: some View { var body: some View {
Form { Form {
Section { Section {
/*Toggle(isOn: $userSettings.downloadRecipes) {
Text("Always download new recipes")
}*/
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) { Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
Text("None").tag("None") Text("None").tag("None")
ForEach(viewModel.categories, id: \.name) { category in ForEach(viewModel.categories, id: \.name) { category in
Text(category.name == "*" ? "Other" : category.name).tag(category) Text(category.name == "*" ? "Other" : category.name).tag(category)
} }
} }
Picker("Language", selection: $userSettings.language) {
ForEach(SupportedLanguage.allValues, id: \.self) { lang in
Text(lang.descriptor()).tag(lang.rawValue)
}
}
} header: { } header: {
Text("General") Text("General")
} footer: { } footer: {
Text("The selected cookbook will open on app launch by default.") Text("The selected cookbook will open on app launch by default.")
} }
Section {
Picker("Language", selection: $userSettings.language) {
ForEach(SupportedLanguage.allValues, id: \.self) { lang in
Text(lang.descriptor()).tag(lang.rawValue)
}
}
} footer: {
Text("If \'Same as Device\' is selected and your device language is not supported yet, this option will default to english.")
}
Section { Section {
Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!) Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!)
} header: { } header: {
@@ -142,3 +113,24 @@ struct SettingsView: View {
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 ""
}
}
}