WIP - Complete App refactoring

This commit is contained in:
VincentMeilinger
2025-05-26 15:52:24 +02:00
parent 29fd3c668b
commit 5acf3b9c4f
49 changed files with 1996 additions and 543 deletions

View File

@@ -6,68 +6,177 @@
//
import SwiftUI
import SwiftData
struct MainView: View {
@StateObject var appState = AppState()
@StateObject var groceryList = GroceryList()
//@State var cookbookState: CookbookState = CookbookState()
@Environment(\.modelContext) var modelContext
@Query var recipes: [Recipe] = []
// Tab ViewModels
@StateObject var recipeViewModel = RecipeTabView.ViewModel()
@StateObject var searchViewModel = SearchTabView.ViewModel()
enum Tab {
case recipes, search, groceryList
}
var body: some View {
TabView {
RecipeTabView()
.environmentObject(recipeViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
.tabItem {
Label("Recipes", systemImage: "book.closed.fill")
}
.tag(Tab.recipes)
SearchTabView()
.environmentObject(searchViewModel)
.environmentObject(appState)
.environmentObject(groceryList)
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(Tab.search)
GroceryListTabView()
.environmentObject(groceryList)
.tabItem {
if #available(iOS 17.0, *) {
Label("Grocery List", systemImage: "storefront")
} else {
Label("Grocery List", systemImage: "heart.text.square")
}
}
.tag(Tab.groceryList)
}
.task {
recipeViewModel.presentLoadingIndicator = true
await appState.getCategories()
await appState.updateAllRecipeDetails()
// Open detail view for default category
if UserSettings.shared.defaultCategory != "" {
if let cat = appState.categories.first(where: { c in
if c.name == UserSettings.shared.defaultCategory {
return true
}
return false
}) {
recipeViewModel.selectedCategory = cat
VStack {
List {
ForEach(recipes) { recipe in
Text(recipe.name)
}
}
await groceryList.load()
recipeViewModel.presentLoadingIndicator = false
Button("New") {
let recipe = Recipe(id: UUID().uuidString, name: "Neues Rezept", keywords: [], prepTime: "", cookTime: "", totalTime: "", recipeDescription: "", yield: 0, category: "", tools: [], ingredients: [], instructions: [], nutrition: [:], ingredientMultiplier: 0)
modelContext.insert(recipe)
}
}
/*NavigationSplitView {
VStack {
List(selection: $cookbookState.selectedCategory) {
ForEach(cookbookState.categories) { category in
Text(category.name)
.tag(category)
}
}
.listStyle(.plain)
.onAppear {
Task {
await cookbookState.loadCategories()
}
}
}
} content: {
if let selectedCategory = cookbookState.selectedCategory {
List(selection: $cookbookState.selectedRecipeStub) {
ForEach(cookbookState.recipeStubs[selectedCategory.name] ?? [], id: \.id) { recipeStub in
Text(recipeStub.title)
.tag(recipeStub)
}
}
.onAppear {
Task {
await cookbookState.loadRecipeStubs(category: selectedCategory.name)
}
}
} else {
Text("Please select a category.")
.foregroundColor(.secondary)
}
} detail: {
if let selectedRecipe = cookbookState.selectedRecipe {
if let recipe = cookbookState.recipes[selectedRecipe.id] {
RecipeView(recipe: recipe)
} else {
ProgressView()
.onAppear {
Task {
await cookbookState.loadRecipe(id: selectedRecipe.id)
}
}
}
} else {
Text("Please select a recipe.")
.foregroundColor(.secondary)
}
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(action: {
cookbookState.showGroceries = true
}) {
Label("Grocery List", systemImage: "cart")
}
}
ToolbarItem(placement: .topBarLeading) {
Button(action: {
cookbookState.showSettings = true
}) {
Label("Settings", systemImage: "gearshape")
}
}
}*/
}
}
/*struct CategoryListView: View {
@Bindable var cookbookState: CookbookState
var body: some View {
List(cookbookState.selectedAccountState.categories) { category in
NavigationLink {
RecipeListView(
cookbookState: cookbookState,
selectedCategory: category.name,
showEditView: .constant(false)
)
} label: {
HStack(alignment: .center) {
if cookbookState.selectedAccountState.selectedCategory != nil &&
category.name == cookbookState.selectedAccountState.selectedCategory?.name {
Image(systemName: "book")
} else {
Image(systemName: "book.closed.fill")
}
if category.name == "*" {
Text("Other")
.font(.system(size: 20, weight: .medium, design: .default))
} else {
Text(category.name)
.font(.system(size: 20, weight: .medium, design: .default))
}
Spacer()
Text("\(category.recipe_count)")
.font(.system(size: 15, weight: .bold, design: .default))
.foregroundStyle(Color.background)
.frame(width: 25, height: 25, alignment: .center)
.minimumScaleFactor(0.5)
.background {
Circle()
.foregroundStyle(Color.secondary)
}
}.padding(7)
}
}
}
}*/
/*struct CategoryListView: View {
@State var state: CookbookState
var body: some View {
List(selection: $state.categoryListSelection) {
ForEach(state.categories) { category in
NavigationLink(value: category) {
HStack(alignment: .center) {
if state.categoryListSelection != nil &&
category.name == state.categoryListSelection {
Image(systemName: "book")
} else {
Image(systemName: "book.closed.fill")
}
if category.name == "*" {
Text("Other")
.font(.system(size: 20, weight: .medium, design: .default))
} else {
Text(category.name)
.font(.system(size: 20, weight: .medium, design: .default))
}
Spacer()
Text("\(category.recipe_count)")
.font(.system(size: 15, weight: .bold, design: .default))
.foregroundStyle(Color.background)
.frame(width: 25, height: 25, alignment: .center)
.minimumScaleFactor(0.5)
.background {
Circle()
.foregroundStyle(Color.secondary)
}
}.padding(7)
}
}
}
}
}*/

View File

@@ -7,7 +7,7 @@
import Foundation
import SwiftUI
/*
struct OnboardingView: View {
@State var selectedTab: Int = 0
@@ -244,3 +244,4 @@ struct ServerAddressField_Preview: PreviewProvider {
.background(Color.nextcloudBlue)
}
}
*/

View File

@@ -9,7 +9,7 @@ import Foundation
import SwiftUI
/*
struct TokenLoginView: View {
@Binding var showAlert: Bool
@Binding var alertMessage: String
@@ -105,3 +105,4 @@ struct TokenLoginView: View {
return true
}
}
*/

View File

@@ -8,7 +8,7 @@
import Foundation
import SwiftUI
import WebKit
/*
enum V2LoginStage: LoginStage {
case login, validate
@@ -82,7 +82,7 @@ struct V2LoginView: View {
Task {
let error = await sendLoginV2Request()
if let error = error {
alertMessage = "A network error occured (\(error.rawValue))."
alertMessage = "A network error occured (\(error.localizedDescription))."
showAlert = true
}
if let loginRequest = loginRequest {
@@ -157,7 +157,7 @@ struct V2LoginView: View {
func checkLogin(response: LoginV2Response?, error: NetworkError?) {
if let error = error {
alertMessage = "Login failed. Please login via the browser and try again. (\(error.rawValue))"
alertMessage = "Login failed. Please login via the browser and try again. (\(error.localizedDescription))"
showAlert = true
return
}
@@ -209,3 +209,4 @@ struct WebView: UIViewRepresentable {
uiView.load(request)
}
}
*/

View File

@@ -8,9 +8,10 @@
import Foundation
import SwiftUI
/*
struct RecipeCardView: View {
@EnvironmentObject var appState: AppState
@State var recipe: Recipe
@State var recipe: CookbookApiRecipeV1
@State var recipeThumb: UIImage?
@State var isDownloaded: Bool? = nil
@@ -69,3 +70,63 @@ struct RecipeCardView: View {
.frame(height: 80)
}
}
*/
/*
struct RecipeCardView: View {
@State var state: AccountState
@State var recipe: RecipeStub
@State var recipeThumb: UIImage?
@State var isDownloaded: Bool? = nil
var body: some View {
HStack {
if let recipeThumb = recipeThumb {
Image(uiImage: recipeThumb)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 17))
} else {
Image(systemName: "square.text.square")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundStyle(Color.white)
.padding(10)
.background(Color("ncblue"))
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 17))
}
Text(recipe.name)
.font(.headline)
.padding(.leading, 4)
Spacer()
if let isDownloaded = isDownloaded {
VStack {
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
.foregroundColor(.secondary)
.padding()
Spacer()
}
}
}
.background(Color.backgroundHighlight)
.clipShape(RoundedRectangle(cornerRadius: 17))
.task {
recipeThumb = await state.getImage(
id: recipe.id,
size: .THUMB
)
isDownloaded = recipe.storedLocally
}
.refreshable {
recipeThumb = await state.getImage(
id: recipe.id,
size: .THUMB
)
}
.frame(height: 80)
}
}
*/

View File

@@ -9,14 +9,14 @@ import Foundation
import SwiftUI
/*
struct RecipeListView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryList
@State var categoryName: String
@State var searchText: String = ""
@Binding var showEditView: Bool
@State var selectedRecipe: Recipe? = nil
@State var selectedRecipe: CookbookApiRecipeV1? = nil
var body: some View {
Group {
@@ -56,7 +56,7 @@ struct RecipeListView: View {
.searchable(text: $searchText, prompt: "Search recipes/keywords")
.navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName)
.navigationDestination(for: Recipe.self) { recipe in
.navigationDestination(for: CookbookApiRecipeV1.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
.environmentObject(appState)
.environmentObject(groceryList)
@@ -85,7 +85,7 @@ struct RecipeListView: View {
}
}
func recipesFiltered() -> [Recipe] {
func recipesFiltered() -> [CookbookApiRecipeV1] {
guard let recipes = appState.recipes[categoryName] else { return [] }
guard searchText != "" else { return recipes }
return recipes.filter { recipe in
@@ -94,3 +94,86 @@ struct RecipeListView: View {
}
}
}
*/
/*
struct RecipeListView: View {
@Bindable var cookbookState: CookbookState
@State var selectedCategory: String
@State var searchText: String = ""
@Binding var showEditView: Bool
var body: some View {
Group {
let recipes = recipesFiltered()
if !recipes.isEmpty {
List(recipesFiltered(), selection: $cookbookState.selectedAccountState.selectedRecipe) { recipe in
RecipeCardView(state: cookbookState.selectedAccountState, recipe: recipe)
.shadow(radius: 2)
.background(
NavigationLink {
RecipeView(viewModel: RecipeView.ViewModel(recipeStub: recipe))
.environment(cookbookState)
} label: {
EmptyView()
}
.buttonStyle(.plain)
.opacity(0)
)
.frame(height: 85)
.listRowInsets(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
.listRowSeparatorTint(.clear)
}
.listStyle(.plain)
} else {
VStack {
Text("There are no recipes in this cookbook!")
Button {
Task {
let _ = await cookbookState.selectedAccountState.getCategories()
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: selectedCategory)
}
} label: {
Text("Refresh")
.bold()
}
.buttonStyle(.bordered)
}.padding()
}
}
.searchable(text: $searchText, prompt: "Search recipes/keywords")
.navigationTitle(selectedCategory == "*" ? String(localized: "Other") : selectedCategory)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
print("Add new recipe")
showEditView = true
} label: {
Image(systemName: "plus.circle.fill")
}
}
}
.task {
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(
named: selectedCategory
)
}
.refreshable {
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(
named: selectedCategory
)
}
}
func recipesFiltered() -> [RecipeStub] {
guard let recipes = cookbookState.selectedAccountState.recipeStubs[selectedCategory] else { return [] }
guard searchText != "" else { return recipes }
return recipes.filter { recipe in
recipe.name.lowercased().contains(searchText.lowercased()) || // check name for occurence of search term
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased())) // check keywords for search term
}
}
}
*/

View File

@@ -8,7 +8,7 @@
import Foundation
import SwiftUI
/*
struct RecipeView: View {
@EnvironmentObject var appState: AppState
@Environment(\.dismiss) private var dismiss
@@ -164,7 +164,7 @@ struct RecipeView: View {
let recipeDetail = await appState.getRecipe(
id: viewModel.recipe.recipe_id,
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
) ?? RecipeDetail.error
) ?? CookbookApiRecipeDetailV1.error
viewModel.setupView(recipeDetail: recipeDetail)
// Show download badge
@@ -182,7 +182,7 @@ struct RecipeView: View {
} else {
// Prepare view for a new recipe
viewModel.setupView(recipeDetail: RecipeDetail())
viewModel.setupView(recipeDetail: CookbookApiRecipeDetailV1())
viewModel.editMode = true
viewModel.isDownloaded = false
}
@@ -231,8 +231,8 @@ struct RecipeView: View {
// MARK: - RecipeView ViewModel
class ViewModel: ObservableObject {
@Published var observableRecipeDetail: ObservableRecipeDetail = ObservableRecipeDetail()
@Published var recipeDetail: RecipeDetail = RecipeDetail.error
@Published var observableRecipeDetail: Recipe = Recipe()
@Published var recipeDetail: CookbookApiRecipeDetailV1 = CookbookApiRecipeDetailV1.error
@Published var recipeImage: UIImage? = nil
@Published var editMode: Bool = false
@Published var showTitle: Bool = false
@@ -244,7 +244,7 @@ struct RecipeView: View {
@Published var presentIngredientEditView: Bool = false
@Published var presentToolEditView: Bool = false
var recipe: Recipe
var recipe: CookbookApiRecipeV1
var sharedURL: URL? = nil
var newRecipe: Bool = false
@@ -254,13 +254,13 @@ struct RecipeView: View {
var alertAction: () async -> () = { }
// Initializers
init(recipe: Recipe) {
init(recipe: CookbookApiRecipeV1) {
self.recipe = recipe
}
init() {
self.newRecipe = true
self.recipe = Recipe(
self.recipe = CookbookApiRecipeV1(
name: String(localized: "New Recipe"),
keywords: "",
dateCreated: "",
@@ -271,9 +271,9 @@ struct RecipeView: View {
}
// View setup
func setupView(recipeDetail: RecipeDetail) {
func setupView(recipeDetail: CookbookApiRecipeDetailV1) {
self.recipeDetail = recipeDetail
self.observableRecipeDetail = ObservableRecipeDetail(recipeDetail)
self.observableRecipeDetail = Recipe(recipeDetail)
}
func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) {
@@ -458,4 +458,434 @@ struct RecipeViewToolBar: ToolbarContent {
}
}
*/
/*
struct RecipeView: View {
@Environment(CookbookState.self) var cookbookState
@Environment(\.dismiss) private var dismiss
@State var viewModel: ViewModel
@GestureState private var dragOffset = CGSize.zero
var imageHeight: CGFloat {
if let image = viewModel.recipeImage {
return image.size.height < 350 ? image.size.height : 350
}
return 200
}
private enum CoordinateSpaces {
case scrollView
}
var body: some View {
ScrollView(showsIndicators: false) {
VStack(spacing: 0) {
ParallaxHeader(
coordinateSpace: CoordinateSpaces.scrollView,
defaultHeight: imageHeight
) {
if let recipeImage = viewModel.recipeImage {
Image(uiImage: recipeImage)
.resizable()
.scaledToFill()
.frame(maxHeight: imageHeight + 200)
.clipped()
} else {
Rectangle()
.frame(height: 400)
.foregroundStyle(
LinearGradient(
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
VStack(alignment: .leading) {
if viewModel.editMode {
RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe)
}
if viewModel.editMode {
RecipeMetadataSection(viewModel: viewModel)
}
HStack {
EditableText(text: $viewModel.recipe.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
.font(.title)
.bold()
Spacer()
if let isDownloaded = viewModel.isDownloaded {
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
.foregroundColor(.secondary)
}
}.padding([.top, .horizontal])
if viewModel.recipe.description != "" || viewModel.editMode {
EditableText(text: $viewModel.recipe.description, editMode: $viewModel.editMode, titleKey: "Description", lineLimit: 0...5, axis: .vertical)
.fontWeight(.medium)
.padding(.horizontal)
.padding(.top, 2)
}
// Recipe Body Section
RecipeDurationSection(viewModel: viewModel)
Divider()
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if(!viewModel.recipe.recipeIngredient.isEmpty || viewModel.editMode) {
RecipeIngredientSection(viewModel: viewModel)
}
if(!viewModel.recipe.recipeInstructions.isEmpty || viewModel.editMode) {
RecipeInstructionSection(viewModel: viewModel)
}
if(!viewModel.recipe.tool.isEmpty || viewModel.editMode) {
RecipeToolSection(viewModel: viewModel)
}
RecipeNutritionSection(viewModel: viewModel)
}
if !viewModel.editMode {
Divider()
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
RecipeKeywordSection(viewModel: viewModel)
MoreInformationSection(viewModel: viewModel)
}
}
}
.padding(.horizontal, 5)
.background(Rectangle().foregroundStyle(.background).shadow(radius: 5).mask(Rectangle().padding(.top, -20)))
}
}
.coordinateSpace(name: CoordinateSpaces.scrollView)
.ignoresSafeArea(.container, edges: .top)
.navigationBarTitleDisplayMode(.inline)
.toolbar(.visible, for: .navigationBar)
//.toolbarTitleDisplayMode(.inline)
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar {
RecipeViewToolBar(viewModel: viewModel)
}
.sheet(isPresented: $viewModel.presentShareSheet) {
ShareView(recipeDetail: viewModel.recipe.toRecipeDetail(),
recipeImage: viewModel.recipeImage,
presentShareSheet: $viewModel.presentShareSheet)
}
.sheet(isPresented: $viewModel.presentInstructionEditView) {
EditableListView(
isPresented: $viewModel.presentInstructionEditView,
items: $viewModel.recipe.recipeInstructions,
title: "Instructions",
emptyListText: "Add cooking steps for fellow chefs to follow.",
titleKey: "Instruction",
lineLimit: 0...10,
axis: .vertical)
}
.sheet(isPresented: $viewModel.presentIngredientEditView) {
EditableListView(
isPresented: $viewModel.presentIngredientEditView,
items: $viewModel.recipe.recipeIngredient,
title: "Ingredients",
emptyListText: "Start by adding your first ingredient! 🥬",
titleKey: "Ingredient",
lineLimit: 0...1,
axis: .horizontal)
}
.sheet(isPresented: $viewModel.presentToolEditView) {
EditableListView(
isPresented: $viewModel.presentToolEditView,
items: $viewModel.recipe.tool,
title: "Tools",
emptyListText: "List your tools here. 🍴",
titleKey: "Tool",
lineLimit: 0...1,
axis: .horizontal)
}
.task {
// Load recipe detail
if let recipeStub = viewModel.recipeStub {
// For existing recipes, load the recipeDetail and image
let recipe = await cookbookState.selectedAccountState.getRecipe(
id: recipeStub.id
) ?? Recipe()
viewModel.recipe = recipe
// Show download badge
/*if viewModel.recipeStub!.storedLocally == nil {
viewModel.recipeStub?.storedLocally = cookbookState.selectedAccountState.recipeDetailExists(
recipeId: viewModel.recipe.recipe_id
)
}
viewModel.isDownloaded = viewModel.recipeStub!.storedLocally
*/
// Load recipe image
viewModel.recipeImage = await cookbookState.selectedAccountState.getImage(
id: recipeStub.id,
size: .FULL
)
} else {
// Prepare view for a new recipe
viewModel.editMode = true
viewModel.isDownloaded = false
viewModel.recipe = Recipe()
}
}
.alert(viewModel.alertType.localizedTitle, isPresented: $viewModel.presentAlert) {
ForEach(viewModel.alertType.alertButtons) { buttonType in
if buttonType == .OK {
Button(AlertButton.OK.rawValue, role: .cancel) {
Task {
await viewModel.alertAction()
}
}
} else if buttonType == .CANCEL {
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
} else if buttonType == .DELETE {
Button(AlertButton.DELETE.rawValue, role: .destructive) {
Task {
await viewModel.alertAction()
}
}
}
}
} message: {
Text(viewModel.alertType.localizedDescription)
}
.onAppear {
if UserSettings.shared.keepScreenAwake {
UIApplication.shared.isIdleTimerDisabled = true
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
}
.onChange(of: viewModel.editMode) { newValue in
if newValue && cookbookState.selectedAccountState.keywords.isEmpty {
Task {
if let keywords = await cookbookState.selectedAccountState.getTags()?.sorted(by: { a, b in
a.recipe_count > b.recipe_count
}) {
cookbookState.selectedAccountState.keywords = keywords
}
}
}
}
}
// MARK: - RecipeView ViewModel
@Observable class ViewModel {
var recipeImage: UIImage? = nil
var editMode: Bool = false
var showTitle: Bool = false
var isDownloaded: Bool? = nil
var importUrl: String = ""
var presentShareSheet: Bool = false
var presentInstructionEditView: Bool = false
var presentIngredientEditView: Bool = false
var presentToolEditView: Bool = false
var recipeStub: RecipeStub? = nil
var recipe: Recipe = Recipe()
var sharedURL: URL? = nil
var newRecipe: Bool = false
// Alerts
var presentAlert = false
var alertType: UserAlert = RecipeAlert.GENERIC
var alertAction: () async -> () = { }
// Initializers
init(recipeStub: RecipeStub) {
self.recipeStub = recipeStub
}
init() {
self.newRecipe = true
}
func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) {
alertType = type
alertAction = action
presentAlert = true
}
}
}
extension RecipeView {
func importRecipe(from url: String) async -> UserAlert? {
/*let (scrapedRecipe, error) = await appState.importRecipe(url: url)
if let scrapedRecipe = scrapedRecipe {
viewModel.setupView(recipeDetail: scrapedRecipe)
return nil
}*/
do {
let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: url)
if let scrapedRecipe = scrapedRecipe {
viewModel.recipe = scrapedRecipe.toRecipe()
}
if let error = error {
return error
}
} catch {
print("Error")
}
return nil
}
}
// MARK: - Tool Bar
struct RecipeViewToolBar: ToolbarContent {
@Environment(CookbookState.self) var cookbookState
@Environment(\.dismiss) private var dismiss
@State var viewModel: RecipeView.ViewModel
var body: some ToolbarContent {
if viewModel.editMode {
ToolbarItemGroup(placement: .topBarLeading) {
Button("Cancel") {
viewModel.editMode = false
if viewModel.newRecipe {
dismiss()
}
}
if !viewModel.newRecipe {
Menu {
Button(role: .destructive) {
viewModel.presentAlert(
RecipeAlert.CONFIRM_DELETE,
action: {
await handleDelete()
}
)
} label: {
Label("Delete Recipe", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
ToolbarItem(placement: .topBarTrailing) {
Button {
Task {
await handleUpload()
}
} label: {
if viewModel.newRecipe {
Text("Upload Recipe")
} else {
Text("Upload Changes")
}
}
}
} else {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Button {
viewModel.editMode = true
} label: {
Label("Edit", systemImage: "pencil")
}
Button {
print("Sharing recipe ...")
viewModel.presentShareSheet = true
} label: {
Label("Share Recipe", systemImage: "square.and.arrow.up")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
}
func handleUpload() async {
if viewModel.newRecipe {
print("Uploading new recipe.")
if let recipeValidationError = recipeValid() {
viewModel.presentAlert(recipeValidationError)
return
}
if let alert = await cookbookState.selectedAccountState.postRecipe(recipe: viewModel.recipe) {
viewModel.presentAlert(alert)
return
}
} else {
print("Uploading changed recipe.")
if let alert = await cookbookState.selectedAccountState.updateRecipe(recipe: viewModel.recipe) {
viewModel.presentAlert(alert)
return
}
}
let _ = await cookbookState.selectedAccountState.getCategories()
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: viewModel.recipe.recipeCategory)
let _ = await cookbookState.selectedAccountState.getRecipe(id: viewModel.recipe.id)
viewModel.editMode = false
viewModel.presentAlert(RecipeAlert.UPLOAD_SUCCESS)
}
func handleDelete() async {
let category = viewModel.recipe.recipeCategory
if let alert = await cookbookState.selectedAccountState.deleteRecipe(id: viewModel.recipe.id) {
viewModel.presentAlert(alert)
return
}
let _ = await cookbookState.selectedAccountState.getCategories()
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: category)
viewModel.presentAlert(RecipeAlert.DELETE_SUCCESS)
dismiss()
}
func recipeValid() -> RecipeAlert? {
// Check if the recipe has a name
if viewModel.recipe.name.replacingOccurrences(of: " ", with: "") == "" {
return RecipeAlert.NO_TITLE
}
// Check if the recipe has a unique name
for recipeList in cookbookState.selectedAccountState.recipeStubs.values {
for r in recipeList {
if r.name
.replacingOccurrences(of: " ", with: "")
.lowercased() ==
viewModel.recipe.name
.replacingOccurrences(of: " ", with: "")
.lowercased()
{
return RecipeAlert.DUPLICATE
}
}
}
return nil
}
}
*/

View File

@@ -9,17 +9,17 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Duration Section
/*
struct RecipeDurationSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
@State var presentPopover: Bool = false
var body: some View {
VStack(alignment: .leading) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
DurationView(time: viewModel.recipe.prepTime, title: LocalizedStringKey("Preparation"))
DurationView(time: viewModel.recipe.cookTime, title: LocalizedStringKey("Cooking"))
DurationView(time: viewModel.recipe.totalTime, title: LocalizedStringKey("Total time"))
}
if viewModel.editMode {
Button {
@@ -34,9 +34,9 @@ struct RecipeDurationSection: View {
.padding()
.popover(isPresented: $presentPopover) {
EditableDurationView(
prepTime: viewModel.observableRecipeDetail.prepTime,
cookTime: viewModel.observableRecipeDetail.cookTime,
totalTime: viewModel.observableRecipeDetail.totalTime
prepTime: viewModel.recipe.prepTime,
cookTime: viewModel.recipe.cookTime,
totalTime: viewModel.recipe.totalTime
)
}
}
@@ -94,10 +94,10 @@ fileprivate struct EditableDurationView: View {
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
}
.padding()
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
.onChange(of: prepTime.hourComponent) { updateTotalTime() }
.onChange(of: prepTime.minuteComponent) { updateTotalTime() }
.onChange(of: cookTime.hourComponent) { updateTotalTime() }
.onChange(of: cookTime.minuteComponent) { updateTotalTime() }
}
}
@@ -142,3 +142,5 @@ fileprivate struct TimePickerView: View {
.padding()
}
}
*/

View File

@@ -10,9 +10,9 @@ import SwiftUI
// MARK: - RecipeView Import Section
/*
struct RecipeImportSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var importRecipe: (String) async -> UserAlert?
var body: some View {
@@ -49,4 +49,4 @@ struct RecipeImportSection: View {
.padding(.top, 5)
}
}
*/

View File

@@ -9,23 +9,23 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Ingredients Section
/*
struct RecipeIngredientSection: View {
@EnvironmentObject var groceryList: GroceryList
@ObservedObject var viewModel: RecipeView.ViewModel
@Environment(CookbookState.self) var cookbookState
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
HStack {
Button {
withAnimation {
if groceryList.containsRecipe(viewModel.observableRecipeDetail.id) {
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
if cookbookState.groceryList.containsRecipe(viewModel.recipe.id) {
cookbookState.groceryList.deleteGroceryRecipe(viewModel.recipe.id)
} else {
groceryList.addItems(
viewModel.observableRecipeDetail.recipeIngredient,
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
cookbookState.groceryList.addItems(
viewModel.recipe.recipeIngredient,
toRecipe: viewModel.recipe.id,
recipeName: viewModel.recipe.name
)
}
}
@@ -45,26 +45,26 @@ struct RecipeIngredientSection: View {
.foregroundStyle(.secondary)
.bold()
ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier)
ServingPickerView(selectedServingSize: $viewModel.recipe.ingredientMultiplier)
}
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
ForEach(0..<viewModel.recipe.recipeIngredient.count, id: \.self) { ix in
IngredientListItem(
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
servings: $viewModel.observableRecipeDetail.ingredientMultiplier,
recipeYield: Double(viewModel.observableRecipeDetail.recipeYield),
recipeId: viewModel.observableRecipeDetail.id
ingredient: $viewModel.recipe.recipeIngredient[ix],
servings: $viewModel.recipe.ingredientMultiplier,
recipeYield: Double(viewModel.recipe.recipeYield),
recipeId: viewModel.recipe.id
) {
groceryList.addItem(
viewModel.observableRecipeDetail.recipeIngredient[ix],
toRecipe: viewModel.observableRecipeDetail.id,
recipeName: viewModel.observableRecipeDetail.name
cookbookState.groceryList.addItem(
viewModel.recipe.recipeIngredient[ix],
toRecipe: viewModel.recipe.id,
recipeName: viewModel.recipe.name
)
}
.padding(4)
}
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
if viewModel.recipe.ingredientMultiplier != Double(viewModel.recipe.recipeYield) {
HStack() {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.secondary)
@@ -83,14 +83,14 @@ struct RecipeIngredientSection: View {
}
}
.padding()
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
.animation(.easeInOut, value: viewModel.recipe.ingredientMultiplier)
}
}
// MARK: - RecipeIngredientSection List Item
fileprivate struct IngredientListItem: View {
@EnvironmentObject var groceryList: GroceryList
@Environment(CookbookState.self) var cookbookState
@Binding var ingredient: String
@Binding var servings: Double
@State var recipeYield: Double
@@ -110,7 +110,7 @@ fileprivate struct IngredientListItem: View {
var body: some View {
HStack(alignment: .top) {
if groceryList.containsItem(at: recipeId, item: ingredient) {
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
if #available(iOS 17.0, *) {
Image(systemName: "storefront")
.foregroundStyle(Color.green)
@@ -140,11 +140,11 @@ fileprivate struct IngredientListItem: View {
}
Spacer()
}
.onChange(of: servings) { newServings in
.onChange(of: servings) { _, newServings in
if recipeYield == 0 {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings)
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings)
} else {
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield)
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings/recipeYield)
}
}
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
@@ -168,8 +168,8 @@ fileprivate struct IngredientListItem: View {
.onEnded { gesture in
withAnimation {
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
if groceryList.containsItem(at: recipeId, item: ingredient) {
groceryList.deleteItem(ingredient, fromRecipe: recipeId)
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
cookbookState.groceryList.deleteItem(ingredient, fromRecipe: recipeId)
} else {
addToGroceryListAction()
}
@@ -209,9 +209,12 @@ struct ServingPickerView: View {
.bold()
}
}
.onChange(of: selectedServingSize) { newValue in
.onChange(of: selectedServingSize) { _, newValue in
if newValue < 0 { selectedServingSize = 0 }
else if newValue > 100 { selectedServingSize = 100 }
}
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Instructions Section
/*
struct RecipeInstructionSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
@@ -19,8 +19,8 @@ struct RecipeInstructionSection: View {
SecondaryLabel(text: LocalizedStringKey("Instructions"))
Spacer()
}
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
ForEach(viewModel.recipe.recipeInstructions.indices, id: \.self) { ix in
RecipeInstructionListItem(instruction: $viewModel.recipe.recipeInstructions[ix], index: ix+1)
}
if viewModel.editMode {
Button {
@@ -56,4 +56,4 @@ fileprivate struct RecipeInstructionListItem: View {
.animation(.easeInOut, value: isSelected)
}
}
*/

View File

@@ -9,16 +9,16 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Keyword Section
/*
struct RecipeKeywordSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
let columns: [GridItem] = [ GridItem(.flexible(minimum: 50, maximum: 200), spacing: 5) ]
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
Group {
if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: $viewModel.observableRecipeDetail.keywords)
if !viewModel.recipe.keywords.isEmpty && !viewModel.editMode {
RecipeListSection(list: $viewModel.recipe.keywords)
} else {
Text(LocalizedStringKey("No keywords."))
}
@@ -189,3 +189,4 @@ struct KeywordPickerView_Previews: PreviewProvider {
}
}
*/

View File

@@ -9,14 +9,14 @@ import Foundation
import SwiftUI
// MARK: - Recipe Metadata Section
/*
struct RecipeMetadataSection: View {
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: RecipeView.ViewModel
@Environment(CookbookState.self) var cookbookState
@State var viewModel: RecipeView.ViewModel
@State var keywords: [RecipeKeyword] = []
var categories: [String] {
appState.categories.map({ category in category.name })
cookbookState.selectedAccountState.categories.map({ category in category.name })
}
@State var presentKeywordSheet: Bool = false
@@ -28,11 +28,11 @@ struct RecipeMetadataSection: View {
// Category
SecondaryLabel(text: "Category")
HStack {
TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory)
TextField("Category", text: $viewModel.recipe.recipeCategory)
.lineLimit(1)
.textFieldStyle(.roundedBorder)
Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
Picker("Choose", selection: $viewModel.recipe.recipeCategory) {
Text("").tag("")
ForEach(categories, id: \.self) { item in
Text(item)
@@ -45,10 +45,10 @@ struct RecipeMetadataSection: View {
// Keywords
SecondaryLabel(text: "Keywords")
if !viewModel.observableRecipeDetail.keywords.isEmpty {
if !viewModel.recipe.keywords.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
ForEach(viewModel.recipe.keywords, id: \.self) { keyword in
Text(keyword)
.padding(5)
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
@@ -70,11 +70,11 @@ struct RecipeMetadataSection: View {
Button {
presentServingsPopover.toggle()
} label: {
Text("\(viewModel.observableRecipeDetail.recipeYield) Serving(s)")
Text("\(viewModel.recipe.recipeYield) Serving(s)")
.lineLimit(1)
}
.popover(isPresented: $presentServingsPopover) {
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.observableRecipeDetail.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.recipe.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
}
}
}
@@ -82,7 +82,7 @@ struct RecipeMetadataSection: View {
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
.padding([.horizontal, .bottom], 5)
.sheet(isPresented: $presentKeywordSheet) {
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
KeywordPickerView(title: "Keywords", searchSuggestions: cookbookState.selectedAccountState.keywords, selection: $viewModel.recipe.keywords)
}
}
}
@@ -126,22 +126,22 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
// MARK: - RecipeView More Information Section
struct MoreInformationSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
VStack(alignment: .leading) {
if let dateCreated = viewModel.recipeDetail.dateCreated {
if let dateCreated = viewModel.recipe.dateCreated {
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: dateCreated) ?? "")")
}
if let dateModified = viewModel.recipeDetail.dateModified {
if let dateModified = viewModel.recipe.dateModified {
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: dateModified) ?? "")")
}
if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) {
if viewModel.recipe.url != "", let url = URL(string: viewModel.recipe.url ?? "") {
HStack(alignment: .top) {
Text("URL:")
Link(destination: url) {
Text(viewModel.observableRecipeDetail.url)
Text(viewModel.recipe.url ?? "")
}
}
}
@@ -157,3 +157,5 @@ struct MoreInformationSection: View {
.padding()
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Nutrition Section
/*
struct RecipeNutritionSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
@@ -28,7 +28,7 @@ struct RecipeNutritionSection: View {
} else if !nutritionEmpty() {
VStack(alignment: .leading) {
ForEach(Nutrition.allCases, id: \.self) { nutrition in
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
if let value = viewModel.recipe.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
HStack(alignment: .top) {
Text("\(nutrition.localizedDescription): \(value)")
.multilineTextAlignment(.leading)
@@ -43,7 +43,7 @@ struct RecipeNutritionSection: View {
}
} title: {
HStack {
if let servingSize = viewModel.observableRecipeDetail.nutrition["servingSize"] {
if let servingSize = viewModel.recipe.nutrition["servingSize"] {
SecondaryLabel(text: "Nutrition (\(servingSize))")
} else {
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
@@ -56,17 +56,19 @@ struct RecipeNutritionSection: View {
func binding(for key: String) -> Binding<String> {
Binding(
get: { viewModel.observableRecipeDetail.nutrition[key, default: ""] },
set: { viewModel.observableRecipeDetail.nutrition[key] = $0 }
get: { viewModel.recipe.nutrition[key, default: ""] },
set: { viewModel.recipe.nutrition[key] = $0 }
)
}
func nutritionEmpty() -> Bool {
for nutrition in Nutrition.allCases {
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
if let value = viewModel.recipe.nutrition[nutrition.dictKey] {
return false
}
}
return true
}
}
*/

View File

@@ -9,9 +9,9 @@ import Foundation
import SwiftUI
// MARK: - RecipeView Tool Section
/*
struct RecipeToolSection: View {
@ObservedObject var viewModel: RecipeView.ViewModel
@State var viewModel: RecipeView.ViewModel
var body: some View {
VStack(alignment: .leading) {
@@ -20,7 +20,7 @@ struct RecipeToolSection: View {
Spacer()
}
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
RecipeListSection(list: $viewModel.recipe.tool)
if viewModel.editMode {
Button {
@@ -35,3 +35,5 @@ struct RecipeToolSection: View {
}
*/

View File

@@ -10,7 +10,7 @@ import SwiftUI
struct ShareView: View {
@State var recipeDetail: RecipeDetail
@State var recipeDetail: CookbookApiRecipeDetailV1
@State var recipeImage: UIImage?
@Binding var presentShareSheet: Bool

View File

@@ -7,7 +7,7 @@
import Foundation
import SwiftUI
/*
struct CollapsibleView<C: View, T: View>: View {
@State var titleColor: Color = .white
@State var isCollapsed: Bool = true
@@ -48,3 +48,4 @@ struct CollapsibleView<C: View, T: View>: View {
}
}
}
*/

View File

@@ -9,8 +9,12 @@ import Foundation
import SwiftUI
struct SettingsView: View {
var body: some View {
Text("Settings")
}
}
/*struct SettingsView: View {
@EnvironmentObject var appState: AppState
@ObservedObject var userSettings = UserSettings.shared
@ObservedObject var viewModel = ViewModel()
@@ -248,3 +252,4 @@ extension SettingsView {
*/

View File

@@ -8,36 +8,34 @@
import Foundation
import SwiftUI
/*
struct GroceryListTabView: View {
@EnvironmentObject var groceryList: GroceryList
@Environment(CookbookState.self) var cookbookState
var body: some View {
NavigationStack {
if groceryList.groceryDict.isEmpty {
if cookbookState.groceryList.groceryDict.isEmpty {
EmptyGroceryListView()
} else {
List {
ForEach(groceryList.groceryDict.keys.sorted(), id: \.self) { key in
ForEach(cookbookState.groceryList.groceryDict.keys.sorted(), id: \.self) { key in
Section {
ForEach(groceryList.groceryDict[key]!.items) { item in
ForEach(cookbookState.groceryList.groceryDict[key]!.items) { item in
GroceryListItemView(item: item, toggleAction: {
groceryList.toggleItemChecked(item)
groceryList.objectWillChange.send()
cookbookState.groceryList.toggleItemChecked(item)
}, deleteAction: {
groceryList.deleteItem(item.name, fromRecipe: key)
withAnimation {
groceryList.objectWillChange.send()
cookbookState.groceryList.deleteItem(item.name, fromRecipe: key)
}
})
}
} header: {
HStack {
Text(groceryList.groceryDict[key]!.name)
Text(cookbookState.groceryList.groceryDict[key]!.name)
.foregroundStyle(Color.nextcloudBlue)
Spacer()
Button {
groceryList.deleteGroceryRecipe(key)
cookbookState.groceryList.deleteGroceryRecipe(key)
} label: {
Image(systemName: "trash")
.foregroundStyle(Color.nextcloudBlue)
@@ -51,7 +49,7 @@ struct GroceryListTabView: View {
.navigationTitle("Grocery List")
.toolbar {
Button {
groceryList.deleteAll()
cookbookState.groceryList.deleteAll()
} label: {
Text("Delete")
.foregroundStyle(Color.nextcloudBlue)
@@ -143,25 +141,22 @@ class GroceryRecipeItem: Identifiable, Codable {
@MainActor class GroceryList: ObservableObject {
@Observable class GroceryList {
let dataStore: DataStore = DataStore()
@Published var groceryDict: [String: GroceryRecipe] = [:]
@Published var sortBySimilarity: Bool = false
var groceryDict: [String: GroceryRecipe] = [:]
var sortBySimilarity: Bool = false
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
print("Adding item of recipe \(String(describing: recipeName))")
DispatchQueue.main.async {
if self.groceryDict[recipeId] != nil {
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
} else {
let newRecipe = GroceryRecipe(name: recipeName ?? "-", items: [GroceryRecipeItem(itemName)])
self.groceryDict[recipeId] = newRecipe
}
if saveGroceryDict {
self.save()
self.objectWillChange.send()
}
if self.groceryDict[recipeId] != nil {
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
} else {
let newRecipe = GroceryRecipe(name: recipeName ?? "-", items: [GroceryRecipeItem(itemName)])
self.groceryDict[recipeId] = newRecipe
}
if saveGroceryDict {
self.save()
}
}
@@ -170,7 +165,6 @@ class GroceryRecipeItem: Identifiable, Codable {
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
}
save()
objectWillChange.send()
}
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
@@ -182,14 +176,12 @@ class GroceryRecipeItem: Identifiable, Codable {
groceryDict.removeValue(forKey: recipeId)
}
save()
objectWillChange.send()
}
func deleteGroceryRecipe(_ recipeId: String) {
print("Deleting grocery recipe with id \(recipeId)")
groceryDict.removeValue(forKey: recipeId)
save()
objectWillChange.send()
}
func deleteAll() {
@@ -234,4 +226,4 @@ class GroceryRecipeItem: Identifiable, Codable {
}
}
*/

View File

@@ -8,7 +8,7 @@
import Foundation
import SwiftUI
/*
struct RecipeTabView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var groceryList: GroceryList
@@ -179,3 +179,5 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
}
}
*/

View File

@@ -7,7 +7,7 @@
import Foundation
import SwiftUI
/*
struct SearchTabView: View {
@EnvironmentObject var viewModel: SearchTabView.ViewModel
@EnvironmentObject var appState: AppState
@@ -30,7 +30,7 @@ struct SearchTabView: View {
.listRowSeparatorTint(.clear)
}
.listStyle(.plain)
.navigationDestination(for: Recipe.self) { recipe in
.navigationDestination(for: CookbookApiRecipeV1.self) { recipe in
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
}
.searchable(text: $viewModel.searchText, prompt: "Search recipes/keywords")
@@ -48,7 +48,7 @@ struct SearchTabView: View {
}
class ViewModel: ObservableObject {
@Published var allRecipes: [Recipe] = []
@Published var allRecipes: [CookbookApiRecipeV1] = []
@Published var searchText: String = ""
@Published var searchMode: SearchMode = .name
@@ -58,7 +58,7 @@ struct SearchTabView: View {
case name = "Name & Keywords", ingredient = "Ingredients"
}
func recipesFiltered() -> [Recipe] {
func recipesFiltered() -> [CookbookApiRecipeV1] {
if searchMode == .name {
guard searchText != "" else { return allRecipes }
return allRecipes.filter { recipe in
@@ -72,3 +72,4 @@ struct SearchTabView: View {
}
}
}
*/