From 1c572f659890e76a1228038543f74c4ffd282a25 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Fri, 14 Aug 2026 18:33:26 -0400 Subject: [PATCH 1/4] Drop files into Minna This is a really hacky version, but truly the only way to do this in a sandboxed app. The two next best solutions are for a companion non-sandboxed process or the user has to manually allow permission through a file importer. This feature hinges on the fact that during the completion of a NSItemProvider you get access to the original URL that comes from a bookmark. You can then generate a bookmark off of this URL and then re-grab a URL. That new URL will then be a full permission URL that we can use like a url retrieved from a file importer --- Minna.xcodeproj/project.pbxproj | 8 +- Minna/Database/Import/FileImporter.swift | 195 ++++++++++-------- Minna/Views/Navigation/NavigationCore.swift | 5 +- Minna/Views/Navigation/NavigationRouter.swift | 5 + 4 files changed, 120 insertions(+), 93 deletions(-) diff --git a/Minna.xcodeproj/project.pbxproj b/Minna.xcodeproj/project.pbxproj index 24619f6..4eb1098 100644 --- a/Minna.xcodeproj/project.pbxproj +++ b/Minna.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ DB9696AC2FF492BF00CB037A /* MinnaChat in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AB2FF492BF00CB037A /* MinnaChat */; }; DB9696AE2FF492C000CB037A /* ModelManager in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AD2FF492C000CB037A /* ModelManager */; }; DBA6E66130254545002A94F2 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = DBA6E66030254545002A94F2 /* PostHog */; }; + DBC1AC60302F6F5E0001A9B4 /* Config.local.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */; }; DBD7CF502FF5689400A2CA0A /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CF4F2FF5689400A2CA0A /* KeychainSwift */; }; DBD7CFBB2FF59FEC00A2CA0A /* ModernSettingsWindow in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CFBA2FF59FEC00A2CA0A /* ModernSettingsWindow */; }; DBFD0AA8300EA44F0060744B /* LookAtMe in Frameworks */ = {isa = PBXBuildFile; productRef = DBFD0AA7300EA44F0060744B /* LookAtMe */; }; @@ -82,8 +83,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - DBC0F1A02FF0000100000001 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - DBC0F1A02FF0000100000002 /* Config.local.example.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.local.example.xcconfig; sourceTree = ""; }; DB0CF4D02FEDCA6B0036306B /* Minna.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Minna.app; sourceTree = BUILT_PRODUCTS_DIR; }; DB0CF4DF2FEDCA6D0036306B /* MinnaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MinnaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DB0CF4E92FEDCA6D0036306B /* MinnaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MinnaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -94,6 +93,8 @@ DB6E4C843013E9F800487D86 /* SearchModelAssets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = SearchModelAssets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DB9696A62FF4929600CB037A /* DatabaseSchema */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DatabaseSchema; sourceTree = ""; }; DB9696A72FF4929600CB037A /* MinnaChat */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MinnaChat; sourceTree = ""; }; + DBC0F1A02FF0000100000001 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; + DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.local.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -218,7 +219,7 @@ isa = PBXGroup; children = ( DBC0F1A02FF0000100000001 /* Config.xcconfig */, - DBC0F1A02FF0000100000002 /* Config.local.example.xcconfig */, + DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */, DB0CF7242FEDE88A0036306B /* ci_scripts */, DB0CF4D22FEDCA6B0036306B /* Minna */, DB0CF4E22FEDCA6D0036306B /* MinnaTests */, @@ -458,6 +459,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + DBC1AC60302F6F5E0001A9B4 /* Config.local.xcconfig in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Minna/Database/Import/FileImporter.swift b/Minna/Database/Import/FileImporter.swift index 8c2e323..6751fe9 100644 --- a/Minna/Database/Import/FileImporter.swift +++ b/Minna/Database/Import/FileImporter.swift @@ -14,45 +14,56 @@ import DatabaseSchema import Logging extension View { -// func standardDropDestination(presented: Binding, selectedFolder: Folder?, modelContext: ModelContext, irisContext: IrisContext, database: Database) -> some View { -// self -// .onDrop(of: [.fileURL], isTargeted: presented) { providers in -// for provider in providers { -// provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { item, error in -// guard let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) else { return } -// -// if url.hasDirectoryPath { -// if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.contentTypeKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { -// let urls = enumerator.allObjects.compactMap { $0 as? URL } -// -// -// do { -// try importURLs(urls, selectedFolder: selectedFolder, modelContext: modelContext, irisContext: irisContext, database: database) -// } catch { -// SentrySDK.capture(error: error) -// Log.logger.error("Failed to import files", error: error) -// } -//// -//// for case let fileURL as URL in enumerator { -//// } -// } -// } else { -//// do { -//// let selectedURLs = try result.get() -//// try importURLs(selectedURLs, selectedFolder: selectedFolder, modelContext: modelContext, irisContext: irisContext, database: database) -//// } catch { -//// SentrySDK.capture(error: error) -//// Log.logger.error("Failed to import files", error: error) -//// } -// -// } -// } -// } -// -// return true -// } -// -// } + private func loadProviderItem(provider: NSItemProvider, for type: UTType, options: NSDictionary?) async -> URL? { + await withCheckedContinuation { continuation in + // MAYBE: Pass the progress that is retrieved here backwards so the user can see the import process + _ = provider.loadFileRepresentation(for: type, openInPlace: true) { url, wasOpenedInPlace, error in + guard error == nil, let fileURL = url else { + Log.logger.error("Failed to access dropped item", error: error) + continuation.resume(returning: nil) + return + } + + if wasOpenedInPlace { + continuation.resume(returning: fileURL) + } else { + Log.logger.warning("Was not able to open \(fileURL) in place") + continuation.resume(returning: fileURL) + } + } + } + } + + func standardDropDestination(presented: Binding, selectedFolder: Folder?, modelContext: ModelContext, irisContext: IrisContext, database: Database) -> some View { + self + .dropDestination(for: URL.self) { urls, session in + var scopedURLs: [URL] = [] + + for url in urls { + do { + // OKAY LISTEN UP + // This is a workaround to get security scoped URLs out of a drop destination. By default macOS (and maybe iOS) do not allow you to get security scoped URLs within a drop session. This is because drop sessions are intended to be a form of IPC. When performing IPC you don't really want another app to be able to jump into another app's storage. + // One thing the system does give us is a URL that is resolvable to a bookmark. If we resolve the bookmark with security scope options, and then immediately reconstruct it, we get access to an actual URL that we can use the same as a url from a file importer. + // This is most definitely a hack / workaround. There is a chance this breaks in future versions of the app. In that case we need to prompt the user to open URLs that can't be security scoped using a file importer with its starting file set to the URL we retrieve here. + let data = try url.bookmarkData(options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], includingResourceValuesForKeys: [.contentTypeKey, .isDirectoryKey]) + var isStale: Bool = false + + guard let url = try? URL(resolvingBookmarkData: data, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale) else { throw SecurityScopeError.unableToCreateSecurityScope } + + scopedURLs.append(url) + } catch { + // TODO: Tell the user and have them use the file selector. + Log.logger.error("Failed to gain security scope for \(url)", error: error) + } + } + + do { + try importURLs(scopedURLs, selectedFolder: selectedFolder, modelContext: modelContext, irisContext: irisContext, database: database) + } catch { + Log.logger.error("Failed to import urls in a drop session", error: error) + } + } + } func standardFileImporter(presented: Binding, selectedFolder: Folder?, modelContext: ModelContext, irisContext: IrisContext, database: Database) -> some View { self @@ -76,69 +87,75 @@ extension View { SentrySDK.capture(message: "IrisDB instance was not connected.") return } - + var folder: Folder - + if let selectedFolder { - folder = selectedFolder + folder = selectedFolder } else { - // Capture the unfilled UUID so the predicate operates (it needs local state) - let unfilledUUID = database.unfilledFolderUUID - var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == unfilledUUID }) - descriptor.fetchLimit = 1 - let folders = try modelContext.fetch(descriptor) - guard let unfilledFolder = folders.first else { return } - folder = unfilledFolder + // Capture the unfilled UUID so the predicate operates (it needs local state) + let unfilledUUID = database.unfilledFolderUUID + var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == unfilledUUID }) + descriptor.fetchLimit = 1 + let folders = try modelContext.fetch(descriptor) + guard let unfilledFolder = folders.first else { + Log.logger.error("Failed to find the unfilled folder.") + return + } + folder = unfilledFolder } - + // Track URLs inserted so we do not insert any duplicates. var insertedURLs: Set = [] - + var insertedFiles: [File] = [] - + for url in urls { - let gotAccess = url.startAccessingSecurityScopedResource() - guard gotAccess else { return } - - do { - // URL may be a directory, so this can return many urls. - let files = try FileFactory.files(from: url, in: folder) - - // Local copy of file urls for the search predicate. - let urls = files.compactMap({$0.url}) - - // Skip anything already persisted in the store. - let descriptor = FetchDescriptor(predicate: #Predicate { urls.contains($0.url) }) - - // Find all of the URLs in this set that already exist in the database. - let existingURLs: Set = Set((try? modelContext.fetch(descriptor))?.compactMap({$0.url}) ?? []) - insertedURLs = insertedURLs.union(existingURLs) - - for file in files { - // Skip anything we've already inserted into the database. - guard !insertedURLs.contains(file.url) else { - continue - } - - insertedFiles.append(file) - modelContext.insert(file) - } - } catch { - SentrySDK.capture(error: error) - Log.logger.error("Failed to create file", error: error, metadata: ["url": "\(url)"]) - } - - url.stopAccessingSecurityScopedResource() + let gotAccess = url.startAccessingSecurityScopedResource() + defer { url.stopAccessingSecurityScopedResource() } + guard gotAccess else { + Log.logger.error("Failed to gain security scope for \(url)") + continue + } + + do { + // URL may be a directory, so this can return many urls. + let files = try FileFactory.files(from: url, in: folder) + + // Local copy of file urls for the search predicate. + let urls = files.compactMap({$0.url}) + + // Skip anything already persisted in the store. + let descriptor = FetchDescriptor(predicate: #Predicate { urls.contains($0.url) }) + + // Find all of the URLs in this set that already exist in the database. + let existingURLs: Set = Set((try? modelContext.fetch(descriptor))?.compactMap({$0.url}) ?? []) + insertedURLs = insertedURLs.union(existingURLs) + + for file in files { + // Skip anything we've already inserted into the database. + guard !insertedURLs.contains(file.url) else { + Log.logger.info("Skipping \(file.url) because it has already been imported.") + continue + } + + insertedFiles.append(file) + modelContext.insert(file) + } + } catch { + SentrySDK.capture(error: error) + Log.logger.error("Failed to create file", error: error, metadata: ["url": "\(url)"]) + } } - + // Make sure the database is fully saved so service tasks can access files. try modelContext.save() - + // Run service tasks. These are both async, they will dispatch their own tasks within. for file in insertedFiles { - try irisContext.insert(file) - database.queueDescriptionUpdate(for: file) + Log.logger.info("Inserting \(file.title) into search database.") + try irisContext.insert(file) + database.queueDescriptionUpdate(for: file) } - } } diff --git a/Minna/Views/Navigation/NavigationCore.swift b/Minna/Views/Navigation/NavigationCore.swift index c13825a..962839d 100644 --- a/Minna/Views/Navigation/NavigationCore.swift +++ b/Minna/Views/Navigation/NavigationCore.swift @@ -32,6 +32,8 @@ public struct NavigationCore: View { @State var presentUnknownErrorAlert: Bool = false @State var presentDatabaseInitializationError: Bool = false + @State var isDroppingFile: Bool = false + public var body: some View { NavigationSplitView { List(selection: $navigationRouter.selectedTab) { @@ -94,7 +96,7 @@ public struct NavigationCore: View { modelContext.undoManager = undoManager do { - try irisContext.database + _ = try irisContext.database } catch let error as IrisContextError { switch error { case .notConnected, .unknown: @@ -164,6 +166,7 @@ public struct NavigationCore: View { } message: { Text("Please contact support (support@tryminna.com). Error: \(database.initializationError?.localizedDescription ?? "unknown")") } + .standardDropDestination(presented: $isDroppingFile, selectedFolder: navigationRouter.currentFolder, modelContext: modelContext, irisContext: irisContext, database: database) } func addFolder(in folder: Folder?) { diff --git a/Minna/Views/Navigation/NavigationRouter.swift b/Minna/Views/Navigation/NavigationRouter.swift index a2a3add..66b5aff 100644 --- a/Minna/Views/Navigation/NavigationRouter.swift +++ b/Minna/Views/Navigation/NavigationRouter.swift @@ -19,15 +19,20 @@ final class NavigationRouter { var path: NavigationPath = NavigationPath() var selectedTab: NavigationDestination? = .search + var currentFolder: Folder? = nil + func push(_ chat: Chat) { + currentFolder = chat.file.folder self.path.append(chat) } func push(_ folder: Folder) { + currentFolder = folder self.path.append(folder) } func push(_ file: File) { + currentFolder = file.folder self.path.append(file) } } From 05fa40fc5207a940da63d737b41d2a69cf52d21d Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Sat, 15 Aug 2026 19:56:44 -0400 Subject: [PATCH 2/4] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220e07e..7e23ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format has been adjusted to include a release Title. This has been inserted ## [Unreleased] ### Added +- Files can be drag and dropped into Minna. + - Not all files are supported because of issues in macOS drag and drop behaviors. - Minna is now licensed under the Apache License 2.0. ### Changed From 8a1e534c61ea3a398860e19b583d4d117dd20bc7 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Mon, 17 Aug 2026 13:21:11 -0400 Subject: [PATCH 3/4] Remove config.local.xcconfig --- Minna.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Minna.xcodeproj/project.pbxproj b/Minna.xcodeproj/project.pbxproj index 4eb1098..b78a622 100644 --- a/Minna.xcodeproj/project.pbxproj +++ b/Minna.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ DB9696AC2FF492BF00CB037A /* MinnaChat in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AB2FF492BF00CB037A /* MinnaChat */; }; DB9696AE2FF492C000CB037A /* ModelManager in Frameworks */ = {isa = PBXBuildFile; productRef = DB9696AD2FF492C000CB037A /* ModelManager */; }; DBA6E66130254545002A94F2 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = DBA6E66030254545002A94F2 /* PostHog */; }; - DBC1AC60302F6F5E0001A9B4 /* Config.local.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */; }; DBD7CF502FF5689400A2CA0A /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CF4F2FF5689400A2CA0A /* KeychainSwift */; }; DBD7CFBB2FF59FEC00A2CA0A /* ModernSettingsWindow in Frameworks */ = {isa = PBXBuildFile; productRef = DBD7CFBA2FF59FEC00A2CA0A /* ModernSettingsWindow */; }; DBFD0AA8300EA44F0060744B /* LookAtMe in Frameworks */ = {isa = PBXBuildFile; productRef = DBFD0AA7300EA44F0060744B /* LookAtMe */; }; @@ -94,7 +93,6 @@ DB9696A62FF4929600CB037A /* DatabaseSchema */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DatabaseSchema; sourceTree = ""; }; DB9696A72FF4929600CB037A /* MinnaChat */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MinnaChat; sourceTree = ""; }; DBC0F1A02FF0000100000001 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; - DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.local.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -219,7 +217,6 @@ isa = PBXGroup; children = ( DBC0F1A02FF0000100000001 /* Config.xcconfig */, - DBC1AC5F302F6F5E0001A9B4 /* Config.local.xcconfig */, DB0CF7242FEDE88A0036306B /* ci_scripts */, DB0CF4D22FEDCA6B0036306B /* Minna */, DB0CF4E22FEDCA6D0036306B /* MinnaTests */, @@ -459,7 +456,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBC1AC60302F6F5E0001A9B4 /* Config.local.xcconfig in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 9cc3f6097d8cdf0a803b7e6a8f348c4cb99df8a4 Mon Sep 17 00:00:00 2001 From: Taylor Lineman Date: Mon, 17 Aug 2026 16:57:06 -0400 Subject: [PATCH 4/4] Fixed spelling mistake unfilled -> unfiled --- Minna/Database/Database.swift | 4 +-- Minna/Database/FrontendDatabase.swift | 35 ++++++++++---------- Minna/Database/Import/FileImporter.swift | 12 +++---- Minna/Database/Previews/SampleDatabase.swift | 10 +++--- Minna/Views/Search/SearchStartupView.swift | 4 +-- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/Minna/Database/Database.swift b/Minna/Database/Database.swift index 7bf94b6..e7e2781 100644 --- a/Minna/Database/Database.swift +++ b/Minna/Database/Database.swift @@ -9,9 +9,9 @@ import Foundation import DatabaseSchema protocol Database { - var unfilledFolderUUID: UUID { get } + var unfiledFolderUUID: UUID { get } var initializationError: (any Error)? { get } - func unfilledFolder() -> Folder + func unfiledFolder() -> Folder func queueDescriptionUpdate(for file: File) } diff --git a/Minna/Database/FrontendDatabase.swift b/Minna/Database/FrontendDatabase.swift index 77b4be2..2fcbb35 100644 --- a/Minna/Database/FrontendDatabase.swift +++ b/Minna/Database/FrontendDatabase.swift @@ -17,7 +17,8 @@ import FoundationModels class FrontendDatabase: Database { static let shared: FrontendDatabase = FrontendDatabase() - private static let unfilledFolderKey: String = "unfilled_folder_key" + // Yes there is a spelling mistake in this key, it is too late to change it. + private static let unfiledFolderKey: String = "unfilled_folder_key" // Swift Data Variables var modelContainer: ModelContainer @@ -25,7 +26,7 @@ class FrontendDatabase: Database { modelContainer.mainContext } - var unfilledFolderUUID: UUID + var unfiledFolderUUID: UUID var initializationError: (any Error)? private var fileDescriptionWriter: FileDescriptionWriter @@ -33,12 +34,12 @@ class FrontendDatabase: Database { private let indexingQueue: RateLimitedQueue = RateLimitedQueue() init() { - if let uuidString = UserDefaults.standard.object(forKey: FrontendDatabase.unfilledFolderKey) as? String, let uuid = UUID(uuidString: uuidString) { - unfilledFolderUUID = uuid + if let uuidString = UserDefaults.standard.object(forKey: FrontendDatabase.unfiledFolderKey) as? String, let uuid = UUID(uuidString: uuidString) { + unfiledFolderUUID = uuid } else { - // TODO: Try and find an existing unfilled folder UUID in case the user defaults got wiped. - unfilledFolderUUID = UUID() - UserDefaults.standard.set(unfilledFolderUUID.uuidString, forKey: FrontendDatabase.unfilledFolderKey) + // TODO: Try and find an existing unfiled folder UUID in case the user defaults got wiped. + unfiledFolderUUID = UUID() + UserDefaults.standard.set(unfiledFolderUUID.uuidString, forKey: FrontendDatabase.unfiledFolderKey) } let modelConfiguration = ModelConfiguration(schema: Schema.minnaSchema) @@ -83,25 +84,25 @@ class FrontendDatabase: Database { } private func populateStartupData() throws { - try populateUnfilledFolder() + try populateunfiledFolder() try populateAppleProvider() try populateMLXProvider() try context.save() } - private func populateUnfilledFolder() throws { - let uuid = unfilledFolderUUID + private func populateunfiledFolder() throws { + let uuid = unfiledFolderUUID var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == uuid }) descriptor.fetchLimit = 1 guard try context.fetch(descriptor).isEmpty else { return } - let unfilledFolder = Folder(uuid: unfilledFolderUUID, name: "Unfilled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), protected: true) - context.insert(unfilledFolder) + let unfiledFolder = Folder(uuid: unfiledFolderUUID, name: "Unfiled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), protected: true) + context.insert(unfiledFolder) } - public func unfilledFolder() -> Folder { - let uuid = unfilledFolderUUID + public func unfiledFolder() -> Folder { + let uuid = unfiledFolderUUID var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == uuid }) descriptor.fetchLimit = 1 @@ -109,9 +110,9 @@ class FrontendDatabase: Database { return folder } - let unfilledFolder = Folder(uuid: unfilledFolderUUID, name: "Unfilled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), protected: true) - context.insert(unfilledFolder) - return unfilledFolder + let unfiledFolder = Folder(uuid: unfiledFolderUUID, name: "Unfiled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), protected: true) + context.insert(unfiledFolder) + return unfiledFolder } private func populateAppleProvider() throws { diff --git a/Minna/Database/Import/FileImporter.swift b/Minna/Database/Import/FileImporter.swift index 6751fe9..5419052 100644 --- a/Minna/Database/Import/FileImporter.swift +++ b/Minna/Database/Import/FileImporter.swift @@ -93,16 +93,16 @@ extension View { if let selectedFolder { folder = selectedFolder } else { - // Capture the unfilled UUID so the predicate operates (it needs local state) - let unfilledUUID = database.unfilledFolderUUID - var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == unfilledUUID }) + // Capture the unfiled UUID so the predicate operates (it needs local state) + let unfiledUUID = database.unfiledFolderUUID + var descriptor = FetchDescriptor(predicate: #Predicate { $0.uuid == unfiledUUID }) descriptor.fetchLimit = 1 let folders = try modelContext.fetch(descriptor) - guard let unfilledFolder = folders.first else { - Log.logger.error("Failed to find the unfilled folder.") + guard let unfiledFolder = folders.first else { + Log.logger.error("Failed to find the unfiled folder.") return } - folder = unfilledFolder + folder = unfiledFolder } // Track URLs inserted so we do not insert any duplicates. diff --git a/Minna/Database/Previews/SampleDatabase.swift b/Minna/Database/Previews/SampleDatabase.swift index beb91e6..10493ab 100644 --- a/Minna/Database/Previews/SampleDatabase.swift +++ b/Minna/Database/Previews/SampleDatabase.swift @@ -28,7 +28,7 @@ class SampleDatabase: Database { static let shared = SampleDatabase() public var sampleFolders: [Folder] = [ - Folder(name: "Unfilled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), files: [ + Folder(name: "Unfiled", icon: FolderIcon(symbol: .symbol(SFSymbol.trayFull.rawValue), color: .champagne), files: [ ], protected: true), Folder(name: "Coding", icon: FolderIcon(symbol: .symbol(SFSymbol.ellipsisCurlybraces.rawValue), color: .azure), children: [ @@ -279,7 +279,7 @@ class SampleDatabase: Database { ] let modelContainer: ModelContainer - var unfilledFolderUUID: UUID = UUID() + var unfiledFolderUUID: UUID = UUID() var initializationError: (any Error)? var context: ModelContext { @@ -299,8 +299,8 @@ class SampleDatabase: Database { private func populateSampleData() throws { for folder in sampleFolders { - if folder.name == "Unfilled" { - unfilledFolderUUID = folder.uuid + if folder.name == "Unfiled" { + unfiledFolderUUID = folder.uuid } context.insert(folder) @@ -317,7 +317,7 @@ class SampleDatabase: Database { context.insert(appleProvider) } - func unfilledFolder() -> Folder { + func unfiledFolder() -> Folder { return sampleFolders.first! } diff --git a/Minna/Views/Search/SearchStartupView.swift b/Minna/Views/Search/SearchStartupView.swift index 94573c2..80b7050 100644 --- a/Minna/Views/Search/SearchStartupView.swift +++ b/Minna/Views/Search/SearchStartupView.swift @@ -36,13 +36,13 @@ struct SearchStartupView: View { } .onAppear { if draft == nil { - draft = Chat.make(in: database.unfilledFolder()) + draft = Chat.make(in: database.unfiledFolder()) } } } private func startNewChat() { - draft = Chat.make(in: database.unfilledFolder()) + draft = Chat.make(in: database.unfiledFolder()) } }