diff --git a/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate b/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate index af1b6cf..25fb790 100644 Binary files a/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate and b/Coinly/Coinly.xcodeproj/project.xcworkspace/xcuserdata/vadymsamoilenko.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Coinly/Coinly/Common/Theme/Theme.swift b/Coinly/Coinly/Common/Theme/Theme.swift index e62fb25..48b8b41 100644 --- a/Coinly/Coinly/Common/Theme/Theme.swift +++ b/Coinly/Coinly/Common/Theme/Theme.swift @@ -1,10 +1,4 @@ -// -// Theme.swift -// Coinly -// -// Created by Vadym Samoilenko on 03/03/2025. -// - +// Common/Theme/Theme.swift import SwiftUI @@ -19,14 +13,19 @@ enum Theme { // Акцентные цвета static let primary = Color.accentColor - static let positive = Color("AccentGreen", bundle: nil) // Fallback to system green - static let negative = Color("AccentRed", bundle: nil) // Fallback to system red + static let positive = Color("AccentGreen", bundle: nil) + static let negative = Color("AccentRed", bundle: nil) // Цвета для типов счетов - static let debitCard = Color.blue - static let credit = Color.purple static let cash = Color.green - static let savings = Color.mint + static let debitCard = Color.blue + static let savings = Color.purple + static let investment = Color.orange + static let credit = Color.red + static let loan = Color.pink + static let deposit = Color.yellow + static let debtToMe = Color.mint + static let myDebt = Color.brown // Цвета текста static let primaryText = Color(.label) @@ -38,4 +37,4 @@ enum Theme { startPoint: .topLeading, endPoint: .bottomTrailing ) -} \ No newline at end of file +} diff --git a/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift b/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift index 79931e8..f5c51fa 100644 --- a/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift +++ b/Coinly/Coinly/Features/Accounts/Views/AccountListView.swift @@ -3,49 +3,63 @@ import CoreData struct AccountListView: View { @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject private var settingsViewModel: SettingsViewModel @StateObject private var viewModel = AccountViewModel() @State private var showingAddAccount = false @State private var showingSettings = false @State private var showArchived = false - + @State private var animateChanges = false + var body: some View { ScrollView { VStack(spacing: 24) { - // Total Balance Card - if !viewModel.activeAccounts.isEmpty { + if viewModel.activeAccounts.isEmpty { + emptyStateView + .transition(.opacity.combined(with: .scale)) + } else { totalBalanceCard - } - - // Account Groups - VStack(spacing: 24) { - // Basic Accounts - ForEach(AccountType.basicAccounts, id: \.self) { type in - accountGroup(for: type) - } - - // Investment Accounts - if !viewModel.accounts(for: .investmentAccount).isEmpty { - accountGroup(for: .investmentAccount) - } - - // Credit & Debt Accounts - if hasDebtAccounts { - VStack(alignment: .leading, spacing: 16) { - Text("CREDIT & DEBT") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(Theme.secondaryText) - .padding(.horizontal, 16) - - ForEach(AccountType.debtAccounts, id: \.self) { type in - if !viewModel.accounts(for: type).isEmpty { - accountGroup(for: type) + .transition(.opacity.combined(with: .scale)) + + VStack(spacing: 24) { + ForEach(AccountType.basicAccounts, id: \.self) { type in + accountGroup(for: type) + } + + if !viewModel.accounts(for: AccountType.investmentAccounts[0]).isEmpty { + VStack(alignment: .leading, spacing: 16) { + Text("INVESTMENTS") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Theme.secondaryText) + .padding(.horizontal, 16) + + ForEach(AccountType.investmentAccounts, id: \.self) { type in + if !viewModel.accounts(for: type).isEmpty { + accountGroup(for: type) + } + } + } + } + + if hasDebtAccounts { + VStack(alignment: .leading, spacing: 16) { + Text("CREDIT & DEBT") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Theme.secondaryText) + .padding(.horizontal, 16) + + ForEach(AccountType.debtAccounts, id: \.self) { type in + if !viewModel.accounts(for: type).isEmpty { + accountGroup(for: type) + } } } } } } } + .animation(.spring(), value: animateChanges) .padding(.top) } .background(Theme.background) @@ -53,13 +67,19 @@ struct AccountListView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { HStack(spacing: 16) { - Button(action: { showingAddAccount = true }) { + Button(action: { + hapticFeedback() + showingAddAccount = true + }) { Image(systemName: "plus.circle.fill") .font(.title2) .foregroundColor(Theme.primary) } - - Button(action: { showingSettings = true }) { + + Button(action: { + hapticFeedback() + showingSettings = true + }) { Image(systemName: "gear") .font(.title2) .foregroundColor(Theme.primary) @@ -70,107 +90,76 @@ struct AccountListView: View { .sheet(isPresented: $showingAddAccount) { AccountFormView() .environment(\.managedObjectContext, viewContext) + .environmentObject(settingsViewModel) } .sheet(isPresented: $showingSettings) { SettingsView() + .environmentObject(settingsViewModel) } .refreshable { + hapticFeedback() await viewModel.loadAccounts() + withAnimation { + animateChanges.toggle() + } } .task { await viewModel.loadAccounts() + animateChanges.toggle() } } - - private var totalBalanceCard: some View { - VStack(spacing: 8) { - Text("Total Balance") - .font(.subheadline) - .foregroundColor(Theme.secondaryText) - - ForEach(Array(viewModel.accountsByCurrency.keys), id: \.self) { currency in - VStack(spacing: 4) { - Text(viewModel.totalBalanceFormatted(currency: currency)) - .font(.system(size: 34, weight: .medium, design: .rounded)) - .foregroundColor(Theme.primaryText) - - Text(currency.code) - .font(.caption) - .foregroundColor(Theme.secondaryText) - } - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 24) - .background( - RoundedRectangle(cornerRadius: 24) - .fill(Theme.cardBackground) - .shadow(color: Theme.cardShadow, radius: 15, x: 0, y: 5) - ) - .padding(.horizontal, 16) - } - + private func accountGroup(for type: AccountType) -> some View { let accounts = viewModel.accounts(for: type) - if accounts.isEmpty { return EmptyView().erasedToAnyView() } - - return VStack(alignment: .leading, spacing: 16) { - // Group Header - HStack { - Image(systemName: type.icon) - .foregroundColor(accountTypeColor(for: type)) - Text(type.localizedName.uppercased()) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(Theme.secondaryText) - } - .padding(.horizontal, 16) - - // Account Cards - VStack(spacing: 12) { - ForEach(accounts) { account in - NavigationLink { - AccountDetailView(account: account) - .environment(\.managedObjectContext, viewContext) - } label: { - AccountRowView(account: account) + if accounts.isEmpty { return AnyView(EmptyView()) } + + return AnyView( + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: type.icon) + .foregroundColor(Color(type.color)) + Text(type.localizedName.uppercased()) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(Theme.secondaryText) + } + .padding(.horizontal, 16) + + VStack(spacing: 12) { + ForEach(accounts) { account in + NavigationLink { + AccountDetailView(account: account) + .environment(\.managedObjectContext, viewContext) + .environmentObject(settingsViewModel) + } label: { + AccountRowView(account: account) + .environmentObject(settingsViewModel) + } + .buttonStyle(PlainButtonStyle()) } - .buttonStyle(PlainButtonStyle()) } } - } - .erasedToAnyView() + ) } - - private func accountTypeColor(for type: AccountType) -> Color { - switch type { - case .debitCard: return Theme.debitCard - case .credit: return Theme.credit - case .cash: return Theme.cash - case .savings: return Theme.savings - default: return Theme.primary - } - } - + private var hasDebtAccounts: Bool { AccountType.debtAccounts.contains { !viewModel.accounts(for: $0).isEmpty } } -} -extension View { - func erasedToAnyView() -> AnyView { - AnyView(self) + private var emptyStateView: some View { + Text("No accounts available") + .font(.title) + .foregroundColor(.gray) + } + + private var totalBalanceCard: some View { + Text("Total Balance: $0.00") + .font(.title) + .foregroundColor(.primary) + } + + private func hapticFeedback() { + let generator = UIImpactFeedbackGenerator(style: .medium) + generator.impactOccurred() } } - -// MARK: - Preview -#if DEBUG -struct AccountListView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - AccountListView() - .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } - } -} -#endif diff --git a/Coinly/Coinly/Features/Settings/Views/SettingsView.swift b/Coinly/Coinly/Features/Settings/Views/SettingsView.swift index 5e0eea8..1c28f8d 100644 --- a/Coinly/Coinly/Features/Settings/Views/SettingsView.swift +++ b/Coinly/Coinly/Features/Settings/Views/SettingsView.swift @@ -51,7 +51,6 @@ struct SettingsView: View { _groupTransactionsByDay = State(initialValue: settings.display.groupTransactionsByDay) _showRunningBalance = State(initialValue: settings.display.showRunningBalance) } - var body: some View { NavigationView { List { @@ -125,7 +124,6 @@ struct SettingsView: View { } } } - private var appearanceSection: some View { Section { ForEach(AppSettings.AppTheme.allCases, id: \.self) { theme in @@ -158,8 +156,8 @@ struct SettingsView: View { .tag(currency) } } - .onChange(of: selectedCurrency) { newCurrency in - viewModel.updateDefaultCurrency(newCurrency) + .onChange(of: selectedCurrency) { oldValue, newValue in + viewModel.updateDefaultCurrency(newValue) } } header: { Text("CURRENCY") @@ -169,51 +167,50 @@ struct SettingsView: View { private var privacySection: some View { Section { Toggle("Hide Balances", isOn: $hideBalances) - .onChange(of: hideBalances) { newValue in + .onChange(of: hideBalances) { oldValue, newValue in viewModel.updatePrivacySettings(hideBalances: newValue) } Toggle("Use Face ID", isOn: $useBiometrics) - .onChange(of: useBiometrics) { newValue in + .onChange(of: useBiometrics) { oldValue, newValue in viewModel.updatePrivacySettings(useBiometrics: newValue) } Toggle("Require Passcode on Launch", isOn: $requirePasscodeOnLaunch) - .onChange(of: requirePasscodeOnLaunch) { newValue in + .onChange(of: requirePasscodeOnLaunch) { oldValue, newValue in viewModel.updatePrivacySettings(requirePasscode: newValue) } } header: { Text("PRIVACY & SECURITY") } } - private var notificationsSection: some View { Section { Toggle("Enable Notifications", isOn: $notificationsEnabled) - .onChange(of: notificationsEnabled) { newValue in + .onChange(of: notificationsEnabled) { oldValue, newValue in viewModel.updateNotificationSettings(isEnabled: newValue) } if notificationsEnabled { Toggle("Daily Balance Reminder", isOn: $dailyReminder) - .onChange(of: dailyReminder) { newValue in + .onChange(of: dailyReminder) { oldValue, newValue in viewModel.updateNotificationSettings(dailyReminder: newValue) } if dailyReminder { DatePicker("Reminder Time", selection: $dailyReminderTime, displayedComponents: .hourAndMinute) - .onChange(of: dailyReminderTime) { newValue in + .onChange(of: dailyReminderTime) { oldValue, newValue in viewModel.updateNotificationSettings(reminderTime: newValue) } } Toggle("Weekly Report", isOn: $weeklyReport) - .onChange(of: weeklyReport) { newValue in + .onChange(of: weeklyReport) { oldValue, newValue in viewModel.updateNotificationSettings(weeklyReport: newValue) } Toggle("Transaction Alerts", isOn: $transactionAlerts) - .onChange(of: transactionAlerts) { newValue in + .onChange(of: transactionAlerts) { oldValue, newValue in viewModel.updateNotificationSettings(transactionAlerts: newValue) } } @@ -225,24 +222,23 @@ struct SettingsView: View { private var displaySection: some View { Section { Toggle("Show Cents", isOn: $showCents) - .onChange(of: showCents) { newValue in + .onChange(of: showCents) { oldValue, newValue in viewModel.updateDisplaySettings(showCents: newValue) } Toggle("Group Transactions by Day", isOn: $groupTransactionsByDay) - .onChange(of: groupTransactionsByDay) { newValue in + .onChange(of: groupTransactionsByDay) { oldValue, newValue in viewModel.updateDisplaySettings(groupByDay: newValue) } Toggle("Show Running Balance", isOn: $showRunningBalance) - .onChange(of: showRunningBalance) { newValue in + .onChange(of: showRunningBalance) { oldValue, newValue in viewModel.updateDisplaySettings(showRunningBalance: newValue) } } header: { Text("DISPLAY") } } - private var appInfoSection: some View { Section { HStack { @@ -296,96 +292,95 @@ struct SettingsView: View { .contentShape(Rectangle()) } } - @ViewBuilder - private var userProfileSection: some View { - if let email = viewModel.settings.userEmail { - VStack(alignment: .leading, spacing: 4) { - Text(email) + private var userProfileSection: some View { + if let email = viewModel.settings.userEmail { + VStack(alignment: .leading, spacing: 4) { + Text(email) + .foregroundColor(.primary) + Text("Signed in") + .font(.caption) + .foregroundColor(.secondary) + } + + Button(role: .destructive) { + // Handle sign out + } label: { + Text("Sign Out") + } + } + } + + private var authenticationSection: some View { + VStack(spacing: 12) { + NavigationLink { + SignInView() + } label: { + HStack { + Image(systemName: "person.fill") + Text("Sign In") + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(10) + } + + NavigationLink { + SignUpView() + } label: { + HStack { + Image(systemName: "person.badge.plus") + Text("Create Account") + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.secondary.opacity(0.1)) .foregroundColor(.primary) - Text("Signed in") - .font(.caption) - .foregroundColor(.secondary) - } - - Button(role: .destructive) { - // Handle sign out - } label: { - Text("Sign Out") - } - } - } - - private var authenticationSection: some View { - VStack(spacing: 12) { - NavigationLink { - SignInView() - } label: { - HStack { - Image(systemName: "person.fill") - Text("Sign In") + .cornerRadius(10) } - .frame(maxWidth: .infinity) - .padding() - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(10) } + .padding(.vertical, 8) + } + + // MARK: - Actions + private func checkForUpdates() { + Task { + await viewModel.checkForUpdates() + } + } + + private func resetSettings() { + viewModel.resetSettings() - NavigationLink { - SignUpView() - } label: { - HStack { - Image(systemName: "person.badge.plus") - Text("Create Account") - } - .frame(maxWidth: .infinity) - .padding() - .background(Color.secondary.opacity(0.1)) - .foregroundColor(.primary) - .cornerRadius(10) - } - } - .padding(.vertical, 8) - } - - // MARK: - Actions - private func checkForUpdates() { - Task { - await viewModel.checkForUpdates() + // Update local state + selectedTheme = viewModel.settings.theme + selectedLanguage = viewModel.settings.language + selectedCurrency = viewModel.settings.defaultCurrency + + // Privacy + hideBalances = viewModel.settings.privacy.hideBalances + useBiometrics = viewModel.settings.privacy.useBiometrics + requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch + + // Notifications + notificationsEnabled = viewModel.settings.notifications.isEnabled + dailyReminder = viewModel.settings.notifications.dailyReminder + dailyReminderTime = viewModel.settings.notifications.dailyReminderTime + weeklyReport = viewModel.settings.notifications.weeklyReport + transactionAlerts = viewModel.settings.notifications.transactionAlerts + + // Display + showCents = viewModel.settings.display.showCents + groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay + showRunningBalance = viewModel.settings.display.showRunningBalance } } - - private func resetSettings() { - viewModel.resetSettings() - - // Update local state - selectedTheme = viewModel.settings.theme - selectedLanguage = viewModel.settings.language - selectedCurrency = viewModel.settings.defaultCurrency - - // Privacy - hideBalances = viewModel.settings.privacy.hideBalances - useBiometrics = viewModel.settings.privacy.useBiometrics - requirePasscodeOnLaunch = viewModel.settings.privacy.requirePasscodeOnLaunch - - // Notifications - notificationsEnabled = viewModel.settings.notifications.isEnabled - dailyReminder = viewModel.settings.notifications.dailyReminder - dailyReminderTime = viewModel.settings.notifications.dailyReminderTime - weeklyReport = viewModel.settings.notifications.weeklyReport - transactionAlerts = viewModel.settings.notifications.transactionAlerts - - // Display - showCents = viewModel.settings.display.showCents - groupTransactionsByDay = viewModel.settings.display.groupTransactionsByDay - showRunningBalance = viewModel.settings.display.showRunningBalance - } -} -// MARK: - Preview -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() + // MARK: - Preview + struct SettingsView_Previews: PreviewProvider { + static var previews: some View { + SettingsView() + } } -}