- Phase 0: project.yml fixes (CODE_SIGN_ENTITLEMENTS confirmed) - Phase 1: Enhanced models (HabitModels, TaskModels, FinanceModels, SavingsModels, UserModels) - Phase 1: Enhanced APIService with all endpoints (habits/log/stats, tasks/uncomplete, finance/analytics, savings/*) - Phase 2: DashboardView rewrite - day progress bar, 4 stat cards, habit/task lists with Undo (3 sec) - Phase 3: TrackerView - HabitListView (streak badge, swipe delete, archive), TaskListView (priority, overdue), StatisticsView (heatmap 84 days, line chart, bar chart via Swift Charts) - Phase 4: FinanceView rewrite - month picker, summary card, top expenses progress bars, pie chart, line chart, transactions by day, analytics tab with bar chart + month comparison - Phase 5: SavingsView rewrite - overview with overdue block, categories tab with type icons, operations tab with category filter + add sheet - Phase 6: SettingsView - dark/light theme, profile edit, telegram chat id, notifications toggle + time, timezone picker, logout - Added: AddHabitView with weekly day selector + interval days - Added: AddTaskView with icon/color/due date picker - Haptic feedback on all toggle actions
294 lines
14 KiB
Swift
294 lines
14 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - SettingsView
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject var authManager: AuthManager
|
|
@AppStorage("colorScheme") private var colorSchemeRaw: String = "dark"
|
|
@State private var profile: UserProfile?
|
|
@State private var isLoading = true
|
|
@State private var isSaving = false
|
|
@State private var showPasswordChange = false
|
|
|
|
// Profile fields
|
|
@State private var telegramChatId = ""
|
|
@State private var morningNotification = true
|
|
@State private var eveningNotification = true
|
|
@State private var morningTime = "09:00"
|
|
@State private var eveningTime = "21:00"
|
|
@State private var timezone = "Europe/Moscow"
|
|
@State private var username = ""
|
|
|
|
var isDark: Bool { colorSchemeRaw != "light" }
|
|
|
|
let timezones = [
|
|
"Europe/Moscow", "Europe/Kaliningrad", "Europe/Samara", "Asia/Yekaterinburg",
|
|
"Asia/Omsk", "Asia/Krasnoyarsk", "Asia/Irkutsk", "Asia/Yakutsk",
|
|
"Asia/Vladivostok", "Asia/Magadan", "Asia/Kamchatka",
|
|
"UTC", "Europe/London", "Europe/Berlin", "Europe/Paris", "America/New_York"
|
|
]
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color(hex: "0a0a1a").ignoresSafeArea()
|
|
ScrollView {
|
|
VStack(spacing: 0) {
|
|
// Header / Avatar
|
|
VStack(spacing: 12) {
|
|
ZStack {
|
|
Circle().fill(Color(hex: "0D9488").opacity(0.2)).frame(width: 80, height: 80)
|
|
Text(String(authManager.userName.prefix(1)).uppercased())
|
|
.font(.largeTitle.bold()).foregroundColor(Color(hex: "0D9488"))
|
|
}
|
|
Text(authManager.userName).font(.title2.bold()).foregroundColor(.white)
|
|
Text("ID: \(authManager.userId)").font(.caption).foregroundColor(Color(hex: "8888aa"))
|
|
}
|
|
.padding(.top, 24)
|
|
.padding(.bottom, 28)
|
|
|
|
if isLoading {
|
|
ProgressView().tint(Color(hex: "0D9488")).padding(.top, 20)
|
|
} else {
|
|
VStack(spacing: 20) {
|
|
// MARK: Appearance
|
|
SettingsSection(title: "Внешний вид") {
|
|
SettingsToggle(icon: "moon.fill", title: isDark ? "Тёмная тема" : "Светлая тема", color: "6366f1", isOn: isDark) {
|
|
colorSchemeRaw = isDark ? "light" : "dark"
|
|
}
|
|
}
|
|
|
|
// MARK: Profile
|
|
SettingsSection(title: "Профиль") {
|
|
VStack(spacing: 8) {
|
|
Label("Имя пользователя", systemImage: "person.fill")
|
|
.font(.caption).foregroundColor(Color(hex: "8888aa"))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
TextField("Username", text: $username)
|
|
.foregroundColor(.white).padding(14)
|
|
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07)))
|
|
}
|
|
.padding(.horizontal, 4)
|
|
|
|
SettingsButton(icon: "lock.fill", title: "Сменить пароль", color: "7c3aed") {
|
|
showPasswordChange = true
|
|
}
|
|
}
|
|
|
|
// MARK: Telegram
|
|
SettingsSection(title: "Telegram Бот") {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Label("Chat ID", systemImage: "paperplane.fill")
|
|
.font(.caption).foregroundColor(Color(hex: "8888aa"))
|
|
TextField("Например: 123456789", text: $telegramChatId)
|
|
.keyboardType(.numbersAndPunctuation)
|
|
.foregroundColor(.white).padding(14)
|
|
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07)))
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "info.circle").font(.caption2).foregroundColor(Color(hex: "0D9488"))
|
|
Text("Напишите /start боту @pulse_tracking_bot, чтобы получить Chat ID")
|
|
.font(.caption2).foregroundColor(Color(hex: "8888aa"))
|
|
}
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
|
|
// MARK: Notifications
|
|
SettingsSection(title: "Уведомления") {
|
|
VStack(spacing: 12) {
|
|
SettingsToggle(icon: "sunrise.fill", title: "Утренние уведомления", color: "ffa502", isOn: morningNotification) {
|
|
morningNotification.toggle()
|
|
}
|
|
if morningNotification {
|
|
HStack {
|
|
Text("Время").font(.callout).foregroundColor(Color(hex: "8888aa"))
|
|
Spacer()
|
|
TextField("09:00", text: $morningTime)
|
|
.keyboardType(.numbersAndPunctuation)
|
|
.foregroundColor(.white)
|
|
.multilineTextAlignment(.trailing)
|
|
.frame(width: 60)
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
|
|
Divider().background(Color.white.opacity(0.08))
|
|
|
|
SettingsToggle(icon: "moon.stars.fill", title: "Вечерние уведомления", color: "6366f1", isOn: eveningNotification) {
|
|
eveningNotification.toggle()
|
|
}
|
|
if eveningNotification {
|
|
HStack {
|
|
Text("Время").font(.callout).foregroundColor(Color(hex: "8888aa"))
|
|
Spacer()
|
|
TextField("21:00", text: $eveningTime)
|
|
.keyboardType(.numbersAndPunctuation)
|
|
.foregroundColor(.white)
|
|
.multilineTextAlignment(.trailing)
|
|
.frame(width: 60)
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Timezone
|
|
SettingsSection(title: "Часовой пояс") {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Label("Выберите часовой пояс", systemImage: "clock.fill")
|
|
.font(.caption).foregroundColor(Color(hex: "8888aa"))
|
|
Picker("Часовой пояс", selection: $timezone) {
|
|
ForEach(timezones, id: \.self) { tz in Text(tz).tag(tz) }
|
|
}
|
|
.pickerStyle(.wheel)
|
|
.frame(height: 120)
|
|
.clipped()
|
|
.background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.04)))
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
|
|
// MARK: Save Button
|
|
Button(action: { Task { await saveProfile() } }) {
|
|
HStack {
|
|
if isSaving { ProgressView().tint(.black).scaleEffect(0.8) }
|
|
else { Text("Сохранить изменения").font(.headline).foregroundColor(.black) }
|
|
}
|
|
.frame(maxWidth: .infinity).padding()
|
|
.background(LinearGradient(colors: [Color(hex: "0D9488"), Color(hex: "14b8a6")], startPoint: .leading, endPoint: .trailing))
|
|
.cornerRadius(14)
|
|
}
|
|
.disabled(isSaving)
|
|
.padding(.horizontal)
|
|
|
|
// MARK: Logout
|
|
Button(action: { authManager.logout() }) {
|
|
HStack {
|
|
Image(systemName: "rectangle.portrait.and.arrow.right")
|
|
Text("Выйти из аккаунта")
|
|
}
|
|
.font(.callout.bold())
|
|
.foregroundColor(Color(hex: "ff4757"))
|
|
.frame(maxWidth: .infinity).padding()
|
|
.background(RoundedRectangle(cornerRadius: 14).fill(Color(hex: "ff4757").opacity(0.1)))
|
|
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "ff4757").opacity(0.3), lineWidth: 1))
|
|
}
|
|
.padding(.horizontal)
|
|
|
|
Text("Pulse v1.1 • Made with ❤️").font(.caption).foregroundColor(Color(hex: "8888aa"))
|
|
.padding(.bottom, 20)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.task { await loadProfile() }
|
|
.sheet(isPresented: $showPasswordChange) {
|
|
ChangePasswordView(isPresented: $showPasswordChange)
|
|
.presentationDetents([.medium])
|
|
.presentationDragIndicator(.visible)
|
|
.presentationBackground(Color(hex: "0a0a1a"))
|
|
}
|
|
}
|
|
|
|
func loadProfile() async {
|
|
isLoading = true
|
|
username = authManager.userName
|
|
if let p = try? await APIService.shared.getProfile(token: authManager.token) {
|
|
profile = p
|
|
telegramChatId = p.telegramChatId ?? ""
|
|
morningNotification = p.morningNotification ?? true
|
|
eveningNotification = p.eveningNotification ?? true
|
|
morningTime = p.morningTime ?? "09:00"
|
|
eveningTime = p.eveningTime ?? "21:00"
|
|
timezone = p.timezone ?? "Europe/Moscow"
|
|
}
|
|
isLoading = false
|
|
}
|
|
|
|
func saveProfile() async {
|
|
isSaving = true
|
|
let req = UpdateProfileRequest(
|
|
telegramChatId: telegramChatId.isEmpty ? nil : telegramChatId,
|
|
morningNotification: morningNotification,
|
|
eveningNotification: eveningNotification,
|
|
morningTime: morningTime,
|
|
eveningTime: eveningTime,
|
|
timezone: timezone
|
|
)
|
|
_ = try? await APIService.shared.updateProfile(token: authManager.token, request: req)
|
|
// Update username if changed
|
|
if username != authManager.userName {
|
|
var req2 = URLRequest(url: URL(string: "https://api.digital-home.site/auth/me")!)
|
|
req2.httpMethod = "PUT"
|
|
req2.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req2.setValue("Bearer \(authManager.token)", forHTTPHeaderField: "Authorization")
|
|
req2.httpBody = try? JSONEncoder().encode(["username": username])
|
|
_ = try? await URLSession.shared.data(for: req2)
|
|
await MainActor.run { authManager.userName = username; UserDefaults.standard.set(username, forKey: "userName") }
|
|
}
|
|
isSaving = false
|
|
}
|
|
}
|
|
|
|
// MARK: - SettingsSection
|
|
|
|
struct SettingsSection<Content: View>: View {
|
|
let title: String
|
|
@ViewBuilder let content: () -> Content
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text(title).font(.subheadline.bold()).foregroundColor(Color(hex: "8888aa")).padding(.horizontal)
|
|
VStack(spacing: 8) {
|
|
content()
|
|
}
|
|
.padding(16)
|
|
.background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.04)))
|
|
.padding(.horizontal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SettingsToggle
|
|
|
|
struct SettingsToggle: View {
|
|
let icon: String
|
|
let title: String
|
|
let color: String
|
|
let isOn: Bool
|
|
let onToggle: () -> Void
|
|
var body: some View {
|
|
HStack(spacing: 14) {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 8).fill(Color(hex: color).opacity(0.2)).frame(width: 36, height: 36)
|
|
Image(systemName: icon).foregroundColor(Color(hex: color)).font(.subheadline)
|
|
}
|
|
Text(title).font(.callout).foregroundColor(.white)
|
|
Spacer()
|
|
Toggle("", isOn: Binding(get: { isOn }, set: { _ in onToggle() }))
|
|
.tint(Color(hex: "0D9488"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SettingsButton
|
|
|
|
struct SettingsButton: View {
|
|
let icon: String
|
|
let title: String
|
|
let color: String
|
|
let action: () -> Void
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: 14) {
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 8).fill(Color(hex: color).opacity(0.2)).frame(width: 36, height: 36)
|
|
Image(systemName: icon).foregroundColor(Color(hex: color)).font(.subheadline)
|
|
}
|
|
Text(title).font(.callout).foregroundColor(.white)
|
|
Spacer()
|
|
Image(systemName: "chevron.right").foregroundColor(Color(hex: "8888aa")).font(.caption)
|
|
}
|
|
}
|
|
}
|
|
}
|