Primera build enviada

This commit is contained in:
2026-01-19 14:40:43 +01:00
parent c6be398e5a
commit b03d35194f
36 changed files with 1641 additions and 561 deletions
@@ -18,7 +18,18 @@ struct AccountEditorView: View {
NavigationStack {
Form {
Section {
TextField("Account name", text: $name)
if isEditingDefaultAccount {
HStack {
Text(Account.defaultAccountName)
.foregroundColor(.secondary)
Spacer()
Image(systemName: "lock.fill")
.font(.caption)
.foregroundColor(.secondary)
}
} else {
TextField("Account name", text: $name)
}
Picker("Currency", selection: $currencyCode) {
ForEach(CurrencyPicker.commonCodes, id: \.self) { code in
Text(code).tag(code)
@@ -30,6 +41,8 @@ struct AccountEditorView: View {
if let errorMessage {
Text(errorMessage)
.foregroundColor(.negativeRed)
} else if isEditingDefaultAccount {
Text("The Default account name cannot be changed.")
}
}
@@ -67,9 +80,16 @@ struct AccountEditorView: View {
.presentationDetents([.large])
}
private var isEditingDefaultAccount: Bool {
account?.isDefaultAccount == true
}
private var isValid: Bool {
if isEditingDefaultAccount {
return true // Can only edit currency/inputMode for Default account
}
let trimmed = name.trimmingCharacters(in: .whitespaces)
return !trimmed.isEmpty && !isDuplicateName(trimmed)
return !trimmed.isEmpty && errorMessage == nil
}
private func validateName() {
@@ -78,17 +98,24 @@ struct AccountEditorView: View {
errorMessage = nil
return
}
errorMessage = isDuplicateName(trimmed) ? "An account with this name already exists." : nil
}
private func isDuplicateName(_ trimmed: String) -> Bool {
let normalized = trimmed.lowercased()
return accountRepository.accounts.contains { existing in
if let account, existing.id == account.id {
return false
// Check if trying to use reserved "Default" name for a non-default account
if trimmed.lowercased() == Account.defaultAccountName.lowercased() {
let isCreatingNew = account == nil
let isEditingNonDefault = account != nil && !account!.isDefaultAccount
if isCreatingNew || isEditingNonDefault {
errorMessage = "The name '\(Account.defaultAccountName)' is reserved."
return
}
return (existing.name).trimmingCharacters(in: .whitespaces).lowercased() == normalized
}
// Check for duplicate names
if !accountRepository.isNameAvailable(trimmed, excludingAccountId: account?.safeId) {
errorMessage = "An account with this name already exists."
return
}
errorMessage = nil
}
private func loadAccount() {
@@ -102,15 +129,17 @@ struct AccountEditorView: View {
}
private func saveAccount() {
validateName()
guard errorMessage == nil else { return }
let trimmedName = name.trimmingCharacters(in: .whitespaces)
if !isEditingDefaultAccount {
validateName()
guard errorMessage == nil else { return }
}
let trimmedName = isEditingDefaultAccount ? Account.defaultAccountName : name.trimmingCharacters(in: .whitespaces)
let enforcedFrequency: NotificationFrequency = .monthly
let enforcedCustomMonths = 1
if let account {
accountRepository.updateAccount(
account,
name: trimmedName,
name: isEditingDefaultAccount ? nil : trimmedName, // Don't update name for Default account
currency: currencyCode,
inputMode: inputMode,
notificationFrequency: enforcedFrequency,
@@ -8,6 +8,11 @@ struct AccountsView: View {
@State private var selectedAccount: Account?
@State private var showingPaywall = false
@State private var accountToDelete: Account?
@State private var showingDeleteBlocked = false
private var availableAccounts: [Account] {
accountRepository.accounts.filter { $0.safeId != nil }
}
var body: some View {
ZStack {
@@ -15,37 +20,47 @@ struct AccountsView: View {
List {
Section {
ForEach(accountRepository.accounts) { account in
ForEach(availableAccounts, id: \.objectID) { account in
let canDelete = accountRepository.canDeleteAccount(account)
Button {
selectedAccount = account
} label: {
HStack {
VStack(alignment: .leading) {
Text(account.name)
.font(.headline)
HStack(spacing: 4) {
Text(account.name)
.font(.headline)
if account.isDefaultAccount {
Image(systemName: "star.fill")
.font(.caption2)
.foregroundColor(.appWarning)
}
}
Text(account.currencyCode ?? AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
if accountStore.selectedAccount?.id == account.id && !accountStore.showAllAccounts {
if accountStore.selectedAccount?.safeId == account.safeId && !accountStore.showAllAccounts {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.appPrimary)
}
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
accountToDelete = account
} label: {
Label("Delete", systemImage: "trash")
if canDelete {
Button(role: .destructive) {
accountToDelete = account
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
} header: {
Text("Accounts")
} footer: {
Text(iapService.isPremium ? "Create multiple accounts for business, family, or goals." : "Free users can create one account.")
Text(iapService.isPremium ? "Create multiple accounts for business, family, or goals. The Default account cannot be deleted." : "Free users can create one account.")
}
}
.scrollContentBackground(.hidden)
@@ -61,14 +76,20 @@ struct AccountsView: View {
) {
Button("Delete", role: .destructive) {
guard let accountToDelete else { return }
if accountStore.selectedAccount?.id == accountToDelete.id {
accountStore.selectAllAccounts()
if accountRepository.canDeleteAccount(accountToDelete) {
accountRepository.deleteAccount(accountToDelete)
} else {
showingDeleteBlocked = true
}
accountRepository.deleteAccount(accountToDelete)
self.accountToDelete = nil
}
} message: {
Text("This will remove the account and unlink its sources.")
Text("This will remove the account and all its sources and snapshots.")
}
.alert("Cannot Delete Account", isPresented: $showingDeleteBlocked) {
Button("OK") {}
} message: {
Text("The Default account cannot be deleted. You must keep at least one account.")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
@@ -260,13 +260,13 @@ struct ChartsContainerView: View {
Divider()
ForEach(accountStore.accounts) { account in
ForEach(availableAccounts, id: \.objectID) { account in
Button {
accountStore.selectAccount(account)
} label: {
HStack {
Text(account.name)
if accountStore.selectedAccount?.id == account.id && !accountStore.showAllAccounts {
if accountStore.selectedAccount?.safeId == account.safeId && !accountStore.showAllAccounts {
Image(systemName: "checkmark")
}
}
@@ -276,6 +276,10 @@ struct ChartsContainerView: View {
Image(systemName: "person.2.circle")
}
}
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
}
// MARK: - Chart Type Button
@@ -89,6 +89,9 @@ struct DashboardView: View {
.sheet(isPresented: $showingCustomize) {
DashboardCustomizeView(configs: $sectionConfigs)
}
.sheet(isPresented: $viewModel.showingPaywall) {
PaywallView()
}
}
}
@@ -107,13 +110,13 @@ struct DashboardView: View {
Divider()
ForEach(accountStore.accounts) { account in
ForEach(availableAccounts, id: \.objectID) { account in
Button {
accountStore.selectAccount(account)
} label: {
HStack {
Text(account.name)
if accountStore.selectedAccount?.id == account.id && !accountStore.showAllAccounts {
if accountStore.selectedAccount?.safeId == account.safeId && !accountStore.showAllAccounts {
Image(systemName: "checkmark")
}
}
@@ -124,6 +127,10 @@ struct DashboardView: View {
}
}
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
private var visibleSections: [DashboardSectionConfig] {
sectionConfigs.filter { $0.isVisible }
}
@@ -144,7 +151,16 @@ struct DashboardView: View {
changeLabel: calmModeEnabled ? "since last update" : "today",
isPositive: calmModeEnabled
? viewModel.latestPortfolioChange.absolute >= 0
: viewModel.isDayChangePositive
: viewModel.isDayChangePositive,
forecast: viewModel.portfolioForecast,
isPremium: iapService.isPremium,
onUnlockTap: {
viewModel.showingPaywall = true
},
yearChange: viewModel.portfolioSummary.formattedYearChange,
sinceInceptionChange: viewModel.portfolioSummary.formattedAllTimeReturn,
isYearPositive: viewModel.isYearChangePositive,
isSinceInceptionPositive: viewModel.portfolioSummary.allTimeReturn >= 0
)
}
case .monthlyCheckIn:
@@ -243,6 +259,13 @@ struct TotalValueCard: View {
let changeText: String
let changeLabel: String
let isPositive: Bool
var forecast: PortfolioForecast?
var isPremium: Bool = false
var onUnlockTap: (() -> Void)?
var yearChange: String?
var sinceInceptionChange: String?
var isYearPositive: Bool = true
var isSinceInceptionPositive: Bool = true
var body: some View {
VStack(spacing: 8) {
@@ -264,6 +287,77 @@ struct TotalValueCard: View {
.foregroundColor(.white.opacity(0.8))
}
.foregroundColor(.white)
// YoY and Since Inception returns
if yearChange != nil || sinceInceptionChange != nil {
HStack(spacing: 16) {
if let yearChange = yearChange {
VStack(spacing: 2) {
Text("YoY")
.font(.caption2)
.foregroundColor(.white.opacity(0.7))
Text(yearChange)
.font(.caption.weight(.semibold))
.foregroundColor(.white)
}
}
if let sinceInceptionChange = sinceInceptionChange {
VStack(spacing: 2) {
Text("Since inception")
.font(.caption2)
.foregroundColor(.white.opacity(0.7))
Text(sinceInceptionChange)
.font(.caption.weight(.semibold))
.foregroundColor(.white)
}
}
}
.padding(.top, 4)
}
// Forecast section
if let forecast = forecast {
Divider()
.background(Color.white.opacity(0.3))
.padding(.vertical, 8)
if isPremium {
VStack(spacing: 4) {
HStack(spacing: 4) {
Image(systemName: "wand.and.stars")
.font(.caption)
Text("12-month forecast")
.font(.caption)
}
.foregroundColor(.white.opacity(0.85))
Text(forecast.formattedForecastValue)
.font(.title2.weight(.bold))
.foregroundColor(.white)
Text(forecast.formattedGrowthPercentage)
.font(.caption.weight(.semibold))
.foregroundColor(forecast.isPositiveGrowth ? .white : .white.opacity(0.8))
}
} else {
Button {
onUnlockTap?()
} label: {
HStack(spacing: 6) {
Image(systemName: "lock.fill")
.font(.caption)
Text("Unlock 12-month forecast")
.font(.caption.weight(.semibold))
}
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.white.opacity(0.2))
.cornerRadius(16)
}
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 24)
@@ -36,6 +36,10 @@ struct ImportDataView: View {
private let accountRepository = AccountRepository()
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
init(importContext: ImportContext = .settings) {
self.importContext = importContext
}
@@ -146,8 +150,17 @@ struct ImportDataView: View {
Text(errorMessage ?? "")
}
.onAppear {
if selectedAccountId == nil {
selectedAccountId = accountStore.selectedAccount?.id ?? accountStore.accounts.first?.id
// Ensure selectedAccountId is valid and exists in availableAccounts
let validIds = Set(availableAccounts.compactMap { $0.safeId })
if selectedAccountId == nil || !validIds.contains(selectedAccountId!) {
selectedAccountId = accountStore.selectedAccount?.safeId ?? availableAccounts.first?.safeId
}
}
.onChange(of: availableAccounts) { _, newAccounts in
// Re-validate selectedAccountId when accounts change
let validIds = Set(newAccounts.compactMap { $0.safeId })
if let currentId = selectedAccountId, !validIds.contains(currentId) {
selectedAccountId = newAccounts.first?.safeId
}
}
.onChange(of: accountSelection) { _, _ in
@@ -219,18 +232,17 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
}
private var shouldShowAccountSelection: Bool {
importContext == .onboarding
// Show account selection when there are multiple accounts (Premium) or during onboarding
iapService.isPremium && availableAccounts.count > 1 || importContext == .onboarding
}
private var importFooterText: String {
if importContext == .onboarding {
if shouldShowAccountSelection {
return iapService.isPremium
? "Import will be added to the selected account."
: "Free users import into the existing account."
: "Free users import into the Default account."
}
return iapService.isPremium
? "Accounts are imported as provided."
: "Free users import into the selected account."
return "Data will be imported into your Default account."
}
private var accountSection: some View {
@@ -245,8 +257,8 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
if accountSelection == .existing {
Picker("Import into", selection: $selectedAccountId) {
ForEach(accountStore.accounts) { account in
Text(account.name).tag(Optional(account.id))
ForEach(availableAccounts, id: \.objectID) { account in
Text(account.name).tag(Optional(account.safeId))
}
}
.disabled(isImporting)
@@ -273,9 +285,9 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
}
private var selectedAccountName: String? {
accountStore.accounts.first { $0.id == selectedAccountId }?.name
availableAccounts.first { $0.safeId == selectedAccountId }?.name
?? accountStore.selectedAccount?.name
?? accountStore.accounts.first?.name
?? availableAccounts.first?.name
}
private func validateNewAccountName() -> String? {
@@ -355,12 +367,17 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
}
private func handleImportContent(_ content: String) {
let shouldForceSingleAccount = importContext == .onboarding
let allowMultipleAccounts = iapService.isPremium && !shouldForceSingleAccount
var defaultAccountName = accountStore.selectedAccount?.name
?? accountStore.accounts.first?.name
// For non-Premium users or when only one account exists, use the Default account
// For Premium users with multiple accounts, respect their selection
let useAccountSelection = shouldShowAccountSelection && iapService.isPremium
let allowMultipleAccounts = false // Always import into a single account
if shouldForceSingleAccount && iapService.isPremium {
var defaultAccountName = accountStore.accounts.first(where: { $0.isDefaultAccount })?.name
?? accountStore.selectedAccount?.name
?? accountStore.accounts.first?.name
?? Account.defaultAccountName
if useAccountSelection {
if accountSelection == .new {
accountErrorMessage = validateNewAccountName()
guard accountErrorMessage == nil else { return }
@@ -375,7 +392,7 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
)
defaultAccountName = account.name
} else {
defaultAccountName = selectedAccountName
defaultAccountName = selectedAccountName ?? defaultAccountName
}
}
@@ -397,7 +414,11 @@ Personal,Stocks,Index Fund,2024-01-01,15000,12000,Long-term
isImporting = false
if importResult.errors.isEmpty {
resultMessage = "Imported \(importResult.sourcesCreated) sources and \(importResult.snapshotsCreated) snapshots."
var message = "Imported \(importResult.sourcesCreated) sources and \(importResult.snapshotsCreated) snapshots."
if importResult.snapshotsUpdated > 0 {
message += " Updated \(importResult.snapshotsUpdated) existing snapshots."
}
resultMessage = message
tabSelection.selectedTab = 0
dismiss()
} else {
@@ -1,5 +1,6 @@
import SwiftUI
import StoreKit
import SwiftUI
import UIKit
struct SettingsView: View {
private let iapService: IAPService
@@ -67,6 +68,9 @@ struct SettingsView: View {
.sheet(isPresented: $viewModel.showingExportOptions) {
ExportOptionsSheet(viewModel: viewModel)
}
.sheet(item: $viewModel.shareItem) { shareItem in
ActivityView(activityItems: [shareItem.url])
}
.sheet(isPresented: $viewModel.showingImportSheet) {
ImportDataView()
}
@@ -578,6 +582,22 @@ struct SettingsView: View {
}
}
// MARK: - Activity View
struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities
)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
// MARK: - Export Options Sheet
struct ExportOptionsSheet: View {
@@ -587,59 +607,75 @@ struct ExportOptionsSheet: View {
var body: some View {
NavigationStack {
List {
Section {
Button {
viewModel.exportData(format: .csv)
dismiss()
} label: {
HStack {
Image(systemName: "tablecells")
.foregroundColor(.positiveGreen)
.frame(width: 30)
if viewModel.isExporting {
Section {
VStack(alignment: .leading, spacing: 12) {
ProgressView(value: viewModel.exportProgress)
.progressViewStyle(.linear)
VStack(alignment: .leading) {
Text("CSV")
.font(.headline)
Text("Compatible with Excel, Google Sheets")
.font(.caption)
.foregroundColor(.secondary)
Text(viewModel.exportStatus)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 8)
} header: {
Text("Exporting...")
}
} else {
Section {
Button {
viewModel.exportData(format: .csv)
} label: {
HStack {
Image(systemName: "tablecells")
.foregroundColor(.positiveGreen)
.frame(width: 30)
VStack(alignment: .leading) {
Text("CSV")
.font(.headline)
Text("Compatible with Excel, Google Sheets")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
Button {
viewModel.exportData(format: .json)
dismiss()
} label: {
HStack {
Image(systemName: "doc.text")
.foregroundColor(.appPrimary)
.frame(width: 30)
Button {
viewModel.exportData(format: .json)
} label: {
HStack {
Image(systemName: "doc.text")
.foregroundColor(.appPrimary)
.frame(width: 30)
VStack(alignment: .leading) {
Text("JSON")
.font(.headline)
Text("Full data structure for backup")
.font(.caption)
.foregroundColor(.secondary)
VStack(alignment: .leading) {
Text("JSON")
.font(.headline)
Text("Full data structure for backup")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
} header: {
Text("Select Format")
}
} header: {
Text("Select Format")
}
}
.navigationTitle("Export Data")
.navigationTitle(viewModel.isExporting ? "Exporting" : "Export Data")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Cancel") {
dismiss()
}
.disabled(viewModel.isExporting)
}
}
}
.presentationDetents([.medium])
.interactiveDismissDisabled(viewModel.isExporting)
}
}
@@ -10,19 +10,28 @@ struct AddSourceView: View {
@State private var initialValue = ""
@State private var showingCategoryPicker = false
@State private var selectedAccountId: UUID?
@State private var duplicateError: String?
@StateObject private var categoryRepository = CategoryRepository()
@StateObject private var sourceRepository = InvestmentSourceRepository()
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
var body: some View {
NavigationStack {
Form {
if accountStore.accounts.count > 1 {
if availableAccounts.count > 1 {
Section {
Picker("Account", selection: $selectedAccountId) {
ForEach(accountStore.accounts) { account in
Text(account.name).tag(Optional(account.id))
ForEach(availableAccounts, id: \.objectID) { account in
Text(account.name).tag(Optional(account.safeId))
}
}
.onChange(of: selectedAccountId) { _, _ in
validateSourceName(name)
}
} header: {
Text("Account")
}
@@ -32,6 +41,9 @@ struct AddSourceView: View {
Section {
TextField("Source Name", text: $name)
.textContentType(.organizationName)
.onChange(of: name) { _, newValue in
validateSourceName(newValue)
}
Button {
showingCategoryPicker = true
@@ -61,6 +73,11 @@ struct AddSourceView: View {
}
} header: {
Text("Source Information")
} footer: {
if let error = duplicateError {
Text(error)
.foregroundColor(.negativeRed)
}
}
// Initial Value (Optional)
@@ -107,7 +124,7 @@ struct AddSourceView: View {
selectedCategory = categoryRepository.categories.first
}
if selectedAccountId == nil {
selectedAccountId = accountStore.selectedAccount?.id ?? accountStore.accounts.first?.id
selectedAccountId = accountStore.selectedAccount?.safeId ?? availableAccounts.first?.safeId
}
}
}
@@ -116,7 +133,21 @@ struct AddSourceView: View {
// MARK: - Validation
private var isValid: Bool {
!name.trimmingCharacters(in: .whitespaces).isEmpty && selectedCategory != nil
!name.trimmingCharacters(in: .whitespaces).isEmpty && selectedCategory != nil && duplicateError == nil
}
private func validateSourceName(_ newName: String) {
let trimmed = newName.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
duplicateError = nil
return
}
if sourceRepository.sourceExists(name: trimmed, in: selectedAccount) {
duplicateError = "A source with this name already exists."
} else {
duplicateError = nil
}
}
// MARK: - Save
@@ -177,7 +208,7 @@ struct AddSourceView: View {
private var selectedAccount: Account? {
guard let id = selectedAccountId else { return nil }
return accountStore.accounts.first { $0.id == id }
return availableAccounts.first { $0.safeId == id }
}
}
@@ -248,17 +279,17 @@ struct EditSourceView: View {
self.source = source
_name = State(initialValue: source.name)
_selectedCategory = State(initialValue: source.category)
_selectedAccountId = State(initialValue: source.account?.id)
_selectedAccountId = State(initialValue: source.account?.safeId)
}
var body: some View {
NavigationStack {
Form {
if accountStore.accounts.count > 1 {
if availableAccounts.count > 1 {
Section {
Picker("Account", selection: $selectedAccountId) {
ForEach(accountStore.accounts) { account in
Text(account.name).tag(Optional(account.id))
ForEach(availableAccounts, id: \.objectID) { account in
Text(account.name).tag(Optional(account.safeId))
}
}
} header: {
@@ -320,6 +351,10 @@ struct EditSourceView: View {
}
}
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
private var isValid: Bool {
!name.trimmingCharacters(in: .whitespaces).isEmpty && selectedCategory != nil
}
@@ -343,7 +378,7 @@ struct EditSourceView: View {
private var selectedAccount: Account? {
guard let id = selectedAccountId else { return nil }
return accountStore.accounts.first { $0.id == id }
return availableAccounts.first { $0.safeId == id }
}
}
@@ -424,42 +424,48 @@ struct SourceDetailView: View {
.cornerRadius(AppConstants.UI.smallCornerRadius)
}
ForEach(viewModel.visibleSnapshots) { snapshot in
SnapshotRowView(snapshot: snapshot, onEdit: {
editingSnapshot = snapshot
})
.contentShape(Rectangle())
.onTapGesture {
editingSnapshot = snapshot
}
.contextMenu {
Button {
editingSnapshot = snapshot
} label: {
Label("Edit", systemImage: "pencil")
}
}
.swipeActions(edge: .trailing) {
Button {
editingSnapshot = snapshot
} label: {
Label("Edit", systemImage: "pencil")
}
// Use LazyVStack for better performance with many snapshots
LazyVStack(spacing: 0) {
ForEach(viewModel.visibleSnapshots) { snapshot in
VStack(spacing: 0) {
SnapshotRowView(snapshot: snapshot, onEdit: {
editingSnapshot = snapshot
})
.contentShape(Rectangle())
.onTapGesture {
editingSnapshot = snapshot
}
.contextMenu {
Button {
editingSnapshot = snapshot
} label: {
Label("Edit", systemImage: "pencil")
}
}
.swipeActions(edge: .trailing) {
Button {
editingSnapshot = snapshot
} label: {
Label("Edit", systemImage: "pencil")
}
Button(role: .destructive) {
viewModel.deleteSnapshot(snapshot)
} label: {
Label("Delete", systemImage: "trash")
}
}
Button(role: .destructive) {
viewModel.deleteSnapshot(snapshot)
} label: {
Label("Delete", systemImage: "trash")
}
}
if snapshot.id != viewModel.visibleSnapshots.last?.id {
Divider()
if snapshot.id != viewModel.visibleSnapshots.last?.id {
Divider()
}
}
}
}
if viewModel.snapshots.count > 10 {
Text("+ \(viewModel.snapshots.count - 10) more snapshots")
// Show "more snapshots" only for free users who have limited history
if viewModel.isHistoryLimited && viewModel.hiddenSnapshotCount > 0 {
Text("+ \(viewModel.hiddenSnapshotCount) older snapshots hidden")
.font(.caption)
.foregroundColor(.secondary)
}
@@ -234,13 +234,13 @@ struct SourceListView: View {
Divider()
ForEach(accountStore.accounts) { account in
ForEach(availableAccounts, id: \.objectID) { account in
Button {
accountStore.selectAccount(account)
} label: {
HStack {
Text(account.name)
if accountStore.selectedAccount?.id == account.id && !accountStore.showAllAccounts {
if accountStore.selectedAccount?.safeId == account.safeId && !accountStore.showAllAccounts {
Image(systemName: "checkmark")
}
}
@@ -257,6 +257,10 @@ struct SourceListView: View {
}
}
}
private var availableAccounts: [Account] {
accountStore.accounts.filter { $0.safeId != nil }
}
}
// MARK: - Source Row View