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

@@ -14,104 +14,203 @@ struct MainView: View {
@State private var selectedCategory: Category? = nil
@State private var showEditView: Bool = false
@State private var showSearchView: Bool = false
@State private var showSettingsView: Bool = false
@State private var serverConnection: Bool = false
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)]
var body: some View {
NavigationSplitView {
List(viewModel.categories, selection: $selectedCategory) { category in
if category.recipe_count != 0 {
NavigationLink(value: category) {
HStack(alignment: .center) {
Image(systemName: "book.closed.fill")
Text(category.name == "*" ? "Other" : category.name)
.font(.system(size: 20, weight: .light, design: .serif))
.italic()
}.padding(7)
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 {
NavigationLink(value: category) {
HStack(alignment: .center) {
Image(systemName: "book.closed.fill")
Text(category.name == "*" ? "Other" : category.name)
.font(.system(size: 20, weight: .light, design: .serif))
.italic()
}.padding(7)
}
}
}
}
.navigationTitle("Cookbooks")
.navigationDestination(isPresented: $showSettingsView) {
SettingsView(userSettings: userSettings, viewModel: viewModel)
}
.navigationDestination(isPresented: $showSearchView) {
RecipeSearchView(viewModel: viewModel)
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Menu {
Button {
print("Downloading all recipes ...")
Task {
await viewModel.downloadAllRecipes()
}
} label: {
HStack {
Text("Download all recipes")
Image(systemName: "icloud.and.arrow.down")
}
}
Button {
self.showSettingsView = true
} label: {
Text("Settings")
Image(systemName: "gearshape")
}
} label: {
Image(systemName: "ellipsis.circle")
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
}
}
ToolbarItem(placement: .topBarTrailing) {
}
.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) {
Menu {
Button {
print("Add new recipe")
showEditView = true
print("Downloading all recipes ...")
Task {
await viewModel.downloadAllRecipes()
}
} label: {
HStack {
Image(systemName: "plus.circle.fill")
Text("Download all recipes")
Image(systemName: "icloud.and.arrow.down")
}
}
Button {
self.showSettingsView = true
} label: {
Text("Settings")
Image(systemName: "gearshape")
}
} label: {
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) {
Button {
print("Add new recipe")
showEditView = true
} label: {
Image(systemName: "plus.circle.fill")
}
}
}
}
struct RecipeSearchView: View {
@ObservedObject var viewModel: MainViewModel
@State var searchText: String = ""
@State var allRecipes: [Recipe] = []
var body: some View {
NavigationStack {
VStack {
ScrollView(showsIndicators: false) {
LazyVStack {
ForEach(recipesFiltered(), id: \.recipe_id) { recipe in
NavigationLink(value: recipe) {
RecipeCardView(viewModel: viewModel, recipe: recipe)
}
.buttonStyle(.plain)
}
}
}
}
} 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
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetailView(viewModel: viewModel, recipe: recipe)
}
.searchable(text: $searchText, prompt: "Search recipes")
}
}
.tint(.nextcloudBlue)
.sheet(isPresented: $showEditView) {
RecipeEditView(viewModel: viewModel, isPresented: $showEditView)
.navigationTitle("Search recipe")
}
.task {
await viewModel.loadCategoryList()
if userSettings.defaultCategory != "" {
if let cat = viewModel.categories.first(where: { c in
if c.name == userSettings.defaultCategory {
return true
}
return false
}) {
self.selectedCategory = cat
}
}
allRecipes = await viewModel.getAllRecipes()
}
.refreshable {
await viewModel.loadCategoryList(needsUpdate: true)
}
func recipesFiltered() -> [Recipe] {
guard searchText != "" else { return allRecipes }
return allRecipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased())
}
}
}

View File

@@ -16,11 +16,24 @@ struct RecipeCardView: View {
var body: some View {
HStack {
Image(uiImage: recipeThumb ?? UIImage(named: "cookbook-recipe")!)
.resizable()
.aspectRatio(contentMode: .fill)
if let recipeThumb = recipeThumb {
Image(uiImage: recipeThumb)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.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)
.clipped()
}
Text(recipe.name)
.font(.headline)

View File

@@ -12,6 +12,7 @@ import PhotosUI
struct RecipeEditView: View {
@EnvironmentObject var alertHandler: AlertHandler
@ObservedObject var viewModel: MainViewModel
@State var recipe: RecipeDetail = RecipeDetail()
@Binding var isPresented: Bool
@@ -25,8 +26,6 @@ struct RecipeEditView: View {
@State private var keywords: [String] = []
@State private var keywordSuggestions: [String] = []
@State private var alertMessage: ErrorMessages = .GENERIC
@State private var presentAlert: Bool = false
@State private var waitingForUpload: Bool = false
var body: some View {
@@ -43,8 +42,9 @@ struct RecipeEditView: View {
Menu {
Button {
print("Delete recipe.")
alertMessage = .CONFIRM_DELETE
presentAlert = true
alertHandler.present(alert: .CONFIRM_DELETE) {
deleteRecipe()
}
} label: {
Image(systemName: "trash")
.foregroundStyle(.red)
@@ -152,18 +152,24 @@ struct RecipeEditView: View {
keywords.append(keyword)
}
}
.alert(alertMessage.localizedTitle, isPresented: $presentAlert) {
switch alertMessage {
case .CONFIRM_DELETE:
Button("Cancel", role: .cancel) { }
Button("Delete", role: .destructive) {
deleteRecipe()
.alert(alertHandler.alert.localizedTitle, isPresented: $alertHandler.presentAlert) {
ForEach(alertHandler.alert.alertButtons) { buttonType in
if buttonType == .OK {
Button(AlertButton.OK.rawValue, role: .cancel) {
alertHandler.alertAction()
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: {
Text(alertMessage.localizedDescription)
Text(alertHandler.alert.localizedDescription)
}
}
@@ -179,9 +185,8 @@ struct RecipeEditView: View {
func recipeValid() -> Bool {
// Check if the recipe has a name
if recipe.name == "" {
self.alertMessage = .NO_TITLE
self.presentAlert = true
if recipe.name.replacingOccurrences(of: " ", with: "") == "" {
alertHandler.present(alert: .NO_TITLE)
return false
}
// Check if the recipe has a unique name
@@ -194,8 +199,7 @@ struct RecipeEditView: View {
.replacingOccurrences(of: " ", with: "")
.lowercased()
{
self.alertMessage = .DUPLICATE
self.presentAlert = true
alertHandler.present(alert: .DUPLICATE)
return false
}
}
@@ -266,11 +270,7 @@ struct RecipeEditView: View {
guard let data = data else { return }
do {
let error = try JSONDecoder().decode(ServerMessage.self, from: data)
DispatchQueue.main.sync {
alertMessage = .CUSTOM(title: "Error.", description: LocalizedStringKey(stringLiteral: error.msg))
presentAlert = true
return
}
// TODO: Better error handling (Show error to user!)
} 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 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 {
@ObservedObject var userSettings: UserSettings
@@ -58,23 +20,32 @@ struct SettingsView: View {
var body: some View {
Form {
Section {
/*Toggle(isOn: $userSettings.downloadRecipes) {
Text("Always download new recipes")
}*/
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
Text("None").tag("None")
ForEach(viewModel.categories, id: \.name) { category in
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: {
Text("General")
} footer: {
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 {
Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!)
} 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 ""
}
}
}