diff --git a/MMEX.xcodeproj/project.pbxproj b/MMEX.xcodeproj/project.pbxproj index 8188fceb..cc95d421 100644 --- a/MMEX.xcodeproj/project.pbxproj +++ b/MMEX.xcodeproj/project.pbxproj @@ -768,7 +768,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"MMEX/Preview Content\""; DEVELOPMENT_TEAM = 3N3NHU8X3F; ENABLE_PREVIEWS = YES; @@ -786,7 +786,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1.5; + MARKETING_VERSION = 0.1.6; PRODUCT_BUNDLE_IDENTIFIER = com.guangong.mmex; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -803,7 +803,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"MMEX/Preview Content\""; DEVELOPMENT_TEAM = 3N3NHU8X3F; ENABLE_PREVIEWS = YES; @@ -821,7 +821,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.1.5; + MARKETING_VERSION = 0.1.6; PRODUCT_BUNDLE_IDENTIFIER = com.guangong.mmex; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/MMEX/ContentView.swift b/MMEX/ContentView.swift index d16316f6..9c51acd2 100644 --- a/MMEX/ContentView.swift +++ b/MMEX/ContentView.swift @@ -10,160 +10,202 @@ import SwiftUI struct ContentView: View { @State private var isDocumentPickerPresented = false @State private var isNewDocumentPickerPresented = false - @State private var isSampleDocumnt = false + @State private var isSampleDocument = false @State private var selectedTab = 0 @State private var selectedFileURL: URL? @State private var isPresentingTransactionAddView = false @Environment(\.horizontalSizeClass) var horizontalSizeClass @Environment(\.verticalSizeClass) var verticalSizeClass - - @EnvironmentObject var dataManager: DataManager // Access DataManager from environment + @EnvironmentObject var dataManager: DataManager var body: some View { ZStack { if dataManager.isDatabaseConnected { - if horizontalSizeClass == .regular { - // && verticalSizeClass == .compact { - // iPad layout: Tabs on the side - NavigationSplitView { - SidebarView(selectedTab: $selectedTab) - } detail: { - TabContentView(selectedTab: $selectedTab, isDocumentPickerPresented: $isDocumentPickerPresented) - } - } else { - // Use @StateObject to manage the lifecycle of InfotableViewModel - let infotableViewModel = InfotableViewModel(dataManager: dataManager) - // iPhone layout: Tabs at the bottom - TabView(selection: $selectedTab) { - // Latest Transactions Tab - NavigationView { - TransactionListView2(viewModel: infotableViewModel) // Summary and Edit feature - .navigationBarTitle("Latest Transactions", displayMode: .inline) - } - .tabItem { - Image(systemName: "list.bullet") - Text("Checking") - } - .tag(0) - - // Insights module - NavigationView { - InsightsView(viewModel: InsightsViewModel(dataManager: dataManager)) - .navigationBarTitle("Reports and Insights", displayMode: .inline) - } - .tabItem { - Image(systemName: "arrow.up.right") - Text("Insights") - } - .tag(1) - - // Add Transactions Tab - NavigationView { - TransactionAddView2(selectedTab: $selectedTab) // Reuse or new transaction add - .navigationBarTitle("Add Transaction", displayMode: .inline) - } - .tabItem { - Image(systemName: "plus.circle") - Text("Add Transaction") - } - .tag(2) - - // Combined Management Tab - NavigationView { - ManagementView(isDocumentPickerPresented: $isDocumentPickerPresented) - .navigationBarTitle("Management", displayMode: .inline) - } - .tabItem { - Image(systemName: "folder") - Text("Management") - } - .tag(3) - - // Settings Tab - NavigationView { - SettingsView(viewModel: infotableViewModel) // Payees, Accounts, Currency - .navigationBarTitle("Settings", displayMode: .inline) - } - .tabItem { - Image(systemName: "gearshape") - Text("Settings") - } - .tag(4) - } - .onChange(of: selectedTab) { tab in - if tab == 2 { - isPresentingTransactionAddView = true - } - } - } + connectedView } else { - VStack(spacing: 20) { - Button("Open Database") { - isDocumentPickerPresented = true - } - Button("New Database") { - isNewDocumentPickerPresented = true - isSampleDocumnt = false - } - Button("Sample Database") { - isNewDocumentPickerPresented = true - isSampleDocumnt = true - } - } + disconnectedView } } .fileImporter( isPresented: $isDocumentPickerPresented, allowedContentTypes: [.item], allowsMultipleSelection: false - ) { result in - switch result { - case .success(let urls): - if let selectedURL = urls.first { - if selectedURL.startAccessingSecurityScopedResource() { - dataManager.openDatabase(at: selectedURL) - UserDefaults.standard.set(selectedURL.path, forKey: "SelectedFilePath") - selectedURL.stopAccessingSecurityScopedResource() - } else { - print("Unable to access file at URL: \(selectedURL)") - } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - selectedTab = 0 - } - } - case .failure(let error): - print("Failed to pick a document: \(error.localizedDescription)") - } - } + ) { handleFileImport($0) } .fileExporter( isPresented: $isNewDocumentPickerPresented, document: MMEXDocument(), contentType: .mmb, - defaultFilename: isSampleDocumnt ? "Sample.mmb" : "Untitled.mmb" - ) { result in - switch result { - case .success(let url): - print("Successfully created new document: \(url)") - if url.startAccessingSecurityScopedResource() { - dataManager.openDatabase(at: url) - UserDefaults.standard.set(url.path, forKey: "SelectedFilePath") - url.stopAccessingSecurityScopedResource() - } else { - print("Unable to access file at URL: \(url)") + defaultFilename: isSampleDocument ? "Sample.mmb" : "Untitled.mmb" + ) { handleFileExport($0) } + } + + private var connectedView: some View { + Group { + if horizontalSizeClass == .regular { + NavigationSplitView { + SidebarView(selectedTab: $selectedTab) + } detail: { + TabContentView(selectedTab: $selectedTab, isDocumentPickerPresented: $isDocumentPickerPresented, isNewDocumentPickerPresented: $isNewDocumentPickerPresented, isSampleDocument: $isSampleDocument) } - let repository = dataManager.getRepository() - guard let tables = Bundle.main.url(forResource: "tables.sql", withExtension: "") else { - print("Cannot find tables.sql in bundle") - return + } else { + let infotableViewModel = InfotableViewModel(dataManager: dataManager) + TabView(selection: $selectedTab) { + transactionTab(viewModel: infotableViewModel) + insightsTab(viewModel: InsightsViewModel(dataManager: dataManager)) + addTransactionTab + managementTab + settingsTab(viewModel: infotableViewModel) + } + .onChange(of: selectedTab) { tab in + if tab == 2 { isPresentingTransactionAddView = true } + } + } + } + } + + private var disconnectedView: some View { + VStack(spacing: 30) { + Image(systemName: "tray.fill") + .resizable() + .frame(width: 80, height: 80) + .foregroundColor(.accentColor) + Text("No Database Connected") + .font(.title) + .padding(.bottom, 10) + Text("Please open an existing database or create a new one to get started.") + .font(.body) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Button(action: { isDocumentPickerPresented = true }) { + Label("Open Database", systemImage: "folder.fill") + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + + Button(action: { isNewDocumentPickerPresented = true; isSampleDocument = false }) { + Label("Create New Database", systemImage: "plus.app.fill") + } + .buttonStyle(.bordered) + .controlSize(.large) + + Button(action: { isNewDocumentPickerPresented = true; isSampleDocument = true }) { + Label("Use Sample Database", systemImage: "doc.text.fill") + } + .buttonStyle(.bordered) + .controlSize(.large) + } + .padding() + } + + // Transaction tab + private func transactionTab(viewModel: InfotableViewModel) -> some View { + NavigationView { + TransactionListView2(viewModel: viewModel) + .navigationBarTitle("Latest Transactions", displayMode: .inline) + } + .tabItem { + Image(systemName: "list.bullet") + Text("Checking") + } + .tag(0) + } + + // Insights tab + private func insightsTab(viewModel: InsightsViewModel) -> some View { + NavigationView { + InsightsView(viewModel: viewModel) + .navigationBarTitle("Reports and Insights", displayMode: .inline) + } + .tabItem { + Image(systemName: "arrow.up.right") + Text("Insights") + } + .tag(1) + } + + // Add transaction tab + private var addTransactionTab: some View { + NavigationView { + TransactionAddView2(selectedTab: $selectedTab) + .navigationBarTitle("Add Transaction", displayMode: .inline) + } + .tabItem { + Image(systemName: "plus.circle") + Text("Add Transaction") + } + .tag(2) + } + + // Management tab + private var managementTab: some View { + NavigationView { + ManagementView(isDocumentPickerPresented: $isDocumentPickerPresented, isNewDocumentPickerPresented: $isNewDocumentPickerPresented, isSampleDocument: $isSampleDocument) + .navigationBarTitle("Management", displayMode: .inline) + } + .tabItem { + Image(systemName: "folder") + Text("Management") + } + .tag(3) + } + + // Settings tab + private func settingsTab(viewModel: InfotableViewModel) -> some View { + NavigationView { + SettingsView(viewModel: viewModel) + .navigationBarTitle("Settings", displayMode: .inline) + } + .tabItem { + Image(systemName: "gearshape") + Text("Settings") + } + .tag(4) + } + + // File import handling + private func handleFileImport(_ result: Result<[URL], Error>) { + switch result { + case .success(let urls): + if let selectedURL = urls.first { + if selectedURL.startAccessingSecurityScopedResource() { + dataManager.openDatabase(at: selectedURL) + UserDefaults.standard.set(selectedURL.path, forKey: "SelectedFilePath") + selectedURL.stopAccessingSecurityScopedResource() + } else { + print("Unable to access file at URL: \(selectedURL)") } - repository.execute(url: tables) - if isSampleDocumnt { repository.insertSampleData() } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { selectedTab = 0 } - case .failure(let error): - print("Failed to create a new document: \(error.localizedDescription)") } + case .failure(let error): + print("Failed to pick a document: \(error.localizedDescription)") + } + } + + // File export handling + private func handleFileExport(_ result: Result) { + switch result { + case .success(let url): + print("Successfully created new document: \(url)") + if url.startAccessingSecurityScopedResource() { + dataManager.openDatabase(at: url) + UserDefaults.standard.set(url.path, forKey: "SelectedFilePath") + url.stopAccessingSecurityScopedResource() + } else { + print("Unable to access file at URL: \(url)") + } + let repository = dataManager.getRepository() + if let tables = Bundle.main.url(forResource: "tables.sql", withExtension: "") { + repository.execute(url: tables) + if isSampleDocument { repository.insertSampleData() } + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + selectedTab = 0 + } + case .failure(let error): + print("Failed to create a new document: \(error.localizedDescription)") } } } @@ -196,6 +238,8 @@ struct SidebarView: View { struct TabContentView: View { @Binding var selectedTab: Int @Binding var isDocumentPickerPresented: Bool + @Binding var isNewDocumentPickerPresented: Bool + @Binding var isSampleDocument: Bool @EnvironmentObject var dataManager: DataManager // Access DataManager from environment var body: some View { @@ -214,7 +258,7 @@ struct TabContentView: View { TransactionAddView2(selectedTab: $selectedTab) .navigationBarTitle("Add Transaction", displayMode: .inline) case 3: - ManagementView(isDocumentPickerPresented: $isDocumentPickerPresented) + ManagementView(isDocumentPickerPresented: $isDocumentPickerPresented, isNewDocumentPickerPresented: $isNewDocumentPickerPresented, isSampleDocument: $isSampleDocument) .navigationBarTitle("Management", displayMode: .inline) case 4: SettingsView(viewModel: infotableViewModel) @@ -227,11 +271,7 @@ struct TabContentView: View { } } -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - // Initialize the DataManager and inject it into the preview - let dataManager = DataManager() - ContentView() - .environmentObject(dataManager) // Inject DataManager - } +#Preview(){ + ContentView() + .environmentObject(DataManager()) // Inject DataManager } diff --git a/MMEX/DatabaseManager.swift b/MMEX/DatabaseManager.swift index 34e431a5..16712a9e 100644 --- a/MMEX/DatabaseManager.swift +++ b/MMEX/DatabaseManager.swift @@ -79,6 +79,15 @@ class DataManager: ObservableObject { return nil } + /// Closes the current database connection and resets related states. + func closeDatabase() { + // Nullify the connection and reset the state + db = nil + isDatabaseConnected = false + databaseURL = nil + print("Database connection closed.") + } + func getRepository() -> Repository { Repository(db: db) } func getInfotableRepository() -> InfotableRepository { InfotableRepository(db: db) } func getCurrencyRepository() -> CurrencyRepository { CurrencyRepository(db: db) } diff --git a/MMEX/Views/Accounts/AccountListView.swift b/MMEX/Views/Accounts/AccountListView.swift index 2db7027d..fe2fe814 100644 --- a/MMEX/Views/Accounts/AccountListView.swift +++ b/MMEX/Views/Accounts/AccountListView.swift @@ -131,4 +131,5 @@ struct AccountListView: View { #Preview { AccountListView() + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Assets/AssetListView.swift b/MMEX/Views/Assets/AssetListView.swift index a1caea31..56950feb 100644 --- a/MMEX/Views/Assets/AssetListView.swift +++ b/MMEX/Views/Assets/AssetListView.swift @@ -87,4 +87,5 @@ struct AssetListView: View { #Preview { AssetListView() + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Categories/CategoryListView.swift b/MMEX/Views/Categories/CategoryListView.swift index ec518f9e..ee5728bc 100644 --- a/MMEX/Views/Categories/CategoryListView.swift +++ b/MMEX/Views/Categories/CategoryListView.swift @@ -9,14 +9,16 @@ import SwiftUI struct CategoryListView: View { @State private var categories: [CategoryData] = [] + @State private var filteredCategories: [CategoryData] = [] @EnvironmentObject var dataManager: DataManager // Access DataManager from environment @State private var isPresentingAddView = false @State private var newCategory = CategoryData() + @State private var searchQuery: String = "" // New: Search query var body: some View { NavigationStack { - List($categories) { $category in + List($filteredCategories) { $category in NavigationLink(destination: CategoryDetailView(category: $category)) { HStack { Text(category.name) @@ -33,6 +35,10 @@ struct CategoryListView: View { }) .accessibilityLabel("New Category") } + .searchable(text: $searchQuery) // New: Search bar + .onChange(of: searchQuery, perform: { query in + filterCategories(by: query) + }) } .navigationTitle("Categories") .onAppear { @@ -52,6 +58,7 @@ struct CategoryListView: View { DispatchQueue.main.async { self.categories = loadedCategories + self.filteredCategories = loadedCategories } } } @@ -62,8 +69,17 @@ struct CategoryListView: View { self.categories.append(category) } } + // New: Filter based on the search query + func filterCategories(by query: String) { + if query.isEmpty { + filteredCategories = categories + } else { + filteredCategories = categories.filter { $0.name.localizedCaseInsensitiveContains(query) } + } + } } #Preview { CategoryListView() + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Currencies/CurrencyListView.swift b/MMEX/Views/Currencies/CurrencyListView.swift index dbe673d2..6426adb0 100644 --- a/MMEX/Views/Currencies/CurrencyListView.swift +++ b/MMEX/Views/Currencies/CurrencyListView.swift @@ -127,4 +127,5 @@ struct CurrencyListView: View { #Preview { CurrencyListView() + .environmentObject(DataManager()) } diff --git a/MMEX/Views/ManagementView.swift b/MMEX/Views/ManagementView.swift index d6f43b4d..84a9afc7 100644 --- a/MMEX/Views/ManagementView.swift +++ b/MMEX/Views/ManagementView.swift @@ -8,7 +8,10 @@ import SwiftUI struct ManagementView: View { + @EnvironmentObject var dataManager: DataManager // Access DataManager from environment @Binding var isDocumentPickerPresented: Bool + @Binding var isNewDocumentPickerPresented: Bool + @Binding var isSampleDocument: Bool var body: some View { List { @@ -34,19 +37,60 @@ struct ManagementView: View { } Section(header: Text("Database")) { - Button("Re-open Database") { + Button(action: { isDocumentPickerPresented = true + }) { + Text("Re-open Database") + .frame(maxWidth: .infinity, alignment: .center) } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) + + Button(action: { + isNewDocumentPickerPresented = true + isSampleDocument = false + }) { + Text("New Database") + .frame(maxWidth: .infinity, alignment: .center) + } + .padding() + .background(Color.green) + .foregroundColor(.white) + .cornerRadius(10) + + Button(action: { + isNewDocumentPickerPresented = true + isSampleDocument = true + }) { + Text("Sample Database") + .frame(maxWidth: .infinity, alignment: .center) + } + .padding() + .background(Color.orange) + .foregroundColor(.white) + .cornerRadius(10) + + // Close Database Button + Button(action: { + dataManager.closeDatabase() // Calls method to handle closing the database + }) { + Text("Close Database") + .frame(maxWidth: .infinity, alignment: .center) + } + .padding() + .background(Color.red) + .foregroundColor(.white) + .cornerRadius(10) } } + .listStyle(InsetGroupedListStyle()) // Better styling for iOS } } #Preview { - ManagementView(isDocumentPickerPresented: .constant(false)) + ManagementView(isDocumentPickerPresented: .constant(false), isNewDocumentPickerPresented: .constant(false), isSampleDocument: .constant(false)) + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Payees/PayeeListView.swift b/MMEX/Views/Payees/PayeeListView.swift index ba188ff4..bbeb5625 100644 --- a/MMEX/Views/Payees/PayeeListView.swift +++ b/MMEX/Views/Payees/PayeeListView.swift @@ -101,4 +101,5 @@ struct PayeeListView: View { #Preview { PayeeListView() + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Settings/SettingsView.swift b/MMEX/Views/Settings/SettingsView.swift index ea1bfeb8..643350a2 100644 --- a/MMEX/Views/Settings/SettingsView.swift +++ b/MMEX/Views/Settings/SettingsView.swift @@ -111,4 +111,5 @@ enum DefaultPayeeSetting: String, CaseIterable, Identifiable { #Preview { SettingsView(viewModel: InfotableViewModel(dataManager: DataManager())) + .environmentObject(DataManager()) } diff --git a/MMEX/Views/Transactions/TransactionListView.swift b/MMEX/Views/Transactions/TransactionListView.swift index cf7bef66..bad8cb1f 100644 --- a/MMEX/Views/Transactions/TransactionListView.swift +++ b/MMEX/Views/Transactions/TransactionListView.swift @@ -164,3 +164,8 @@ struct TransactionListView: View { return isoDate } } + +#Preview { + TransactionListView() + .environmentObject(DataManager()) +}