Added category 'All' recipes
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user