Add user-facing appearance setting (System/Light/Dark) wired via preferredColorScheme at the app root. Replace hardcoded .black tints and foreground styles with .primary so toolbar buttons and text remain visible in dark mode. Remove profile picture from settings and SwiftSoup from acknowledgements. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
281 lines
11 KiB
Swift
281 lines
11 KiB
Swift
//
|
|
// SettingsView.swift
|
|
// Nextcloud Cookbook iOS Client
|
|
//
|
|
// Created by Vincent Meilinger on 15.09.23.
|
|
//
|
|
|
|
import EventKit
|
|
import Foundation
|
|
import OSLog
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@EnvironmentObject var groceryListManager: GroceryListManager
|
|
@ObservedObject var userSettings = UserSettings.shared
|
|
@StateObject var viewModel = ViewModel()
|
|
@State private var reminderLists: [EKCalendar] = []
|
|
@State private var remindersPermission: EKAuthorizationStatus = .notDetermined
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Picker("Select a default cookbook", selection: $userSettings.defaultCategory) {
|
|
Text("None").tag("None")
|
|
ForEach(appState.categories, id: \.name) { category in
|
|
Text(category.name == "*" ? "Other" : category.name).tag(category)
|
|
}
|
|
}
|
|
} header: {
|
|
Text("General")
|
|
} footer: {
|
|
Text("The selected cookbook will open on app launch by default.")
|
|
}
|
|
|
|
Section {
|
|
Picker("Appearance", selection: $userSettings.appearanceMode) {
|
|
ForEach(AppearanceMode.allValues, id: \.self) { mode in
|
|
Text(mode.descriptor()).tag(mode.rawValue)
|
|
}
|
|
}
|
|
} footer: {
|
|
Text("Choose whether the app follows the system appearance or always uses light or dark mode.")
|
|
}
|
|
|
|
Section {
|
|
Picker("Grocery list storage", selection: $userSettings.groceryListMode) {
|
|
ForEach(GroceryListMode.allValues, id: \.self) { mode in
|
|
Text(mode.descriptor()).tag(mode.rawValue)
|
|
}
|
|
}
|
|
|
|
if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
|
|
if remindersPermission == .notDetermined {
|
|
Button("Grant Reminders Access") {
|
|
Task {
|
|
let granted = await groceryListManager.requestRemindersAccess()
|
|
remindersPermission = groceryListManager.remindersPermissionStatus
|
|
if granted {
|
|
reminderLists = groceryListManager.availableReminderLists()
|
|
}
|
|
}
|
|
}
|
|
} else if remindersPermission == .denied || remindersPermission == .restricted {
|
|
Text("Reminders access was denied. Please enable it in System Settings to use this feature.")
|
|
.foregroundStyle(.secondary)
|
|
Button("Open Settings") {
|
|
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
UIApplication.shared.open(url)
|
|
}
|
|
}
|
|
} else if remindersPermission == .fullAccess {
|
|
Picker("Reminders list", selection: $userSettings.remindersListIdentifier) {
|
|
ForEach(reminderLists, id: \.calendarIdentifier) { list in
|
|
Text(list.title).tag(list.calendarIdentifier)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Toggle(isOn: $userSettings.grocerySyncEnabled) {
|
|
Text("Sync grocery list across devices")
|
|
}
|
|
} header: {
|
|
Text("Grocery List")
|
|
} footer: {
|
|
if userSettings.grocerySyncEnabled {
|
|
Text("Grocery list state is synced via your Nextcloud server by storing it alongside recipe data.")
|
|
} else if userSettings.groceryListMode == GroceryListMode.appleReminders.rawValue {
|
|
Text("Grocery items will be saved to Apple Reminders. The Grocery List tab will be hidden since you can manage items directly in the Reminders app.")
|
|
} else {
|
|
Text("Grocery items are stored locally on this device.")
|
|
}
|
|
}
|
|
|
|
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("Log out") {
|
|
Logger.view.debug("Log out.")
|
|
viewModel.alertType = .LOG_OUT
|
|
viewModel.showAlert = true
|
|
|
|
}
|
|
.tint(.red)
|
|
|
|
Button("Delete local data") {
|
|
Logger.view.debug("Clear cache.")
|
|
viewModel.alertType = .DELETE_CACHE
|
|
viewModel.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/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(viewModel.alertType.getTitle(), isPresented: $viewModel.showAlert) {
|
|
Button("Cancel", role: .cancel) { }
|
|
if viewModel.alertType == .LOG_OUT {
|
|
Button("Log out", role: .destructive) { logOut() }
|
|
} else if viewModel.alertType == .DELETE_CACHE {
|
|
Button("Delete", role: .destructive) { deleteCache() }
|
|
}
|
|
} message: {
|
|
Text(viewModel.alertType.getMessage())
|
|
}
|
|
.task {
|
|
remindersPermission = groceryListManager.remindersPermissionStatus
|
|
if remindersPermission == .fullAccess {
|
|
reminderLists = groceryListManager.availableReminderLists()
|
|
}
|
|
}
|
|
.onChange(of: userSettings.groceryListMode) { _, _ in
|
|
remindersPermission = groceryListManager.remindersPermissionStatus
|
|
if remindersPermission == .fullAccess {
|
|
reminderLists = groceryListManager.availableReminderLists()
|
|
}
|
|
}
|
|
}
|
|
|
|
func logOut() {
|
|
userSettings.serverAddress = ""
|
|
userSettings.username = ""
|
|
userSettings.token = ""
|
|
userSettings.authString = ""
|
|
appState.deleteAllData()
|
|
userSettings.onboarding = true
|
|
}
|
|
|
|
func deleteCache() {
|
|
appState.deleteAllData()
|
|
}
|
|
}
|
|
|
|
extension SettingsView {
|
|
class ViewModel: ObservableObject {
|
|
@Published var showAlert: Bool = false
|
|
fileprivate var alertType: SettingsAlert = .NONE
|
|
|
|
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 ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|