Code cleanup
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// CategoryPickerView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 03.10.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct CategoryPickerView: View {
|
||||
@State var title: String
|
||||
@State var searchSuggestions: [String]
|
||||
@Binding var selection: String
|
||||
@State var searchText: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TextField(title, text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
List {
|
||||
if searchText != "" {
|
||||
HStack {
|
||||
if selection.contains(searchText) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(searchText)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
selection = searchText
|
||||
}
|
||||
}
|
||||
ForEach(suggestionsFiltered(), id: \.self) { suggestion in
|
||||
HStack {
|
||||
if selection.contains(suggestion) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(suggestion)
|
||||
}
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
selection = suggestion
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle(title)
|
||||
}
|
||||
|
||||
func suggestionsFiltered() -> [String] {
|
||||
guard searchText != "" else { return searchSuggestions }
|
||||
return searchSuggestions.filter { suggestion in
|
||||
suggestion.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// KeywordPickerView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 03.10.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
|
||||
struct KeywordPickerView: View {
|
||||
@State var title: String
|
||||
@State var searchSuggestions: [RecipeKeyword]
|
||||
@Binding var selection: [String]
|
||||
@State var searchText: String = ""
|
||||
|
||||
var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 5)]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
TextField(title, text: $searchText)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding()
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
if searchText != "" {
|
||||
KeywordItemView(
|
||||
keyword: searchText,
|
||||
isSelected: selection.contains(searchText)
|
||||
) { keyword in
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword ? true : false
|
||||
})
|
||||
searchSuggestions.removeAll(where: { s in
|
||||
s.name == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
ForEach(suggestionsFiltered(), id: \.name) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: suggestion.name,
|
||||
count: suggestion.recipe_count,
|
||||
isSelected: selection.contains(suggestion.name)
|
||||
) { keyword in
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider().padding()
|
||||
HStack {
|
||||
Text("Selected keywords:")
|
||||
.font(.headline)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
LazyVGrid(columns: columns, spacing: 5) {
|
||||
ForEach(selection, id: \.self) { suggestion in
|
||||
KeywordItemView(
|
||||
keyword: suggestion,
|
||||
isSelected: true
|
||||
) { keyword in
|
||||
if selection.contains(keyword) {
|
||||
selection.removeAll(where: { s in
|
||||
s == keyword ? true : false
|
||||
})
|
||||
} else {
|
||||
selection.append(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.padding(5)
|
||||
|
||||
}
|
||||
|
||||
func suggestionsFiltered() -> [RecipeKeyword] {
|
||||
guard searchText != "" else { return searchSuggestions }
|
||||
return searchSuggestions.filter { suggestion in
|
||||
suggestion.name.lowercased().contains(searchText.lowercased())
|
||||
}.sorted(by: { a, b in
|
||||
a.recipe_count > b.recipe_count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct KeywordItemView: View {
|
||||
var keyword: String
|
||||
var count: Int? = nil
|
||||
var isSelected: Bool
|
||||
var tapped: (String) -> ()
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if isSelected {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
}
|
||||
Text(keyword)
|
||||
.lineLimit(2)
|
||||
Spacer()
|
||||
if let count = count {
|
||||
Text("(\(count))")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.foregroundStyle(Color("backgroundHighlight"))
|
||||
)
|
||||
.onTapGesture {
|
||||
tapped(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//
|
||||
// RecipeEditView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 29.09.23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
|
||||
|
||||
struct RecipeEditView: View {
|
||||
@ObservedObject var viewModel: RecipeEditViewModel
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
@State var presentAlert = false
|
||||
@State var alertType: UserAlert = RecipeAlert.GENERIC
|
||||
@State var alertAction: @MainActor () async -> () = { }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Button() {
|
||||
isPresented = false
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
.bold()
|
||||
}
|
||||
if !viewModel.uploadNew {
|
||||
Menu {
|
||||
Button {
|
||||
print("Delete recipe.")
|
||||
alertType = RecipeAlert.CONFIRM_DELETE
|
||||
alertAction = {
|
||||
if let res = await viewModel.deleteRecipe() {
|
||||
alertType = res
|
||||
alertAction = { }
|
||||
presentAlert = true
|
||||
} else {
|
||||
self.dismissEditView()
|
||||
}
|
||||
}
|
||||
presentAlert = true
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(.red)
|
||||
Text("Delete recipe")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
.font(.title3)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button() {
|
||||
Task {
|
||||
if viewModel.uploadNew {
|
||||
if let res = await viewModel.uploadNewRecipe() {
|
||||
alertType = res
|
||||
presentAlert = true
|
||||
} else {
|
||||
dismissEditView()
|
||||
}
|
||||
} else {
|
||||
if let res = await viewModel.uploadEditedRecipe() {
|
||||
alertType = res
|
||||
presentAlert = true
|
||||
} else {
|
||||
dismissEditView()
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Upload")
|
||||
.bold()
|
||||
}
|
||||
}.padding()
|
||||
HStack {
|
||||
Text(viewModel.recipe.name == "" ? String(localized: "New recipe") : viewModel.recipe.name)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
Form {
|
||||
if viewModel.showImportSection {
|
||||
Section {
|
||||
TextField(LocalizedStringKey("URL (e.g. example.com/recipe)"), text: $viewModel.importURL)
|
||||
Button {
|
||||
Task {
|
||||
if let res = await viewModel.importRecipe() {
|
||||
alertType = RecipeAlert.CUSTOM(
|
||||
title: res.localizedTitle,
|
||||
description: res.localizedDescription
|
||||
)
|
||||
alertAction = { }
|
||||
presentAlert = true
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(LocalizedStringKey("Import"))
|
||||
}
|
||||
} header: {
|
||||
Text(LocalizedStringKey("Import Recipe"))
|
||||
} footer: {
|
||||
Text(LocalizedStringKey("Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings."))
|
||||
}
|
||||
|
||||
} else {
|
||||
Section {
|
||||
Button() {
|
||||
withAnimation{
|
||||
viewModel.showImportSection = true
|
||||
}
|
||||
} label: {
|
||||
Text("Import recipe from a website")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField("Title", text: $viewModel.recipe.name)
|
||||
Section {
|
||||
TextEditor(text: $viewModel.recipe.description)
|
||||
} header: {
|
||||
Text("Description")
|
||||
}
|
||||
|
||||
Section() {
|
||||
NavigationLink(viewModel.recipe.recipeCategory == "" ? "Category" : "Category: \(viewModel.recipe.recipeCategory)") {
|
||||
CategoryPickerView(
|
||||
title: "Category",
|
||||
searchSuggestions: viewModel.mainViewModel.categories.map({ category in
|
||||
category.name == "*" ? "Other" : category.name
|
||||
}),
|
||||
selection: $viewModel.recipe.recipeCategory)
|
||||
}
|
||||
NavigationLink("Keywords") {
|
||||
KeywordPickerView(
|
||||
title: "Keywords",
|
||||
searchSuggestions: viewModel.keywordSuggestions,
|
||||
selection: $viewModel.keywords
|
||||
)
|
||||
}
|
||||
} footer: {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(viewModel.keywords, id: \.self) { keyword in
|
||||
Text(keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section() {
|
||||
Picker("Servings:", selection: $viewModel.recipe.recipeYield) {
|
||||
ForEach(0..<99, id: \.self) { i in
|
||||
Text("\(i)").tag(i)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
DurationPicker(title: LocalizedStringKey("Preparation duration:"), duration: viewModel.prepDuration)
|
||||
DurationPicker(title: LocalizedStringKey("Cooking duration:"), duration: viewModel.cookDuration)
|
||||
DurationPicker(title: LocalizedStringKey("Total duration:"), duration: viewModel.totalDuration)
|
||||
}
|
||||
|
||||
EditableListSection(title: LocalizedStringKey("Ingredients"), items: $viewModel.recipe.recipeIngredient)
|
||||
EditableListSection(title: LocalizedStringKey("Tools"), items: $viewModel.recipe.tool)
|
||||
EditableListSection(title: LocalizedStringKey("Instructions"), items: $viewModel.recipe.recipeInstructions)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
viewModel.keywordSuggestions = await viewModel.mainViewModel.getKeywords(fetchMode: .preferServer)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.prepareView()
|
||||
}
|
||||
.alert(alertType.localizedTitle, isPresented: $presentAlert) {
|
||||
ForEach(alertType.alertButtons) { buttonType in
|
||||
if buttonType == .OK {
|
||||
Button(AlertButton.OK.rawValue, role: .cancel) {
|
||||
Task {
|
||||
await alertAction()
|
||||
}
|
||||
}
|
||||
} else if buttonType == .CANCEL {
|
||||
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
|
||||
} else if buttonType == .DELETE {
|
||||
Button(AlertButton.DELETE.rawValue, role: .destructive) {
|
||||
Task {
|
||||
await alertAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} message: {
|
||||
Text(alertType.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissEditView() {
|
||||
Task {
|
||||
await viewModel.mainViewModel.getCategories()
|
||||
await viewModel.mainViewModel.getCategory(named: viewModel.recipe.recipeCategory, fetchMode: .preferServer)
|
||||
await viewModel.mainViewModel.updateRecipeDetails(in: viewModel.recipe.recipeCategory)
|
||||
}
|
||||
self.isPresented = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct EditableListSection: View {
|
||||
@State var title: LocalizedStringKey
|
||||
@Binding var items: [String]
|
||||
|
||||
var body: some View {
|
||||
Section() {
|
||||
List {
|
||||
ForEach(items.indices, id: \.self) { ix in
|
||||
HStack(alignment: .top) {
|
||||
Text("\(ix+1).")
|
||||
.padding(.vertical, 10)
|
||||
TextEditor(text: $items[ix])
|
||||
.multilineTextAlignment(.leading)
|
||||
.textFieldStyle(.plain)
|
||||
.padding(.vertical, 1)
|
||||
}
|
||||
}
|
||||
.onMove { indexSet, offset in
|
||||
items.move(fromOffsets: indexSet, toOffset: offset)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
items.remove(atOffsets: indexSet)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Add")
|
||||
Button() {
|
||||
items.append("")
|
||||
} label: {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fileprivate struct DurationPicker: View {
|
||||
@State var title: LocalizedStringKey
|
||||
@ObservedObject var duration: DurationComponents
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
TextField("00", text: $duration.hourComponent)
|
||||
.keyboardType(.decimalPad)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.frame(maxWidth: 40)
|
||||
Text(":")
|
||||
TextField("00", text: $duration.minuteComponent)
|
||||
.keyboardType(.decimalPad)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 40)
|
||||
}
|
||||
.frame(maxHeight: 40)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user