Files
Nextcloud-Cookbook-iOS/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeView.swift
2024-03-01 15:32:04 +01:00

241 lines
9.4 KiB
Swift

//
// RecipeDetailView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 15.09.23.
//
import Foundation
import SwiftUI
struct RecipeView: View {
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject var appState: AppState
@StateObject var viewModel: ViewModel
@State var imageHeight: CGFloat = 350
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()
}
}
VStack(alignment: .leading) {
HStack {
EditableText(text: $viewModel.observableRecipeDetail.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.observableRecipeDetail.description != "" || viewModel.editMode {
EditableText(text: $viewModel.observableRecipeDetail.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()
if viewModel.editMode {
RecipeMetadataSection(viewModel: viewModel)
}
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if(!viewModel.observableRecipeDetail.recipeIngredient.isEmpty || viewModel.editMode) {
RecipeIngredientSection(viewModel: viewModel)
}
if(!viewModel.observableRecipeDetail.recipeInstructions.isEmpty || viewModel.editMode) {
RecipeInstructionSection(viewModel: viewModel)
}
if(!viewModel.observableRecipeDetail.tool.isEmpty || viewModel.editMode) {
RecipeToolSection(viewModel: viewModel)
}
RecipeNutritionSection(viewModel: viewModel)
}
Divider()
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
if !viewModel.editMode {
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)
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
.toolbar {
if viewModel.editMode {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") {
viewModel.editMode = false
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
// TODO: POST edited recipe
if viewModel.newRecipe {
presentationMode.wrappedValue.dismiss()
} else {
viewModel.editMode = false
}
}
}
} else {
ToolbarItem(placement: .topBarTrailing) {
Menu {
Button {
viewModel.editMode = true
} label: {
HStack {
Text("Edit")
Image(systemName: "pencil")
}
}
Button {
print("Sharing recipe ...")
viewModel.presentShareSheet = true
} label: {
Text("Share recipe")
Image(systemName: "square.and.arrow.up")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}
}
.sheet(isPresented: $viewModel.presentShareSheet) {
ShareView(recipeDetail: viewModel.observableRecipeDetail.toRecipeDetail(),
recipeImage: viewModel.recipeImage,
presentShareSheet: $viewModel.presentShareSheet)
}
.task {
// Load recipe detail
if !viewModel.newRecipe {
// For existing recipes, load the recipeDetail and image
let recipeDetail = await appState.getRecipe(
id: viewModel.recipe.recipe_id,
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
) ?? RecipeDetail.error
viewModel.setupView(recipeDetail: recipeDetail)
// Show download badge
if viewModel.recipe.storedLocally == nil {
viewModel.recipe.storedLocally = appState.recipeDetailExists(recipeId: viewModel.recipe.recipe_id)
}
viewModel.isDownloaded = viewModel.recipe.storedLocally
// Load recipe image
viewModel.recipeImage = await appState.getImage(
id: viewModel.recipe.recipe_id,
size: .FULL,
fetchMode: UserSettings.shared.storeImages ? .preferLocal : .onlyServer
)
if let image = viewModel.recipeImage {
imageHeight = image.size.height < 350 ? image.size.height : 350
} else {
imageHeight = 100
}
} else {
// Prepare view for a new recipe
viewModel.setupView(recipeDetail: RecipeDetail())
viewModel.editMode = true
viewModel.isDownloaded = false
}
}
.onAppear {
if UserSettings.shared.keepScreenAwake {
UIApplication.shared.isIdleTimerDisabled = true
}
}
.onDisappear {
UIApplication.shared.isIdleTimerDisabled = false
}
.onChange(of: viewModel.editMode) { newValue in
if newValue && appState.allKeywords.isEmpty {
Task {
appState.allKeywords = await appState.getKeywords(fetchMode: .preferServer).sorted(by: { a, b in
a.recipe_count > b.recipe_count
})
}
}
}
}
// MARK: - RecipeView ViewModel
class ViewModel: ObservableObject {
@Published var observableRecipeDetail: ObservableRecipeDetail = ObservableRecipeDetail()
@Published var recipeDetail: RecipeDetail = RecipeDetail.error
@Published var recipeImage: UIImage? = nil
@Published var editMode: Bool = false
@Published var presentShareSheet: Bool = false
@Published var showTitle: Bool = false
@Published var isDownloaded: Bool? = nil
var newRecipe: Bool = false
var recipe: Recipe
var sharedURL: URL? = nil
init(recipe: Recipe) {
self.recipe = recipe
}
init() {
self.newRecipe = true
self.recipe = Recipe(
name: String(localized: "New Recipe"),
keywords: "",
dateCreated: "",
dateModified: "",
imageUrl: "",
imagePlaceholderUrl: "",
recipe_id: 0)
}
func setupView(recipeDetail: RecipeDetail) {
self.recipeDetail = recipeDetail
self.observableRecipeDetail = ObservableRecipeDetail(recipeDetail)
}
}
}