Nextcloud Login refactoring
This commit is contained in:
@@ -7,55 +7,158 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@Model class GroceryItem {
|
||||
var name: String
|
||||
var isChecked: Bool
|
||||
|
||||
init(name: String, isChecked: Bool) {
|
||||
self.name = name
|
||||
self.isChecked = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
@Model class RecipeGroceries: Identifiable {
|
||||
var id: String
|
||||
var name: String
|
||||
@Relationship(deleteRule: .cascade) var items: [GroceryItem]
|
||||
var multiplier: Double
|
||||
|
||||
init(id: String, name: String, items: [GroceryItem], multiplier: Double) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.items = items
|
||||
self.multiplier = multiplier
|
||||
}
|
||||
|
||||
init(id: String, name: String) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.items = []
|
||||
self.multiplier = 1
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct GroceryListTabView: View {
|
||||
@Environment(CookbookState.self) var cookbookState
|
||||
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@Query var groceryList: [RecipeGroceries] = []
|
||||
@State var newGroceries: String = ""
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
if cookbookState.groceryList.groceryDict.isEmpty {
|
||||
EmptyGroceryListView()
|
||||
} else {
|
||||
List {
|
||||
ForEach(cookbookState.groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
||||
Section {
|
||||
ForEach(cookbookState.groceryList.groceryDict[key]!.items) { item in
|
||||
GroceryListItemView(item: item, toggleAction: {
|
||||
cookbookState.groceryList.toggleItemChecked(item)
|
||||
}, deleteAction: {
|
||||
withAnimation {
|
||||
cookbookState.groceryList.deleteItem(item.name, fromRecipe: key)
|
||||
}
|
||||
})
|
||||
List {
|
||||
HStack(alignment: .top) {
|
||||
TextEditor(text: $newGroceries)
|
||||
.padding(4)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.secondary).opacity(0.5))
|
||||
.focused($isFocused)
|
||||
Button {
|
||||
if !newGroceries.isEmpty {
|
||||
let items = newGroceries
|
||||
.split(separator: "\n")
|
||||
.compactMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
Task {
|
||||
await addGroceryItems(items, toCategory: "Other", named: String(localized: "Other"))
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(cookbookState.groceryList.groceryDict[key]!.name)
|
||||
}
|
||||
newGroceries = ""
|
||||
|
||||
} label: {
|
||||
Text("Add")
|
||||
}
|
||||
.disabled(newGroceries.isEmpty)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
|
||||
ForEach(groceryList, id: \.name) { category in
|
||||
Section {
|
||||
ForEach(category.items, id: \.self) { item in
|
||||
GroceryListItemView(item: item)
|
||||
}
|
||||
} header: {
|
||||
HStack {
|
||||
Text(category.name)
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
Spacer()
|
||||
Button {
|
||||
modelContext.delete(category)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
Spacer()
|
||||
Button {
|
||||
cookbookState.groceryList.deleteGroceryRecipe(key)
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Grocery List")
|
||||
.toolbar {
|
||||
Button {
|
||||
cookbookState.groceryList.deleteAll()
|
||||
} label: {
|
||||
Text("Delete")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
}
|
||||
if groceryList.isEmpty {
|
||||
Text("You're all set for cooking 🍓")
|
||||
.font(.headline)
|
||||
Text("Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe.")
|
||||
.foregroundStyle(.secondary)
|
||||
Text("To add grocieries manually, type them in the box below and press the button. To add multiple items at once, separate them by a new line.")
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Grocery List")
|
||||
.toolbar {
|
||||
Button {
|
||||
do {
|
||||
try modelContext.delete(model: RecipeGroceries.self)
|
||||
} catch {
|
||||
print("Failed to delete all GroceryCategory models.")
|
||||
}
|
||||
} label: {
|
||||
Text("Delete")
|
||||
.foregroundStyle(Color.nextcloudBlue)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func addGroceryItems(_ itemNames: [String], toCategory categoryId: String, named name: String) async {
|
||||
do {
|
||||
// Find or create the target category
|
||||
let categoryPredicate = #Predicate<RecipeGroceries> { $0.id == categoryId }
|
||||
let fetchDescriptor = FetchDescriptor<RecipeGroceries>(predicate: categoryPredicate)
|
||||
|
||||
var targetCategory: RecipeGroceries?
|
||||
if let existingCategory = try modelContext.fetch(fetchDescriptor).first {
|
||||
targetCategory = existingCategory
|
||||
} else {
|
||||
// Create the category if it doesn't exist
|
||||
let newCategory = RecipeGroceries(id: categoryId, name: name)
|
||||
modelContext.insert(newCategory)
|
||||
targetCategory = newCategory
|
||||
}
|
||||
|
||||
guard let category = targetCategory else { return }
|
||||
|
||||
// Add new GroceryItems to the category
|
||||
for itemName in itemNames {
|
||||
let newItem = GroceryItem(name: itemName, isChecked: false)
|
||||
category.items.append(newItem)
|
||||
}
|
||||
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Error adding grocery items: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteGroceryItems(at offsets: IndexSet, in category: RecipeGroceries) {
|
||||
for index in offsets {
|
||||
let itemToDelete = category.items[index]
|
||||
modelContext.delete(itemToDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,9 +166,8 @@ struct GroceryListTabView: View {
|
||||
|
||||
|
||||
fileprivate struct GroceryListItemView: View {
|
||||
let item: GroceryRecipeItem
|
||||
let toggleAction: () -> Void
|
||||
let deleteAction: () -> Void
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@Bindable var item: GroceryItem
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
@@ -81,149 +183,13 @@ fileprivate struct GroceryListItemView: View {
|
||||
}
|
||||
.padding(5)
|
||||
.foregroundStyle(item.isChecked ? Color.secondary : Color.primary)
|
||||
.onTapGesture(perform: toggleAction)
|
||||
.onTapGesture(perform: { item.isChecked.toggle() })
|
||||
.animation(.easeInOut, value: item.isChecked)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
Button(action: deleteAction) {
|
||||
Button(action: { modelContext.delete(item) }) {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct EmptyGroceryListView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
Text("You're all set for cooking 🍓")
|
||||
.font(.headline)
|
||||
Text("Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe.")
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Your grocery list is stored locally and therefore not synchronized across your devices.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.navigationTitle("Grocery List")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Grocery List Logic
|
||||
|
||||
|
||||
class GroceryRecipe: Identifiable, Codable {
|
||||
let name: String
|
||||
var items: [GroceryRecipeItem]
|
||||
|
||||
init(name: String, items: [GroceryRecipeItem]) {
|
||||
self.name = name
|
||||
self.items = items
|
||||
}
|
||||
|
||||
init(name: String, item: GroceryRecipeItem) {
|
||||
self.name = name
|
||||
self.items = [item]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class GroceryRecipeItem: Identifiable, Codable {
|
||||
let name: String
|
||||
var isChecked: Bool
|
||||
|
||||
init(_ name: String, isChecked: Bool = false) {
|
||||
self.name = name
|
||||
self.isChecked = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Observable class GroceryList {
|
||||
let dataStore: DataStore = DataStore()
|
||||
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))")
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func addItems(_ items: [String], toRecipe recipeId: String, recipeName: String? = nil) {
|
||||
for item in items {
|
||||
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||
print("Deleting item \(itemName)")
|
||||
guard let recipe = groceryDict[recipeId] else { return }
|
||||
guard let itemIndex = groceryDict[recipeId]?.items.firstIndex(where: { $0.name == itemName }) else { return }
|
||||
groceryDict[recipeId]?.items.remove(at: itemIndex)
|
||||
if groceryDict[recipeId]!.items.isEmpty {
|
||||
groceryDict.removeValue(forKey: recipeId)
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteGroceryRecipe(_ recipeId: String) {
|
||||
print("Deleting grocery recipe with id \(recipeId)")
|
||||
groceryDict.removeValue(forKey: recipeId)
|
||||
save()
|
||||
}
|
||||
|
||||
func deleteAll() {
|
||||
print("Deleting all grocery items")
|
||||
groceryDict = [:]
|
||||
save()
|
||||
}
|
||||
|
||||
func toggleItemChecked(_ groceryItem: GroceryRecipeItem) {
|
||||
print("Item checked: \(groceryItem.name)")
|
||||
groceryItem.isChecked.toggle()
|
||||
save()
|
||||
}
|
||||
|
||||
func containsItem(at recipeId: String, item: String) -> Bool {
|
||||
guard let recipe = groceryDict[recipeId] else { return false }
|
||||
if recipe.items.contains(where: { $0.name == item }) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsRecipe(_ recipeId: String) -> Bool {
|
||||
return groceryDict[recipeId] != nil
|
||||
}
|
||||
|
||||
func save() {
|
||||
Task {
|
||||
await dataStore.save(data: groceryDict, toPath: "grocery_list.data")
|
||||
}
|
||||
}
|
||||
|
||||
func load() async {
|
||||
do {
|
||||
guard let groceryDict: [String: GroceryRecipe] = try await dataStore.load(
|
||||
fromPath: "grocery_list.data"
|
||||
) else { return }
|
||||
self.groceryDict = groceryDict
|
||||
} catch {
|
||||
print("Unable to load grocery list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -7,88 +7,86 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
|
||||
/*
|
||||
struct RecipeTabView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var groceryList: GroceryList
|
||||
@EnvironmentObject var viewModel: RecipeTabView.ViewModel
|
||||
//@State var cookbookState: CookbookState = CookbookState()
|
||||
@Environment(\.modelContext) var modelContext
|
||||
@Query var recipes: [Recipe]
|
||||
@State var categories: [(String, Int)] = []
|
||||
@State private var selectedRecipe: Recipe?
|
||||
@State private var selectedCategory: String? = "*"
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List(selection: $viewModel.selectedCategory) {
|
||||
// Categories
|
||||
ForEach(appState.categories) { category in
|
||||
NavigationLink(value: category) {
|
||||
HStack(alignment: .center) {
|
||||
if viewModel.selectedCategory != nil &&
|
||||
category.name == viewModel.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)
|
||||
List(selection: $selectedCategory) {
|
||||
CategoryListItem(category: "All Recipes", count: recipes.count, isSelected: selectedCategory == "*")
|
||||
.tag("*") // Tag nil to select all recipes
|
||||
|
||||
Section("Categories") {
|
||||
ForEach(categories, id: \.0.self) { category in
|
||||
CategoryListItem(category: category.0, count: category.1, isSelected: selectedCategory == category.0)
|
||||
.tag(category.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Cookbooks")
|
||||
.toolbar {
|
||||
RecipeTabViewToolBar()
|
||||
}
|
||||
.navigationDestination(isPresented: $viewModel.presentSettingsView) {
|
||||
SettingsView()
|
||||
.environmentObject(appState)
|
||||
}
|
||||
.navigationDestination(isPresented: $viewModel.presentEditView) {
|
||||
RecipeView(viewModel: RecipeView.ViewModel())
|
||||
.environmentObject(appState)
|
||||
.environmentObject(groceryList)
|
||||
}
|
||||
.navigationTitle("Categories")
|
||||
} content: {
|
||||
RecipeListView(selectedCategory: $selectedCategory, selectedRecipe: $selectedRecipe)
|
||||
} detail: {
|
||||
NavigationStack {
|
||||
if let category = viewModel.selectedCategory {
|
||||
RecipeListView(
|
||||
categoryName: category.name,
|
||||
showEditView: $viewModel.presentEditView
|
||||
)
|
||||
.id(category.id) // Workaround: This is needed to update the detail view when the selection changes
|
||||
}
|
||||
|
||||
// Use a conditional view based on selection
|
||||
if let selectedRecipe {
|
||||
//RecipeDetailView(recipe: recipe) // Create a dedicated detail view
|
||||
RecipeView(recipe: selectedRecipe, viewModel: RecipeView.ViewModel(recipe: selectedRecipe))
|
||||
} else {
|
||||
ContentUnavailableView("Select a Recipe", systemImage: "fork.knife.circle")
|
||||
}
|
||||
}
|
||||
.tint(.nextcloudBlue)
|
||||
.task {
|
||||
let connection = await appState.checkServerConnection()
|
||||
DispatchQueue.main.async {
|
||||
viewModel.serverConnection = connection
|
||||
initCategories()
|
||||
return
|
||||
do {
|
||||
try modelContext.delete(model: Recipe.self)
|
||||
} catch {
|
||||
print("Failed to delete recipes and categories.")
|
||||
}
|
||||
|
||||
guard let categories = await CookbookApiV1.getCategories(auth: UserSettings.shared.authString).0 else { return }
|
||||
for category in categories {
|
||||
guard let recipeStubs = await CookbookApiV1.getCategory(auth: UserSettings.shared.authString, named: category.name).0 else { return }
|
||||
for recipeStub in recipeStubs {
|
||||
guard let recipe = await CookbookApiV1.getRecipe(auth: UserSettings.shared.authString, id: recipeStub.id).0 else { return }
|
||||
modelContext.insert(recipe)
|
||||
}
|
||||
}
|
||||
|
||||
}/*
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
Button(action: {
|
||||
//cookbookState.showSettings = true
|
||||
}) {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
func initCategories() {
|
||||
// Load Categories
|
||||
var categoryDict: [String: Int] = [:]
|
||||
for recipe in recipes {
|
||||
// Ensure "Uncategorized" is a valid category if used
|
||||
if !recipe.category.isEmpty {
|
||||
categoryDict[recipe.category, default: 0] += 1
|
||||
} else {
|
||||
categoryDict["Other", default: 0] += 1
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
let connection = await appState.checkServerConnection()
|
||||
DispatchQueue.main.async {
|
||||
viewModel.serverConnection = connection
|
||||
}
|
||||
await appState.getCategories()
|
||||
}
|
||||
categories = categoryDict.map {
|
||||
($0.key, $0.value)
|
||||
}.sorted { $0.0 < $1.0 }
|
||||
}
|
||||
|
||||
class ViewModel: ObservableObject {
|
||||
@@ -98,13 +96,40 @@ struct RecipeTabView: View {
|
||||
@Published var presentLoadingIndicator: Bool = false
|
||||
@Published var presentConnectionPopover: Bool = false
|
||||
@Published var serverConnection: Bool = false
|
||||
|
||||
@Published var selectedCategory: Category? = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fileprivate struct CategoryListItem: View {
|
||||
var category: String
|
||||
var count: Int
|
||||
var isSelected: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
if isSelected {
|
||||
Image(systemName: "book")
|
||||
} else {
|
||||
Image(systemName: "book.closed.fill")
|
||||
}
|
||||
|
||||
Text(category)
|
||||
.font(.system(size: 20, weight: .medium, design: .default))
|
||||
|
||||
Spacer()
|
||||
Text("\(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)
|
||||
}
|
||||
}
|
||||
/*
|
||||
fileprivate struct RecipeTabViewToolBar: ToolbarContent {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@EnvironmentObject var viewModel: RecipeTabView.ViewModel
|
||||
|
||||
234
Nextcloud Cookbook iOS Client/Views/Tabs/SettingsTabView.swift
Normal file
234
Nextcloud Cookbook iOS Client/Views/Tabs/SettingsTabView.swift
Normal file
@@ -0,0 +1,234 @@
|
||||
//
|
||||
// SettingsTabView.swift
|
||||
// Nextcloud Cookbook iOS Client
|
||||
//
|
||||
// Created by Vincent Meilinger on 29.05.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct SettingsTabView: View {
|
||||
@ObservedObject var userSettings = UserSettings.shared
|
||||
|
||||
@State private var avatarImage: UIImage?
|
||||
@State private var userData: UserData?
|
||||
|
||||
@State private var showAlert: Bool = false
|
||||
@State private var alertType: SettingsAlert = .NONE
|
||||
|
||||
@State private var presentLoginSheet: Bool = false
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
if userSettings.authString.isEmpty {
|
||||
HStack(alignment: .center) {
|
||||
if let avatarImage = avatarImage {
|
||||
Image(uiImage: avatarImage)
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
}
|
||||
if let userData = userData {
|
||||
VStack(alignment: .leading) {
|
||||
Text(userData.userDisplayName)
|
||||
.font(.title)
|
||||
.padding(.leading)
|
||||
Text("Username: \(userData.userId)")
|
||||
.font(.subheadline)
|
||||
.padding(.leading)
|
||||
|
||||
|
||||
// TODO: Add actions
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Button("Log out") {
|
||||
print("Log out.")
|
||||
alertType = .LOG_OUT
|
||||
showAlert = true
|
||||
}
|
||||
.tint(.red)
|
||||
} else {
|
||||
Button("Log in") {
|
||||
print("Log in.")
|
||||
presentLoginSheet.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} header: {
|
||||
Text("Nextcloud")
|
||||
} footer: {
|
||||
Text("Log in to your Nextcloud account to sync your recipes. This requires a Nextcloud server with the Nextcloud Cookbook application installed.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $userSettings.expandNutritionSection) {
|
||||
Text("Expand nutrition section")
|
||||
}
|
||||
Toggle(isOn: $userSettings.expandKeywordSection) {
|
||||
Text("Expand keyword section")
|
||||
}
|
||||
Toggle(isOn: $userSettings.expandInfoSection) {
|
||||
Text("Expand information section")
|
||||
}
|
||||
} header: {
|
||||
Text("Recipes")
|
||||
} footer: {
|
||||
Text("Configure which sections in your recipes are expanded by default.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $userSettings.keepScreenAwake) {
|
||||
Text("Keep screen awake when viewing recipes")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("Decimal number format")
|
||||
Spacer()
|
||||
Picker("", selection: $userSettings.decimalNumberSeparator) {
|
||||
Text("Point (e.g. 1.42)").tag(".")
|
||||
Text("Comma (e.g. 1,42)").tag(",")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
} footer: {
|
||||
Text("This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $userSettings.storeRecipes) {
|
||||
Text("Offline recipes")
|
||||
}
|
||||
Toggle(isOn: $userSettings.storeImages) {
|
||||
Text("Store recipe images locally")
|
||||
}
|
||||
Toggle(isOn: $userSettings.storeThumb) {
|
||||
Text("Store recipe thumbnails locally")
|
||||
}
|
||||
} header: {
|
||||
Text("Downloads")
|
||||
} footer: {
|
||||
Text("Configure what is stored on your device.")
|
||||
}
|
||||
|
||||
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: {
|
||||
Text("About")
|
||||
} footer: {
|
||||
Text("If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Link("Get support", destination: URL(string: "https://vincentmeilinger.github.io/Nextcloud-Cookbook-Client-Support/")!)
|
||||
} header: {
|
||||
Text("Support")
|
||||
} footer: {
|
||||
Text("If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information.")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Delete local data") {
|
||||
print("Clear cache.")
|
||||
alertType = .DELETE_CACHE
|
||||
showAlert = true
|
||||
}
|
||||
.tint(.red)
|
||||
|
||||
} header: {
|
||||
Text("Other")
|
||||
} footer: {
|
||||
Text("Deleting local data will not affect the recipe data stored on your server.")
|
||||
}
|
||||
|
||||
Section(header: Text("Acknowledgements")) {
|
||||
VStack(alignment: .leading) {
|
||||
if let url = URL(string: "https://github.com/scinfu/SwiftSoup") {
|
||||
Link("SwiftSoup", destination: url)
|
||||
.font(.headline)
|
||||
Text("An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites.")
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
if let url = URL(string: "https://github.com/techprimate/TPPDF") {
|
||||
Link("TPPDF", destination: url)
|
||||
.font(.headline)
|
||||
Text("A simple-to-use PDF builder for Swift. Used for generating recipe PDF documents.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigationTitle("Settings")
|
||||
.alert(alertType.getTitle(), isPresented: $showAlert) {
|
||||
Button("Cancel", role: .cancel) { }
|
||||
if alertType == .DELETE_CACHE {
|
||||
Button("Delete", role: .destructive) { deleteCachedData() }
|
||||
}
|
||||
} message: {
|
||||
Text(alertType.getMessage())
|
||||
}
|
||||
.task {
|
||||
await getUserData()
|
||||
}
|
||||
.sheet(isPresented: $presentLoginSheet, onDismiss: {}) {
|
||||
V2LoginView()
|
||||
}
|
||||
}
|
||||
|
||||
func getUserData() async {
|
||||
let (data, _) = await NextcloudApi.getAvatar()
|
||||
let (userData, _) = await NextcloudApi.getHoverCard()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.avatarImage = data
|
||||
self.userData = userData
|
||||
}
|
||||
}
|
||||
|
||||
func deleteCachedData() {
|
||||
print("TODO: Delete cached data\n")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user