Recipe edit UI polish
This commit is contained in:
@@ -34,6 +34,16 @@ struct RecipeView: View {
|
||||
.scaledToFill()
|
||||
.frame(maxHeight: imageHeight + 200)
|
||||
.clipped()
|
||||
} else {
|
||||
Rectangle()
|
||||
.frame(height: 400)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
gradient: Gradient(colors: [.white, .nextcloudBlue]),
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +51,11 @@ struct RecipeView: View {
|
||||
if viewModel.editMode {
|
||||
RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe)
|
||||
}
|
||||
|
||||
if viewModel.editMode {
|
||||
RecipeMetadataSection(viewModel: viewModel)
|
||||
}
|
||||
|
||||
HStack {
|
||||
EditableText(text: $viewModel.observableRecipeDetail.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
|
||||
.font(.title)
|
||||
@@ -66,10 +81,6 @@ struct RecipeView: View {
|
||||
|
||||
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)
|
||||
@@ -83,13 +94,12 @@ struct RecipeView: View {
|
||||
RecipeNutritionSection(viewModel: viewModel)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||
if !viewModel.editMode {
|
||||
if !viewModel.editMode {
|
||||
Divider()
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||
RecipeKeywordSection(viewModel: viewModel)
|
||||
MoreInformationSection(viewModel: viewModel)
|
||||
}
|
||||
MoreInformationSection(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 5)
|
||||
@@ -99,6 +109,7 @@ struct RecipeView: View {
|
||||
.coordinateSpace(name: CoordinateSpaces.scrollView)
|
||||
.ignoresSafeArea(.container, edges: .top)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
//.toolbarTitleDisplayMode(.inline)
|
||||
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
|
||||
.toolbar {
|
||||
if viewModel.editMode {
|
||||
@@ -112,7 +123,6 @@ struct RecipeView: View {
|
||||
// Upload Button
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
// TODO: POST edited recipe
|
||||
Task {
|
||||
if viewModel.newRecipe {
|
||||
if let res = await uploadNewRecipe() {
|
||||
@@ -191,6 +201,7 @@ struct RecipeView: View {
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +210,36 @@ struct RecipeView: View {
|
||||
recipeImage: viewModel.recipeImage,
|
||||
presentShareSheet: $viewModel.presentShareSheet)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.presentInstructionEditView) {
|
||||
EditableListView(
|
||||
isPresented: $viewModel.presentInstructionEditView,
|
||||
items: $viewModel.observableRecipeDetail.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.observableRecipeDetail.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.observableRecipeDetail.tool,
|
||||
title: "Tools",
|
||||
emptyListText: "List your tools here. 🍴",
|
||||
titleKey: "Tool",
|
||||
lineLimit: 0...1,
|
||||
axis: .horizontal)
|
||||
}
|
||||
|
||||
.task {
|
||||
// Load recipe detail
|
||||
@@ -282,10 +323,15 @@ struct RecipeView: View {
|
||||
@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
|
||||
@Published var importUrl: String = ""
|
||||
|
||||
@Published var presentShareSheet: Bool = false
|
||||
@Published var presentInstructionEditView: Bool = false
|
||||
@Published var presentIngredientEditView: Bool = false
|
||||
@Published var presentToolEditView: Bool = false
|
||||
|
||||
var recipe: Recipe
|
||||
var sharedURL: URL? = nil
|
||||
var newRecipe: Bool = false
|
||||
@@ -408,26 +454,28 @@ fileprivate struct RecipeImportSection: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
HStack {
|
||||
|
||||
TextField(LocalizedStringKey("URL (e.g. example.com/recipe)"), text: $viewModel.importUrl)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button {
|
||||
Task {
|
||||
if let res = await importRecipe(viewModel.importUrl) {
|
||||
viewModel.alertType = RecipeAlert.CUSTOM(
|
||||
title: res.localizedTitle,
|
||||
description: res.localizedDescription
|
||||
)
|
||||
viewModel.alertAction = { }
|
||||
viewModel.presentAlert = true
|
||||
}
|
||||
.padding(.top, 5)
|
||||
Button {
|
||||
Task {
|
||||
if let res = await importRecipe(viewModel.importUrl) {
|
||||
viewModel.alertType = RecipeAlert.CUSTOM(
|
||||
title: res.localizedTitle,
|
||||
description: res.localizedDescription
|
||||
)
|
||||
viewModel.alertAction = { }
|
||||
viewModel.presentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Text(LocalizedStringKey("Import"))
|
||||
}
|
||||
}.padding(.top, 5)
|
||||
} label: {
|
||||
Text(LocalizedStringKey("Import"))
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
.padding()
|
||||
.background(Rectangle().foregroundStyle(Color.white.opacity(0.1)))
|
||||
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1)))
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,40 +15,28 @@ struct RecipeDurationSection: View {
|
||||
@State var presentPopover: Bool = false
|
||||
|
||||
var body: some View {
|
||||
if !viewModel.editMode {
|
||||
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"))
|
||||
}
|
||||
} else {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
|
||||
Button {
|
||||
presentPopover.toggle()
|
||||
} label: {
|
||||
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
|
||||
}
|
||||
Button {
|
||||
presentPopover.toggle()
|
||||
} label: {
|
||||
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
|
||||
}
|
||||
Button {
|
||||
presentPopover.toggle()
|
||||
} label: {
|
||||
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
|
||||
}
|
||||
}
|
||||
.popover(isPresented: $presentPopover) {
|
||||
EditableDurationView(
|
||||
prepTime: viewModel.observableRecipeDetail.prepTime,
|
||||
cookTime: viewModel.observableRecipeDetail.cookTime,
|
||||
totalTime: viewModel.observableRecipeDetail.totalTime
|
||||
)
|
||||
Button {
|
||||
presentPopover.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
.padding()
|
||||
.popover(isPresented: $presentPopover) {
|
||||
EditableDurationView(
|
||||
prepTime: viewModel.observableRecipeDetail.prepTime,
|
||||
cookTime: viewModel.observableRecipeDetail.cookTime,
|
||||
totalTime: viewModel.observableRecipeDetail.totalTime
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,36 +52,64 @@ fileprivate struct DurationView: View {
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "clock")
|
||||
.bold()
|
||||
.foregroundStyle(.secondary)
|
||||
Text(time.displayString)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct EditableDurationView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@ObservedObject var prepTime: DurationComponents
|
||||
@ObservedObject var cookTime: DurationComponents
|
||||
@ObservedObject var totalTime: DurationComponents
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .center) {
|
||||
HStack {
|
||||
SecondaryLabel(text: "Preparation")
|
||||
Spacer()
|
||||
Button("Done") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
TimePickerView(selectedHour: $prepTime.hourComponent, selectedMinute: $prepTime.minuteComponent)
|
||||
SecondaryLabel(text: "Cooking")
|
||||
|
||||
HStack {
|
||||
SecondaryLabel(text: "Cooking")
|
||||
Spacer()
|
||||
}
|
||||
TimePickerView(selectedHour: $cookTime.hourComponent, selectedMinute: $cookTime.minuteComponent)
|
||||
SecondaryLabel(text: "Total")
|
||||
|
||||
HStack {
|
||||
SecondaryLabel(text: "Total time")
|
||||
Spacer()
|
||||
}
|
||||
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() }
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTotalTime() {
|
||||
var hourComponent = prepTime.hourComponent + cookTime.hourComponent
|
||||
var minuteComponent = prepTime.minuteComponent + cookTime.minuteComponent
|
||||
// Handle potential overflow from minutes to hours
|
||||
if minuteComponent >= 60 {
|
||||
hourComponent += minuteComponent / 60
|
||||
minuteComponent %= 60
|
||||
}
|
||||
totalTime.hourComponent = hourComponent
|
||||
totalTime.minuteComponent = minuteComponent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,27 +59,93 @@ struct EditableText: View {
|
||||
}
|
||||
|
||||
|
||||
struct EditableStringList<Content: View>: View {
|
||||
@Binding var items: [ReorderableItem<String>]
|
||||
@Binding var editMode: Bool
|
||||
struct EditableListView: View {
|
||||
@Binding var isPresented: Bool
|
||||
@Binding var items: [String]
|
||||
@State var title: LocalizedStringKey
|
||||
@State var emptyListText: LocalizedStringKey
|
||||
@State var titleKey: LocalizedStringKey = ""
|
||||
@State var lineLimit: ClosedRange<Int> = 0...50
|
||||
@State var axis: Axis = .vertical
|
||||
|
||||
var content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
if editMode {
|
||||
VStack {
|
||||
ReorderableForEach(items: $items, defaultItem: ReorderableItem(item: "")) { ix, item in
|
||||
TextField("", text: $items[ix].item, axis: axis)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.lineLimit(lineLimit)
|
||||
NavigationView {
|
||||
ZStack {
|
||||
List {
|
||||
if items.isEmpty {
|
||||
Text(emptyListText)
|
||||
}
|
||||
|
||||
ForEach(items.indices, id: \.self) { ix in
|
||||
TextField(titleKey, text: $items[ix], axis: axis)
|
||||
.lineLimit(lineLimit)
|
||||
}
|
||||
.onDelete(perform: deleteItem)
|
||||
.onMove(perform: moveItem)
|
||||
}
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
addItem()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
.foregroundStyle(.white)
|
||||
.bold()
|
||||
.padding()
|
||||
.background(Circle().fill(Color.nextcloudBlue))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.transition(.slide)
|
||||
} else {
|
||||
content()
|
||||
.navigationBarTitle(title, displayMode: .inline)
|
||||
.navigationBarItems(
|
||||
trailing: Button(action: { isPresented = false }) {
|
||||
Text("Done")
|
||||
}
|
||||
)
|
||||
.environment(\.editMode, .constant(.active)) // Bind edit mode to your state variable
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
items.append("")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItem(at offsets: IndexSet) {
|
||||
withAnimation {
|
||||
items.remove(atOffsets: offsets)
|
||||
}
|
||||
}
|
||||
|
||||
private func moveItem(from source: IndexSet, to destination: Int) {
|
||||
withAnimation {
|
||||
items.move(fromOffsets: source, toOffset: destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct EditableListView_Previews: PreviewProvider {
|
||||
// Sample keywords for preview
|
||||
@State static var sampleList: [String] = [
|
||||
/*"3 Eggs",
|
||||
"1 kg Potatos",
|
||||
"3 g Sugar",
|
||||
"1 ml Milk",
|
||||
"Salt, Pepper"*/
|
||||
]
|
||||
|
||||
static var previews: some View {
|
||||
Color.white
|
||||
.sheet(isPresented: .constant(true), content: {
|
||||
EditableListView(isPresented: .constant(true), items: $sampleList, title: "Ingredient", emptyListText: "Add cooking steps for fellow chefs to follow.")
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct RecipeIngredientSection: View {
|
||||
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
|
||||
} else {
|
||||
groceryList.addItems(
|
||||
ReorderableItem.items(viewModel.observableRecipeDetail.recipeIngredient),
|
||||
viewModel.observableRecipeDetail.recipeIngredient,
|
||||
toRecipe: viewModel.observableRecipeDetail.id,
|
||||
recipeName: viewModel.observableRecipeDetail.name
|
||||
)
|
||||
@@ -46,17 +46,23 @@ struct RecipeIngredientSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
EditableStringList(items: $viewModel.observableRecipeDetail.recipeIngredient, editMode: $viewModel.editMode, titleKey: "Ingredient", lineLimit: 0...1, axis: .horizontal) {
|
||||
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
|
||||
IngredientListItem(ingredient: viewModel.observableRecipeDetail.recipeIngredient[ix], recipeId: viewModel.observableRecipeDetail.id) {
|
||||
groceryList.addItem(
|
||||
viewModel.observableRecipeDetail.recipeIngredient[ix].item,
|
||||
toRecipe: viewModel.observableRecipeDetail.id,
|
||||
recipeName: viewModel.observableRecipeDetail.name
|
||||
)
|
||||
}
|
||||
.padding(4)
|
||||
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
|
||||
IngredientListItem(ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix], recipeId: viewModel.observableRecipeDetail.id) {
|
||||
groceryList.addItem(
|
||||
viewModel.observableRecipeDetail.recipeIngredient[ix],
|
||||
toRecipe: viewModel.observableRecipeDetail.id,
|
||||
recipeName: viewModel.observableRecipeDetail.name
|
||||
)
|
||||
}
|
||||
.padding(4)
|
||||
}
|
||||
if viewModel.editMode {
|
||||
Button {
|
||||
viewModel.presentIngredientEditView.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
@@ -66,7 +72,7 @@ struct RecipeIngredientSection: View {
|
||||
|
||||
fileprivate struct IngredientListItem: View {
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@State var ingredient: ReorderableItem<String>
|
||||
@Binding var ingredient: String
|
||||
@State var recipeId: String
|
||||
let addToGroceryListAction: () -> Void
|
||||
@State var isSelected: Bool = false
|
||||
@@ -78,7 +84,7 @@ fileprivate struct IngredientListItem: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
if groceryList.containsItem(at: recipeId, item: ingredient.item) {
|
||||
if groceryList.containsItem(at: recipeId, item: ingredient) {
|
||||
if #available(iOS 17.0, *) {
|
||||
Image(systemName: "storefront")
|
||||
.foregroundStyle(Color.green)
|
||||
@@ -93,7 +99,7 @@ fileprivate struct IngredientListItem: View {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
|
||||
Text("\(ingredient.item)")
|
||||
Text("\(ingredient)")
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(5)
|
||||
Spacer()
|
||||
@@ -119,15 +125,13 @@ fileprivate struct IngredientListItem: View {
|
||||
.onEnded { gesture in
|
||||
withAnimation {
|
||||
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
|
||||
if groceryList.containsItem(at: recipeId, item: ingredient.item) {
|
||||
groceryList.deleteItem(ingredient.item, fromRecipe: recipeId)
|
||||
if groceryList.containsItem(at: recipeId, item: ingredient) {
|
||||
groceryList.deleteItem(ingredient, fromRecipe: recipeId)
|
||||
} else {
|
||||
addToGroceryListAction()
|
||||
}
|
||||
|
||||
}
|
||||
// Animate back to original position
|
||||
|
||||
self.dragOffset = 0
|
||||
self.animationStartOffset = 0
|
||||
}
|
||||
|
||||
@@ -19,19 +19,27 @@ struct RecipeInstructionSection: View {
|
||||
SecondaryLabel(text: LocalizedStringKey("Instructions"))
|
||||
Spacer()
|
||||
}
|
||||
EditableStringList(items: $viewModel.observableRecipeDetail.recipeInstructions, editMode: $viewModel.editMode, titleKey: "Instruction", lineLimit: 0...15, axis: .vertical) {
|
||||
ForEach(0..<viewModel.observableRecipeDetail.recipeInstructions.count, id: \.self) { ix in
|
||||
RecipeInstructionListItem(instruction: viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
|
||||
}
|
||||
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
|
||||
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
|
||||
}
|
||||
}.padding()
|
||||
if viewModel.editMode {
|
||||
Button {
|
||||
viewModel.presentInstructionEditView.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct RecipeInstructionListItem: View {
|
||||
@State var instruction: ReorderableItem<String>
|
||||
@Binding var instruction: String
|
||||
@State var index: Int
|
||||
@State var isSelected: Bool = false
|
||||
|
||||
@@ -39,7 +47,7 @@ fileprivate struct RecipeInstructionListItem: View {
|
||||
HStack(alignment: .top) {
|
||||
Text("\(index)")
|
||||
.monospaced()
|
||||
Text(instruction.item)
|
||||
Text(instruction)
|
||||
}.padding(4)
|
||||
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
||||
.onTapGesture {
|
||||
|
||||
@@ -19,7 +19,6 @@ struct RecipeMetadataSection: View {
|
||||
appState.categories.map({ category in category.name })
|
||||
}
|
||||
|
||||
|
||||
@State var presentKeywordSheet: Bool = false
|
||||
@State var presentServingsPopover: Bool = false
|
||||
@State var presentCategoryPopover: Bool = false
|
||||
@@ -27,7 +26,6 @@ struct RecipeMetadataSection: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
// Category
|
||||
//CategoryPickerView(items: $categories, input: $viewModel.observableRecipeDetail.recipeCategory, titleKey: "Category")
|
||||
SecondaryLabel(text: "Category")
|
||||
HStack {
|
||||
TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory)
|
||||
@@ -42,6 +40,7 @@ struct RecipeMetadataSection: View {
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
// Keywords
|
||||
SecondaryLabel(text: "Keywords")
|
||||
@@ -51,6 +50,8 @@ struct RecipeMetadataSection: View {
|
||||
HStack {
|
||||
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
|
||||
Text(keyword)
|
||||
.padding(5)
|
||||
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +62,7 @@ struct RecipeMetadataSection: View {
|
||||
Text("Select Keywords")
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
// Servings / Yield
|
||||
VStack(alignment: .leading) {
|
||||
@@ -77,7 +79,8 @@ struct RecipeMetadataSection: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Rectangle().foregroundStyle(Color.white.opacity(0.1)))
|
||||
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.white.opacity(0.1)))
|
||||
.padding()
|
||||
.sheet(isPresented: $presentKeywordSheet) {
|
||||
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
|
||||
}
|
||||
@@ -104,47 +107,6 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct CategoryPickerView: View {
|
||||
@Binding var items: [String]
|
||||
@Binding var input: String
|
||||
@State private var pickerChoice: String = ""
|
||||
|
||||
var titleKey: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
SecondaryLabel(text: "Category")
|
||||
.padding([.top, .horizontal])
|
||||
HStack {
|
||||
TextField(titleKey, text: $input)
|
||||
.lineLimit(1)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
.onSubmit {
|
||||
pickerChoice = ""
|
||||
}
|
||||
|
||||
Picker("Select Item", selection: $pickerChoice) {
|
||||
Text("").tag("")
|
||||
ForEach(items, id: \.self) { item in
|
||||
Text(item)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.padding()
|
||||
.onChange(of: pickerChoice) { newValue in
|
||||
if pickerChoice != "" {
|
||||
input = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
pickerChoice = input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - RecipeView More Information Section
|
||||
|
||||
|
||||
@@ -19,9 +19,19 @@ struct RecipeToolSection: View {
|
||||
SecondaryLabel(text: "Tools")
|
||||
Spacer()
|
||||
}
|
||||
EditableStringList(items: $viewModel.observableRecipeDetail.tool, editMode: $viewModel.editMode, titleKey: "Tool", lineLimit: 0...1, axis: .horizontal) {
|
||||
RecipeListSection(list: ReorderableItem.items(viewModel.observableRecipeDetail.tool))
|
||||
|
||||
RecipeListSection(list: viewModel.observableRecipeDetail.tool)
|
||||
|
||||
if viewModel.editMode {
|
||||
Button {
|
||||
viewModel.presentToolEditView.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user