253 lines
9.9 KiB
Swift
253 lines
9.9 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
import EventKit
|
|
|
|
struct SettingsView: View {
|
|
@Environment(\.modelContext) private var context
|
|
@Query private var allSettings: [AppSettings]
|
|
@StateObject private var viewModel = SettingsViewModel()
|
|
@State private var showResetAllDataAlert = false
|
|
|
|
private var settings: AppSettings? { allSettings.first }
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.mealMoodBackground.ignoresSafeArea()
|
|
|
|
if let settings = settings {
|
|
settingsContent(settings: settings)
|
|
}
|
|
}
|
|
.navigationTitle("settings_title")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbarBackground(Color.mealMoodBackground, for: .navigationBar)
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
|
.environment(\.isEnabled, true)
|
|
.toast(isShowing: $viewModel.showToast, message: viewModel.toastMessage)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func settingsContent(settings: AppSettings) -> some View {
|
|
List {
|
|
// Planning section
|
|
Section {
|
|
Picker("settings_meal_windows", selection: Binding(
|
|
get: { settings.mealWindowsEnum },
|
|
set: { settings.mealWindowsEnum = $0 }
|
|
)) {
|
|
Text("meal_windows_dinner_only").tag(MealWindows.dinnerOnly)
|
|
Text("meal_windows_lunch_only").tag(MealWindows.lunchOnly)
|
|
Text("meal_windows_both").tag(MealWindows.both)
|
|
}
|
|
|
|
Toggle("settings_include_weekends", isOn: Binding(
|
|
get: { settings.includeWeekends },
|
|
set: { settings.includeWeekends = $0 }
|
|
))
|
|
.tint(.mealMoodCoral)
|
|
} header: {
|
|
Label("settings_planning", systemImage: "fork.knife")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
// Calendar section
|
|
Section {
|
|
Toggle("settings_icloud_sync", isOn: Binding(
|
|
get: { settings.iCloudSyncEnabledResolved },
|
|
set: { settings.iCloudSyncEnabledResolved = $0 }
|
|
))
|
|
.tint(.mealMoodCoral)
|
|
|
|
Toggle("settings_sync", isOn: Binding(
|
|
get: { settings.syncEnabled },
|
|
set: { newValue in
|
|
if newValue {
|
|
Task {
|
|
let granted = await viewModel.requestCalendarAccess()
|
|
if granted {
|
|
let hasCalendar = viewModel.ensureValidCalendarSelection(settings: settings)
|
|
settings.syncEnabled = hasCalendar
|
|
}
|
|
}
|
|
} else {
|
|
settings.syncEnabled = false
|
|
}
|
|
}
|
|
))
|
|
.tint(.mealMoodCoral)
|
|
|
|
if settings.syncEnabled {
|
|
Picker("settings_sync_mode", selection: Binding(
|
|
get: { settings.syncModeEnum },
|
|
set: { settings.syncModeEnum = $0 }
|
|
)) {
|
|
ForEach(CalendarSyncMode.allCases, id: \.self) { mode in
|
|
Text(LocalizedStringKey(mode.localizedKey)).tag(mode)
|
|
}
|
|
}
|
|
|
|
if !viewModel.availableCalendars.isEmpty {
|
|
Picker("calendar_select", selection: Binding(
|
|
get: { settings.calendarId },
|
|
set: { settings.calendarId = $0 }
|
|
)) {
|
|
Text("calendar_select").tag(nil as String?)
|
|
ForEach(viewModel.availableCalendars, id: \.calendarIdentifier) { cal in
|
|
Text(cal.title).tag(cal.calendarIdentifier as String?)
|
|
}
|
|
}
|
|
}
|
|
|
|
DatePicker("settings_lunch_time", selection: Binding(
|
|
get: { settings.lunchTime },
|
|
set: { settings.lunchTime = $0 }
|
|
), displayedComponents: .hourAndMinute)
|
|
|
|
DatePicker("settings_dinner_time", selection: Binding(
|
|
get: { settings.dinnerTime },
|
|
set: { settings.dinnerTime = $0 }
|
|
), displayedComponents: .hourAndMinute)
|
|
|
|
Picker("settings_event_duration", selection: Binding(
|
|
get: { settings.eventDuration },
|
|
set: { settings.eventDuration = $0 }
|
|
)) {
|
|
Text("duration_30").tag(30)
|
|
Text("duration_60").tag(60)
|
|
Text("duration_90").tag(90)
|
|
Text("duration_120").tag(120)
|
|
}
|
|
|
|
TextField("settings_event_prefix", text: Binding(
|
|
get: { settings.eventPrefix },
|
|
set: { settings.eventPrefix = $0 }
|
|
))
|
|
|
|
Picker("settings_reminder", selection: Binding(
|
|
get: { settings.reminderMinutesBefore },
|
|
set: { settings.reminderMinutesBefore = $0 }
|
|
)) {
|
|
Text("settings_reminder_none").tag(nil as Int?)
|
|
Text("reminder_30").tag(30 as Int?)
|
|
Text("reminder_60").tag(60 as Int?)
|
|
Text("reminder_120").tag(120 as Int?)
|
|
}
|
|
}
|
|
} header: {
|
|
Label("settings_calendar", systemImage: "calendar")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
// Tags section
|
|
Section {
|
|
NavigationLink(destination: TagListView()) {
|
|
Text("settings_manage_tags")
|
|
}
|
|
} header: {
|
|
Label("settings_tags", systemImage: "tag")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
// Language section
|
|
Section {
|
|
Picker("settings_language", selection: Binding(
|
|
get: { settings.languageEnum },
|
|
set: { settings.languageEnum = $0 }
|
|
)) {
|
|
ForEach(AppLanguage.allCases, id: \.self) { lang in
|
|
Text(lang.displayName).tag(lang)
|
|
}
|
|
}
|
|
} header: {
|
|
Label("settings_language", systemImage: "globe")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
// Premium section
|
|
Section {
|
|
HStack {
|
|
Text("settings_premium_status")
|
|
Spacer()
|
|
Text(settings.isPremium ? "Premium" : "Free")
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
}
|
|
|
|
NavigationLink(destination: PremiumView(settings: settings)) {
|
|
Text("settings_remove_ads")
|
|
}
|
|
} header: {
|
|
Label("settings_premium", systemImage: "star")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
Section {
|
|
Button {
|
|
ReviewPromptService.shared.requestFromSettings()
|
|
} label: {
|
|
Label("settings_rate_app", systemImage: "star.bubble")
|
|
}
|
|
|
|
Link(destination: URL(string: "https://mealmood.app")!) {
|
|
Label("settings_website", systemImage: "safari")
|
|
}
|
|
|
|
Link(destination: URL(string: "mailto:support@mealmood.app")!) {
|
|
Label("settings_support", systemImage: "envelope")
|
|
}
|
|
|
|
HStack {
|
|
Text("settings_app_version")
|
|
Spacer()
|
|
Text(viewModel.appVersion)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
}
|
|
|
|
HStack {
|
|
Text("settings_app_build")
|
|
Spacer()
|
|
Text(viewModel.appBuild)
|
|
.foregroundColor(.mealMoodTextSecondary)
|
|
}
|
|
} header: {
|
|
Label("settings_about", systemImage: "info.circle")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
|
|
Section {
|
|
Button(role: .destructive) {
|
|
showResetAllDataAlert = true
|
|
} label: {
|
|
Label("settings_reset_all_data", systemImage: "trash")
|
|
}
|
|
} header: {
|
|
Label("settings_danger_zone", systemImage: "exclamationmark.triangle")
|
|
}
|
|
.listRowBackground(Color.mealMoodSurface)
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.listStyle(.insetGrouped)
|
|
.foregroundColor(.mealMoodTextPrimary)
|
|
.tint(.mealMoodCoral)
|
|
.onAppear {
|
|
if settings.syncEnabled {
|
|
let hasCalendar = viewModel.ensureValidCalendarSelection(settings: settings)
|
|
settings.syncEnabled = hasCalendar
|
|
}
|
|
}
|
|
.alert("calendar_permission_title", isPresented: $viewModel.showCalendarPermissionAlert) {
|
|
Button("calendar_permission_settings") { viewModel.openSystemSettings() }
|
|
Button("reset_cancel", role: .cancel) {}
|
|
} message: {
|
|
Text("calendar_permission_message")
|
|
}
|
|
.alert("settings_reset_all_data_title", isPresented: $showResetAllDataAlert) {
|
|
Button("reset_cancel", role: .cancel) {}
|
|
Button("settings_reset_all_data_confirm", role: .destructive) {
|
|
viewModel.resetAllData(context: context)
|
|
}
|
|
} message: {
|
|
Text("settings_reset_all_data_message")
|
|
}
|
|
}
|
|
}
|