From 85d1721eae5662b738ad94a34a95574ef6a34e51 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 5 Feb 2026 14:55:57 +0100 Subject: [PATCH 01/35] cleaning Signed-off-by: Marino Faggiana --- iOSClient/Files/NCFiles.swift | 41 ++++++++++--------- .../NCCollectionViewCommon.swift | 10 +++-- .../en.lproj/Localizable.strings | 1 + 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index 0abb5a8221..5147dc4c74 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -185,27 +185,11 @@ class NCFiles: NCCollectionViewCommon { startSyncMetadata(metadatas: self.dataSource.getMetadatas()) } - Task { - await networking.networkingTasks.cancel(identifier: "\(self.serverUrl)_NCFiles") - } + await networking.networkingTasks.cancel(identifier: "\(self.serverUrl)_NCFiles") guard !isSearchingMode else { - return networkSearch() - } - - func downloadMetadata(_ metadata: tableMetadata) async -> Bool { - let fileSize = utilityFileSystem.fileProviderStorageSize(metadata.ocId, - fileName: metadata.fileNameView, - userId: metadata.userId, - urlBase: metadata.urlBase) - guard fileSize > 0 else { return false } - - if let tblLocalFile = await database.getTableLocalFileAsync(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { - if tblLocalFile.etag != metadata.etag { - return true - } - } - return false + await networkSearch() + return } let resultsReadFolder = await networkReadFolderAsync(serverUrl: self.serverUrl, forced: forced) @@ -216,7 +200,7 @@ class NCFiles: NCCollectionViewCommon { let metadatasForDownload: [tableMetadata] = resultsReadFolder.metadatas ?? self.dataSource.getMetadatas() Task.detached(priority: .utility) { for metadata in metadatasForDownload where !metadata.directory { - if await downloadMetadata(metadata) { + if await self.downloadMetadata(metadata) { if let metadata = await self.database.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, session: NCNetworking.shared.sessionDownload, selector: NCGlobal.shared.selectorDownloadFile, @@ -230,6 +214,23 @@ class NCFiles: NCCollectionViewCommon { await self.reloadDataSource() } + private func downloadMetadata(_ metadata: tableMetadata) async -> Bool { + let fileSize = utilityFileSystem.fileProviderStorageSize(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + guard fileSize > 0 else { + return false + } + + if let tblLocalFile = await database.getTableLocalFileAsync(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { + if tblLocalFile.etag != metadata.etag { + return true + } + } + return false + } + private func networkReadFolderAsync(serverUrl: String, forced: Bool) async -> (metadatas: [tableMetadata]?, error: NKError, reloadRequired: Bool) { var reloadRequired: Bool = false let resultsReadFile = await NCNetworking.shared.readFileAsync(serverUrlFileName: serverUrl, account: session.account) { task in diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 898dcdc97c..96dbd96b50 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -493,7 +493,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { if isSearchingMode && self.literalSearch?.count ?? 0 >= 2 { - networkSearch() + Task { + await networkSearch() + } } } @@ -657,7 +659,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func getServerData(forced: Bool = false) async { } - @objc func networkSearch() { + func networkSearch() async { guard !networkSearchInProgress else { return } @@ -936,7 +938,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { if self.isSearchingMode { await self.debouncerNetworkSearch.call { - self.networkSearch() + await self.networkSearch() } } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { await self.debouncerReloadDataSource.call { @@ -950,7 +952,7 @@ extension NCCollectionViewCommon: NCTransferDelegate { Task { if self.isSearchingMode { await self.debouncerNetworkSearch.call { - self.networkSearch() + await self.networkSearch() } return } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index fa5310d83b..e50e249d73 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -676,6 +676,7 @@ "_large_upload_tip_" = "Large files require the app to remain open until the transfer is complete"; "_e2ee_upload_tip_" = "End-to-end files require the app to remain open until the transfer is complete"; "_finalizing_wait_" = "Waiting for finalization …"; +"_in_this_folder_" = "In this folder"; // Tip "_tip_pdf_thumbnails_" = "Swipe left from the right edge of the screen to show the thumbnails"; From 4c20ca09f43f63357a4cc74a0a9bf701d82ce7fe Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 5 Feb 2026 15:15:46 +0100 Subject: [PATCH 02/35] networking.searchFiles(literal: Signed-off-by: Marino Faggiana --- .../NCCollectionViewCommon.swift | 45 +++++++------------ .../Networking/NCNetworking+WebDAV.swift | 34 +++++++------- 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 96dbd96b50..24ec9f41c0 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -668,13 +668,11 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, !literalSearch.isEmpty else { return } - let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() + let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) self.networkSearchInProgress = true self.dataSource.removeAll() - Task { - await self.reloadDataSource() - } + await self.reloadDataSource() if capabilities.serverVersionMajor >= global.nextcloudVersion20 { self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in @@ -700,35 +698,26 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.networkSearchInProgress = false } } else { - self.networking.searchFiles(literal: literalSearch, account: session.account) { task in + let results = await self.networking.searchFiles(literal: literalSearch, account: session.account) { task in self.searchDataSourceTask = task Task { await self.reloadDataSource() } - } completion: { metadatasSearch, error in - Task { - guard let metadatasSearch, - error == .success, - self.isSearchingMode - else { - self.networkSearchInProgress = false - await self.reloadDataSource() - return - } - let ocId = metadatasSearch.map { $0.ocId } - let metadatas = await self.database.getMetadatasAsync(predicate: NSPredicate(format: "ocId IN %@", ocId), - withLayout: self.layoutForView, - withAccount: self.session.account) - - self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, - layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, - account: self.session.account) - self.networkSearchInProgress = false - await self.reloadDataSource() - } } + if let ocIds = results.ocIds, results.error == .success { + let metadatas = await self.database.getMetadatasAsync( + predicate: NSPredicate(format: "ocId IN %@", ocIds), + withLayout: self.layoutForView, + withAccount: self.session.account + ) + self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, + layoutForView: self.layoutForView, + providers: self.providers, + searchResults: self.searchResults, + account: self.session.account) + } + self.networkSearchInProgress = false + await self.reloadDataSource() } } diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 89c6d80463..5640b7becf 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -836,33 +836,31 @@ extension NCNetworking { // MARK: - Search /// WebDAV search - func searchFiles(literal: String, - account: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ metadatas: [tableMetadata]?, _ error: NKError) -> Void) { + func searchFiles(literal: String, account: String, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (ocIds: [String]?, error: NKError) { let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: account) let serverUrl = NCSession.shared.getSession(account: account).urlBase - NextcloudKit.shared.searchLiteral(serverUrl: serverUrl, - depth: "infinity", - literal: literal, - showHiddenFiles: showHiddenFiles, - account: account, - options: NKRequestOptions(queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)) { task in + + let results = await NextcloudKit.shared.searchLiteralAsync(serverUrl: serverUrl, + depth: "infinity", + literal: literal, + showHiddenFiles: showHiddenFiles, + account: account) { task in + taskHandler(task) Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, path: serverUrl, name: "searchLiteral") await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } - taskHandler(task) - } completion: { _, files, _, error in - guard error == .success, let files else { return completion(nil, error) } + } - Task { - let (_, metadatas) = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(files) - NCManageDatabase.shared.addMetadatas(metadatas) - completion(metadatas, error) - } + if results.error == .success, let files = results.files { + let (_, metadatas) = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(files) + NCManageDatabase.shared.addMetadatas(metadatas) + let ocIds = metadatas.map { $0.ocId } + return(ocIds, .success) + } else { + return(nil, results.error) } } From d02206cb39ee52bc3fdfbf40fa950107ba5d54da Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 5 Feb 2026 15:52:51 +0100 Subject: [PATCH 03/35] code Signed-off-by: Marino Faggiana --- .../Networking/NCNetworking+WebDAV.swift | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 5640b7becf..1279e9208b 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -836,7 +836,10 @@ extension NCNetworking { // MARK: - Search /// WebDAV search - func searchFiles(literal: String, account: String, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }) async -> (ocIds: [String]?, error: NKError) { + func searchFiles(literal: String, + account: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> (ocIds: [String]?, error: NKError) { let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: account) let serverUrl = NCSession.shared.getSession(account: account).urlBase @@ -866,6 +869,93 @@ extension NCNetworking { /// Unified Search (NC>=20) /// + func unifiedSearchFiles(literal: String, + account: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, + update: @escaping (_ account: String, _ id: String, NKSearchResult?, [tableMetadata]?) -> Void + ) async { + let session = NCSession.shared.getSession(account: account) + let results = await NextcloudKit.shared.unifiedSearchAsync(term: literal, + timeout: 30, + timeoutProvider: 90, + account: account) { _ in + // example filter + // ["calendar", "files", "fulltextsearch"].contains(provider.id) + return true + } request: { request in + if let request { + self.requestsUnifiedSearch.append(request) + } + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: literal, + name: "unifiedSearch") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + taskHandler(task) + } providers: { account, searchProviders in + providers(account, searchProviders) + } update: { account, searchResult, provider, error in + guard let searchResult else { + return + } + Task { + var metadatas: [tableMetadata] = [] + + switch provider.id { + case "files": + for entry in searchResult.entries { + if let filePath = entry.filePath { + if let metadata = await self.loadMetadata(session: session, filePath: filePath) { + metadatas.append(metadata) + } + } else { + print(#function, "[ERROR]: File search entry has no path: \(entry)") + } + } + Task { + update(account, provider.id, searchResult, metadatas) + } + case "fulltextsearch": + // NOTE: FTS could also return attributes like files + // https://github.com/nextcloud/files_fulltextsearch/issues/143 + for entry in searchResult.entries { + let url = URLComponents(string: entry.resourceURL) + guard let dir = url?.queryItems?["dir"]?.value, let filename = url?.queryItems?["scrollto"]?.value else { return } + if let metadata = await NCManageDatabase.shared.getMetadataAsync( + predicate: NSPredicate(format: "account == %@ && path == %@ && fileName == %@", session.account, "/remote.php/dav/files/" + session.user + dir, filename)) { + metadatas.append(metadata) + } else { + if let metadata = await self.loadMetadata(session: session, filePath: dir + filename) { + metadatas.append(metadata) + } + } + + } + update(account, provider.id, searchResult, metadatas) + default: + for entry in searchResult.entries { + let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( + fileName: entry.title, + ocId: NSUUID().uuidString, + serverUrl: session.urlBase, + url: entry.resourceURL, + isUrl: true, + name: searchResult.id, + subline: entry.subline, + iconUrl: entry.thumbnailURL, + session: session, + sceneIdentifier: nil) + metadatas.append(metadata) + } + update(account, provider.id, searchResult, metadatas) + } + } + } + } + func unifiedSearchFiles(literal: String, account: String, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, @@ -1066,6 +1156,18 @@ extension NCNetworking { completion(account, returnMetadata, error) } } + + private func loadMetadata(session: NCSession.Session, + filePath: String) async -> tableMetadata? { + let urlPath = session.urlBase + "/remote.php/dav/files/" + session.user + filePath + let results = await self.readFileAsync(serverUrlFileName: urlPath, account: session.account) + guard let metadata = results.metadata else { + return nil + } + + NCManageDatabase.shared.addMetadata(metadata) + return metadata + } } class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { From e904d8b280c6b4740e5ce91376a4c42af5fa5bcf Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 5 Feb 2026 18:31:06 +0100 Subject: [PATCH 04/35] code Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 8 +- .../NCCollectionViewCommon.swift | 68 ++-- .../Networking/NCNetworking+Search.swift | 182 ++++++++++ .../Networking/NCNetworking+WebDAV.swift | 336 ------------------ 4 files changed, 226 insertions(+), 368 deletions(-) create mode 100644 iOSClient/Networking/NCNetworking+Search.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e0a2d32314..f3c24c8e2a 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -265,6 +265,8 @@ F71F6D0B2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */; }; F71F6D0C2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */; }; F71F6D0D2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */; }; + F71FA7982F35088200E86192 /* NCNetworking+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71FA7972F35088000E86192 /* NCNetworking+Search.swift */; }; + F71FA7992F3508C600E86192 /* NCNetworking+WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */; }; F722133B2D40EF9D002F7438 /* NCFilesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F722133A2D40EF8C002F7438 /* NCFilesNavigationController.swift */; }; F7226EDC1EE4089300EBECB1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7226EDB1EE4089300EBECB1 /* Main.storyboard */; }; F722F0112CFF569500065FB5 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F722F0102CFF569500065FB5 /* MainInterface.storyboard */; }; @@ -308,7 +310,6 @@ F7327E202B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E1F2B73A42F00A462C7 /* NCNetworking+Download.swift */; }; F7327E232B73A42F00A462C7 /* NCNetworking+Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E1F2B73A42F00A462C7 /* NCNetworking+Download.swift */; }; F7327E302B73A86700A462C7 /* NCNetworking+WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */; }; - F7327E322B73A86700A462C7 /* NCNetworking+WebDAV.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */; }; F7327E352B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7327E342B73AEDE00A462C7 /* NCNetworking+LivePhoto.swift */; }; F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7AC1CAF28AB94490032D99F /* Array+Extension.swift */; }; F732D23327CF8AED000B0F1B /* NCPlayerToolBar.xib in Resources */ = {isa = PBXBuildFile; fileRef = F732D23227CF8AED000B0F1B /* NCPlayerToolBar.xib */; }; @@ -1335,6 +1336,7 @@ F71CFA662F2A07C6007A3AE9 /* NCMedia+Netwoking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+Netwoking.swift"; sourceTree = ""; }; F71D2FB62E09BBD700B751CC /* NCAutoUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUploadModel.swift; sourceTree = ""; }; F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSafeArray.swift; sourceTree = ""; }; + F71FA7972F35088000E86192 /* NCNetworking+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCNetworking+Search.swift"; sourceTree = ""; }; F722133A2D40EF8C002F7438 /* NCFilesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFilesNavigationController.swift; sourceTree = ""; }; F7226EDB1EE4089300EBECB1 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; F722F0102CFF569500065FB5 /* MainInterface.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MainInterface.storyboard; sourceTree = ""; }; @@ -2426,6 +2428,7 @@ F785129A2D79899E0087DDD0 /* NCNetworking+TermsOfService.swift */, F71916102E2901E800E13E96 /* NCNetworking+Upload.swift */, F7327E2F2B73A86700A462C7 /* NCNetworking+WebDAV.swift */, + F71FA7972F35088000E86192 /* NCNetworking+Search.swift */, F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */, F755BD9A20594AC7008C5FBB /* NCService.swift */, ); @@ -4292,10 +4295,10 @@ AF22B208277B4E4C00DAB0CC /* NCCreateFormUploadConflictCell.swift in Sources */, F73EF7C22B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, F7148041262EBE4000693E51 /* NCShareExtension.swift in Sources */, + F71FA7992F3508C600E86192 /* NCNetworking+WebDAV.swift in Sources */, F76B3CCF1EAE01BD00921AC9 /* NCBrand.swift in Sources */, F72944F32A84246400246839 /* NCEndToEndMetadataV20.swift in Sources */, F7BAADCC1ED5A87C00B7EAD4 /* NCManageDatabase.swift in Sources */, - F7327E322B73A86700A462C7 /* NCNetworking+WebDAV.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4491,6 +4494,7 @@ AFA2AC8527849604008E1EA7 /* NCActivityCommentView.swift in Sources */, AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */, F75A9EE623796C6F0044CFCE /* NCNetworking.swift in Sources */, + F71FA7982F35088200E86192 /* NCNetworking+Search.swift in Sources */, AA8D31552D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift in Sources */, F758B460212C56A400515F55 /* NCScan.swift in Sources */, F76882262C0DD1E7001CF441 /* NCSettingsView.swift in Sources */, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 24ec9f41c0..662999aff4 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -675,9 +675,10 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.reloadDataSource() if capabilities.serverVersionMajor >= global.nextcloudVersion20 { - self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in - self.searchDataSourceTask = task + await self.networking.unifiedSearchFiles(literal: literalSearch, + account: session.account) { task in Task { + self.searchDataSourceTask = task await self.reloadDataSource() } } providers: { account, searchProviders in @@ -688,14 +689,14 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, providers: self.providers, searchResults: self.searchResults, account: account) - } update: { _, _, searchResult, metadatas in - guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, let searchResult else { return } - self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) - } completion: { _, _ in - Task { - await self.reloadDataSource() + } update: { account, id, searchResult, metadatas in + guard let metadatas, + !metadatas.isEmpty, + self.isSearchingMode, + let searchResult else { + return } - self.networkSearchInProgress = false + self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) } } else { let results = await self.networking.searchFiles(literal: literalSearch, account: session.account) { task in @@ -721,38 +722,43 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } } - func unifiedSearchMore(metadataForSection: NCMetadataForSection?) { + func unifiedSearchMore(metadataForSection: NCMetadataForSection?) async { guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) } - self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in - self.searchDataSourceTask = task + let results = await self.networking.unifiedSearchFilesProvider(providerId: lastSearchResult.id, + term: term, + limit: 20, + cursor: cursor, + account: session.account) { task in Task { + self.searchDataSourceTask = task await self.reloadDataSource() } - } completion: { _, searchResult, metadatas, error in - if error != .success { - Task { - await showErrorBanner(controller: self.controller, text: error.errorDescription, errorCode: error.errorCode) - } - } - - metadataForSection.unifiedSearchInProgress = false - guard let searchResult = searchResult, let metadatas = metadatas else { return } - self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + } + if results.error != .success { Task { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil) - } + await showErrorBanner(controller: self.controller, text: results.error.errorDescription, errorCode: results.error.errorCode) } } + + guard results.error == .success, + let searchResult = results.searchResult, + let metadatas = results.metadatas else { + return + } + + metadataForSection.unifiedSearchInProgress = false + self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) + + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } // MARK: - Push metadata @@ -884,7 +890,9 @@ extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { extension NCCollectionViewCommon: NCSectionFooterDelegate { func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) + Task { + await unifiedSearchMore(metadataForSection: metadataForSection) + } } } diff --git a/iOSClient/Networking/NCNetworking+Search.swift b/iOSClient/Networking/NCNetworking+Search.swift new file mode 100644 index 0000000000..6c583dd576 --- /dev/null +++ b/iOSClient/Networking/NCNetworking+Search.swift @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +extension NCNetworking { + func searchFiles(literal: String, + account: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } + ) async -> (ocIds: [String]?, error: NKError) { + let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: account) + let serverUrl = NCSession.shared.getSession(account: account).urlBase + + let results = await NextcloudKit.shared.searchLiteralAsync(serverUrl: serverUrl, + depth: "infinity", + literal: literal, + showHiddenFiles: showHiddenFiles, + account: account) { task in + taskHandler(task) + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: serverUrl, + name: "searchLiteral") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + + if results.error == .success, let files = results.files { + let (_, metadatas) = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(files) + NCManageDatabase.shared.addMetadatas(metadatas) + let ocIds = metadatas.map { $0.ocId } + return(ocIds, .success) + } else { + return(nil, results.error) + } + } + + func unifiedSearchFiles(literal: String, + account: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, + update: @escaping (_ account: String, _ id: String, NKSearchResult?, [tableMetadata]?) -> Void + ) async { + let session = NCSession.shared.getSession(account: account) + let results = await NextcloudKit.shared.unifiedSearchAsync(term: literal, + timeout: 30, + timeoutProvider: 90, + account: account) { _ in + // example filter + // ["calendar", "files", "fulltextsearch"].contains(provider.id) + return true + } request: { request in + if let request { + self.requestsUnifiedSearch.append(request) + } + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: literal, + name: "unifiedSearch") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + taskHandler(task) + } providers: { account, searchProviders in + providers(account, searchProviders) + } update: { account, searchResult, provider, error in + guard let searchResult else { + return + } + Task { + let metadatas = await self.getSearchResultMetadatas(session: session, providerId: provider.id, searchResult: searchResult) + update(account, provider.id, searchResult, metadatas) + } + } + } + + func unifiedSearchFilesProvider(providerId: String, + term: String, + limit: Int, cursor: Int, + account: String, + taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, + ) async -> (searchResult: NKSearchResult?, metadatas: [tableMetadata]?, error: NKError) { + let session = NCSession.shared.getSession(account: account) + let results = await NextcloudKit.shared.searchProviderAsync(providerId, + term: term, + limit: limit, + cursor: cursor, + timeout: 60, + account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: term, + name: "searchProvider") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + taskHandler(task) + } + guard let searchResult = results.searchResult else { + return(nil, nil, results.error) + } + let metadatas = await getSearchResultMetadatas(session: session, providerId: providerId, searchResult: searchResult) + + return(searchResult, metadatas, results.error) + } + + private func getSearchResultMetadatas(session: NCSession.Session, + providerId: String, + searchResult: NKSearchResult, + ) async -> [tableMetadata]? { + var metadatas: [tableMetadata] = [] + + switch providerId { + case "files": + for entry in searchResult.entries { + if let filePath = entry.filePath { + if let metadata = await self.loadMetadata(session: session, filePath: filePath) { + metadatas.append(metadata) + } + } else { + print(#function, "[ERROR]: File search entry has no path: \(entry)") + } + } + + return(metadatas) + + case "fulltextsearch": + for entry in searchResult.entries { + let url = URLComponents(string: entry.resourceURL) + guard let dir = url?.queryItems?["dir"]?.value, let filename = url?.queryItems?["scrollto"]?.value else { + return(nil) + } + if let metadata = await NCManageDatabase.shared.getMetadataAsync( + predicate: NSPredicate(format: "account == %@ && path == %@ && fileName == %@", session.account, "/remote.php/dav/files/" + session.user + dir, filename)) { + metadatas.append(metadata) + } else { + if let metadata = await self.loadMetadata(session: session, filePath: dir + filename) { + metadatas.append(metadata) + } + } + + } + return(metadatas) + default: + for entry in searchResult.entries { + let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( + fileName: entry.title, + ocId: NSUUID().uuidString, + serverUrl: session.urlBase, + url: entry.resourceURL, + isUrl: true, + name: searchResult.id, + subline: entry.subline, + iconUrl: entry.thumbnailURL, + session: session, + sceneIdentifier: nil) + metadatas.append(metadata) + } + return(metadatas) + } + } + + func cancelUnifiedSearchFiles() { + for request in requestsUnifiedSearch { + request.cancel() + } + requestsUnifiedSearch.removeAll() + } + + private func loadMetadata(session: NCSession.Session, + filePath: String) async -> tableMetadata? { + let urlPath = session.urlBase + "/remote.php/dav/files/" + session.user + filePath + let results = await self.readFileAsync(serverUrlFileName: urlPath, account: session.account) + guard let metadata = results.metadata else { + return nil + } + + NCManageDatabase.shared.addMetadata(metadata) + return metadata + } +} diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 1279e9208b..376b3819ba 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -832,342 +832,6 @@ extension NCNetworking { } } } - - // MARK: - Search - - /// WebDAV search - func searchFiles(literal: String, - account: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in } - ) async -> (ocIds: [String]?, error: NKError) { - let showHiddenFiles = NCPreferences().getShowHiddenFiles(account: account) - let serverUrl = NCSession.shared.getSession(account: account).urlBase - - let results = await NextcloudKit.shared.searchLiteralAsync(serverUrl: serverUrl, - depth: "infinity", - literal: literal, - showHiddenFiles: showHiddenFiles, - account: account) { task in - taskHandler(task) - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - path: serverUrl, - name: "searchLiteral") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } - - if results.error == .success, let files = results.files { - let (_, metadatas) = await NCManageDatabaseCreateMetadata().convertFilesToMetadatasAsync(files) - NCManageDatabase.shared.addMetadatas(metadatas) - let ocIds = metadatas.map { $0.ocId } - return(ocIds, .success) - } else { - return(nil, results.error) - } - } - - /// Unified Search (NC>=20) - /// - func unifiedSearchFiles(literal: String, - account: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, - update: @escaping (_ account: String, _ id: String, NKSearchResult?, [tableMetadata]?) -> Void - ) async { - let session = NCSession.shared.getSession(account: account) - let results = await NextcloudKit.shared.unifiedSearchAsync(term: literal, - timeout: 30, - timeoutProvider: 90, - account: account) { _ in - // example filter - // ["calendar", "files", "fulltextsearch"].contains(provider.id) - return true - } request: { request in - if let request { - self.requestsUnifiedSearch.append(request) - } - } taskHandler: { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - path: literal, - name: "unifiedSearch") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - taskHandler(task) - } providers: { account, searchProviders in - providers(account, searchProviders) - } update: { account, searchResult, provider, error in - guard let searchResult else { - return - } - Task { - var metadatas: [tableMetadata] = [] - - switch provider.id { - case "files": - for entry in searchResult.entries { - if let filePath = entry.filePath { - if let metadata = await self.loadMetadata(session: session, filePath: filePath) { - metadatas.append(metadata) - } - } else { - print(#function, "[ERROR]: File search entry has no path: \(entry)") - } - } - Task { - update(account, provider.id, searchResult, metadatas) - } - case "fulltextsearch": - // NOTE: FTS could also return attributes like files - // https://github.com/nextcloud/files_fulltextsearch/issues/143 - for entry in searchResult.entries { - let url = URLComponents(string: entry.resourceURL) - guard let dir = url?.queryItems?["dir"]?.value, let filename = url?.queryItems?["scrollto"]?.value else { return } - if let metadata = await NCManageDatabase.shared.getMetadataAsync( - predicate: NSPredicate(format: "account == %@ && path == %@ && fileName == %@", session.account, "/remote.php/dav/files/" + session.user + dir, filename)) { - metadatas.append(metadata) - } else { - if let metadata = await self.loadMetadata(session: session, filePath: dir + filename) { - metadatas.append(metadata) - } - } - - } - update(account, provider.id, searchResult, metadatas) - default: - for entry in searchResult.entries { - let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( - fileName: entry.title, - ocId: NSUUID().uuidString, - serverUrl: session.urlBase, - url: entry.resourceURL, - isUrl: true, - name: searchResult.id, - subline: entry.subline, - iconUrl: entry.thumbnailURL, - session: session, - sceneIdentifier: nil) - metadatas.append(metadata) - } - update(account, provider.id, searchResult, metadatas) - } - } - } - } - - func unifiedSearchFiles(literal: String, - account: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - providers: @escaping (_ accout: String, _ searchProviders: [NKSearchProvider]?) -> Void, - update: @escaping (_ account: String, _ id: String, NKSearchResult?, [tableMetadata]?) -> Void, - completion: @escaping (_ account: String, _ error: NKError) -> Void) { - let dispatchGroup = DispatchGroup() - let session = NCSession.shared.getSession(account: account) - dispatchGroup.enter() - dispatchGroup.notify(queue: .main) { - completion(session.account, NKError()) - } - - NextcloudKit.shared.unifiedSearch(term: literal, timeout: 30, timeoutProvider: 90, account: session.account) { _ in - // example filter - // ["calendar", "files", "fulltextsearch"].contains(provider.id) - return true - } request: { request in - if let request = request { - self.requestsUnifiedSearch.append(request) - } - } taskHandler: { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - path: literal, - name: "unifiedSearch") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - taskHandler(task) - } providers: { account, searchProviders in - providers(account, searchProviders) - } update: { account, partialResult, provider, _ in - guard let partialResult = partialResult else { - return - } - var metadatas: [tableMetadata] = [] - - switch provider.id { - case "files": - partialResult.entries.forEach({ entry in - if let filePath = entry.filePath { - let semaphore = DispatchSemaphore(value: 0) - self.loadMetadata(session: session, filePath: filePath, dispatchGroup: dispatchGroup) { _, metadata, _ in - metadatas.append(metadata) - semaphore.signal() - } - semaphore.wait() - } else { - print(#function, "[ERROR]: File search entry has no path: \(entry)") - } - }) - update(account, provider.id, partialResult, metadatas) - case "fulltextsearch": - // NOTE: FTS could also return attributes like files - // https://github.com/nextcloud/files_fulltextsearch/issues/143 - partialResult.entries.forEach({ entry in - let url = URLComponents(string: entry.resourceURL) - guard let dir = url?.queryItems?["dir"]?.value, let filename = url?.queryItems?["scrollto"]?.value else { return } - if let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ && path == %@ && fileName == %@", - session.account, - "/remote.php/dav/files/" + session.user + dir, - filename)) { - metadatas.append(metadata) - } else { - let semaphore = DispatchSemaphore(value: 0) - self.loadMetadata(session: session, filePath: dir + filename, dispatchGroup: dispatchGroup) { _, metadata, _ in - metadatas.append(metadata) - semaphore.signal() - } - semaphore.wait() - } - }) - update(account, provider.id, partialResult, metadatas) - default: - Task { - for entry in partialResult.entries { - let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( - fileName: entry.title, - ocId: NSUUID().uuidString, - serverUrl: session.urlBase, - url: entry.resourceURL, - isUrl: true, - name: partialResult.id, - subline: entry.subline, - iconUrl: entry.thumbnailURL, - session: session, - sceneIdentifier: nil) - metadatas.append(metadata) - } - update(account, provider.id, partialResult, metadatas) - } - } - } completion: { _, _, _ in - self.requestsUnifiedSearch.removeAll() - dispatchGroup.leave() - } - } - - func unifiedSearchFilesProvider(id: String, term: String, - limit: Int, cursor: Int, - account: String, - taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, - completion: @escaping (_ account: String, _ searchResult: NKSearchResult?, _ metadatas: [tableMetadata]?, _ error: NKError) -> Void) { - var metadatas: [tableMetadata] = [] - let session = NCSession.shared.getSession(account: account) - let request = NextcloudKit.shared.searchProvider(id, term: term, limit: limit, cursor: cursor, timeout: 60, account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - path: term, - name: "searchProvider") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - taskHandler(task) - } completion: { account, searchResult, _, error in - guard let searchResult = searchResult else { - return completion(account, nil, metadatas, error) - } - - switch id { - case "files": - searchResult.entries.forEach({ entry in - if let fileId = entry.fileId, let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ && fileId == %@", session.account, String(fileId))) { - metadatas.append(metadata) - } else if let filePath = entry.filePath { - let semaphore = DispatchSemaphore(value: 0) - self.loadMetadata(session: session, filePath: filePath, dispatchGroup: nil) { _, metadata, _ in - metadatas.append(metadata) - semaphore.signal() - } - semaphore.wait() - } else { print(#function, "[ERROR]: File search entry has no path: \(entry)") } - }) - completion(account, searchResult, metadatas, error) - case "fulltextsearch": - // NOTE: FTS could also return attributes like files - // https://github.com/nextcloud/files_fulltextsearch/issues/143 - searchResult.entries.forEach({ entry in - let url = URLComponents(string: entry.resourceURL) - guard let dir = url?.queryItems?["dir"]?.value, let filename = url?.queryItems?["scrollto"]?.value else { return } - if let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ && path == %@ && fileName == %@", - session.account, - "/remote.php/dav/files/" + session.user + dir, filename)) { - metadatas.append(metadata) - } else { - let semaphore = DispatchSemaphore(value: 0) - self.loadMetadata(session: session, filePath: dir + filename, dispatchGroup: nil) { _, metadata, _ in - metadatas.append(metadata) - semaphore.signal() - } - semaphore.wait() - } - }) - completion(account, searchResult, metadatas, error) - default: - Task { - for entry in searchResult.entries { - let metadata = await NCManageDatabaseCreateMetadata().createMetadataAsync( - fileName: entry.title, - ocId: NSUUID().uuidString, - serverUrl: session.urlBase, - url: entry.resourceURL, - isUrl: true, - name: searchResult.name.lowercased(), - subline: entry.subline, - iconUrl: entry.thumbnailURL, - session: session, - sceneIdentifier: nil) - metadatas.append(metadata) - } - completion(account, searchResult, metadatas, error) - } - } - } - if let request = request { - requestsUnifiedSearch.append(request) - } - } - - func cancelUnifiedSearchFiles() { - for request in requestsUnifiedSearch { - request.cancel() - } - requestsUnifiedSearch.removeAll() - } - - private func loadMetadata(session: NCSession.Session, - filePath: String, - dispatchGroup: DispatchGroup? = nil, - completion: @escaping (String, tableMetadata, NKError) -> Void) { - let urlPath = session.urlBase + "/remote.php/dav/files/" + session.user + filePath - - dispatchGroup?.enter() - self.readFile(serverUrlFileName: urlPath, account: session.account) { account, metadata, _, error in - defer { dispatchGroup?.leave() } - guard let metadata else { return } - let returnMetadata = tableMetadata.init(value: metadata) - NCManageDatabase.shared.addMetadata(metadata) - completion(account, returnMetadata, error) - } - } - - private func loadMetadata(session: NCSession.Session, - filePath: String) async -> tableMetadata? { - let urlPath = session.urlBase + "/remote.php/dav/files/" + session.user + filePath - let results = await self.readFileAsync(serverUrlFileName: urlPath, account: session.account) - guard let metadata = results.metadata else { - return nil - } - - NCManageDatabase.shared.addMetadata(metadata) - return metadata - } } class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { From 637842d82829ff7e4349c7969793d086cefa18b1 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 6 Feb 2026 11:30:12 +0100 Subject: [PATCH 05/35] code Signed-off-by: Marino Faggiana --- .../NCCollectionViewCommon.swift | 2 +- .../NCCollectionViewDataSource.swift | 33 ++++++++----------- .../Networking/NCNetworking+Search.swift | 10 +++--- .../en.lproj/Localizable.strings | 2 +- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 662999aff4..9516cd1328 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -689,7 +689,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, providers: self.providers, searchResults: self.searchResults, account: account) - } update: { account, id, searchResult, metadatas in + } update: { searchResult, metadatas in guard let metadatas, !metadatas.isEmpty, self.isSearchingMode, diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift index 0de361ad3a..4b26cf7b62 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift @@ -78,32 +78,25 @@ class NCCollectionViewDataSource: NSObject { } internal func createSections() { - for metadata in self.metadatas { - // skipped livePhoto VIDEO part - if metadata.isLivePhoto, metadata.classFile == NKTypeClassFile.video.rawValue { + for metadata in metadatas { + if metadata.isLivePhoto, + metadata.classFile == NKTypeClassFile.video.rawValue { continue } - let section = NSLocalizedString(self.getSectionValue(metadata: metadata), comment: "") - if !self.sectionsValue.contains(section) { - self.sectionsValue.append(section) + + let section = NSLocalizedString(getSectionValue(metadata: metadata), comment: "") + if !sectionsValue.contains(section) { + sectionsValue.append(section) } } // Unified search if let providers = self.providers, !providers.isEmpty { - let sectionsDictionary = ThreadSafeDictionary() - for section in self.sectionsValue { - if let provider = providers.filter({ $0.id == section}).first { - sectionsDictionary[section] = provider.order - } - } - self.sectionsValue.removeAll() - let sectionsDictionarySorted = sectionsDictionary.sorted(by: {$0.value < $1.value }) - for section in sectionsDictionarySorted { - if section.key == global.appName { - self.sectionsValue.insert(section.key, at: 0) - } else { - self.sectionsValue.append(section.key) - } + let orderMap = Dictionary(uniqueKeysWithValues: + providers.map { ($0.id, $0.order) } + ) + + sectionsValue = sectionsValue.sorted { + (orderMap[$0] ?? 0) < (orderMap[$1] ?? 0) } } else { // normal diff --git a/iOSClient/Networking/NCNetworking+Search.swift b/iOSClient/Networking/NCNetworking+Search.swift index 6c583dd576..bb9aacceb3 100644 --- a/iOSClient/Networking/NCNetworking+Search.swift +++ b/iOSClient/Networking/NCNetworking+Search.swift @@ -41,7 +41,7 @@ extension NCNetworking { account: String, taskHandler: @escaping (_ task: URLSessionTask) -> Void = { _ in }, providers: @escaping (_ account: String, _ searchProviders: [NKSearchProvider]?) -> Void, - update: @escaping (_ account: String, _ id: String, NKSearchResult?, [tableMetadata]?) -> Void + update: @escaping (NKSearchResult?, [tableMetadata]?) -> Void ) async { let session = NCSession.shared.getSession(account: account) let results = await NextcloudKit.shared.unifiedSearchAsync(term: literal, @@ -65,15 +65,17 @@ extension NCNetworking { taskHandler(task) } providers: { account, searchProviders in providers(account, searchProviders) - } update: { account, searchResult, provider, error in - guard let searchResult else { + } update: { _, searchResult, provider, error in + guard let searchResult, error == .success else { return } Task { let metadatas = await self.getSearchResultMetadatas(session: session, providerId: provider.id, searchResult: searchResult) - update(account, provider.id, searchResult, metadatas) + update(searchResult, metadatas) } } + + print(results) } func unifiedSearchFilesProvider(providerId: String, diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index e50e249d73..3aa17e8259 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -561,7 +561,7 @@ "_privacy_screen_footer_" = "Hide the content in the app and show a splash screen when the app is inactive."; "_in_" = "in"; "_enter_passphrase_" = "Enter passphrase (12 words)"; -"_show_more_results_" = "Show more results"; +"_show_more_results_" = "⋯ Load more results"; "_waiting_for_" = "Waiting for:"; "_reachable_wifi_" = "network reachable via Wi-Fi or cable"; "_copy_passphrase_" = "Copy passphrase"; From d2f548751ad15c7a241b5bfe15df249e81473723 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Fri, 6 Feb 2026 15:58:04 +0100 Subject: [PATCH 06/35] code Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 2 +- .../NCCollectionViewCommon.swift | 83 ++++++++++++++++--- .../Section Header Footer/NCSectionFooter.xib | 10 +-- .../Networking/NCNetworking+Search.swift | 57 +++++-------- 4 files changed, 95 insertions(+), 57 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f3c24c8e2a..9f6ba0f5cd 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -6126,7 +6126,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = 7.2.4; + branch = search; kind = branch; }; }; diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 9516cd1328..51f4a34778 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -40,8 +40,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, var networkSearchInProgress: Bool = false var layoutForView: NCDBLayoutForView? var searchDataSourceTask: URLSessionTask? - var providers: [NKSearchProvider]? - var searchResults: [NKSearchResult]? + //var providers: [NKSearchProvider]? + //var searchResults: [NKSearchResult]? var listLayout = NCListLayout() var gridLayout = NCGridLayout() var mediaLayout = NCMediaLayout() @@ -480,7 +480,6 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { isSearchingMode = true - self.providers?.removeAll() self.dataSource.removeAll() Task { await self.reloadDataSource() @@ -505,7 +504,6 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, self.isSearchingMode = false self.networkSearchInProgress = false self.literalSearch = "" - self.providers?.removeAll() self.dataSource.removeAll() Task { await self.reloadDataSource() @@ -664,8 +662,8 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, return } guard !session.account.isEmpty, - let literalSearch = literalSearch, - !literalSearch.isEmpty else { + let term = literalSearch, + !term.isEmpty else { return } let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) @@ -675,20 +673,75 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await self.reloadDataSource() if capabilities.serverVersionMajor >= global.nextcloudVersion20 { + // In This folder + let metadatasInThisFolder = await NCManageDatabase.shared.getMetadatasAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileNameView CONTAINS[c] %@", session.account, self.serverUrl, term)) ?? [] + for metadatas in metadatasInThisFolder { + metadatas.name = "inthisfolder" + } + + let results = await self.networking.unifiedSearchProviders(account: session.account) { task in + Task { + self.searchDataSourceTask = task + await self.reloadDataSource() + } + } + + guard let providers = results.providers, + results.error == .success else { + await showErrorBanner(controller: self.controller, text: results.error.errorDescription, errorCode: results.error.errorCode) + networkSearchInProgress = false + return + } + + self.dataSource = NCCollectionViewDataSource(metadatas: metadatasInThisFolder, + layoutForView: self.layoutForView, + providers: providers, + searchResults: [], + account: session.account) + + if let providers = results.providers { + for provider in providers { + let results = await self.networking.unifiedSearch(providerId: provider.id, + term: term, + limit: 10, + cursor: 0, + account: session.account) { task in + + } + guard let searchResult = results.searchResult, + let metadatas = results.metadatas, + results.error == .success else { + await showErrorBanner(controller: self.controller, text: results.error.errorDescription, errorCode: results.error.errorCode) + self.networkSearchInProgress = false + return + } + + self.dataSource.addSection(metadatas: metadatas, searchResult: searchResult) + + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } + } + + /* + + await self.networking.unifiedSearchFiles(literal: literalSearch, account: session.account) { task in Task { self.searchDataSourceTask = task await self.reloadDataSource() } - } providers: { account, searchProviders in + } providers: { searchProviders in self.providers = searchProviders + self.providers?.insert(NKSearchProvider(id: "inthisfolder", name: NSLocalizedString("_in_this_folder_", comment: ""), order: 0), at: 0) self.searchResults = [] - self.dataSource = NCCollectionViewDataSource(metadatas: [], + self.dataSource = NCCollectionViewDataSource(metadatas: metadatasInThisFolder, layoutForView: self.layoutForView, providers: self.providers, searchResults: self.searchResults, - account: account) + account: self.session.account) } update: { searchResult, metadatas in guard let metadatas, !metadatas.isEmpty, @@ -698,8 +751,9 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } self.networking.unifiedSearchQueue.addOperation(NCCollectionViewUnifiedSearch(collectionViewCommon: self, metadatas: metadatas, searchResult: searchResult)) } + */ } else { - let results = await self.networking.searchFiles(literal: literalSearch, account: session.account) { task in + let results = await self.networking.searchFiles(literal: term, account: session.account) { task in self.searchDataSourceTask = task Task { await self.reloadDataSource() @@ -713,8 +767,6 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, ) self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, layoutForView: self.layoutForView, - providers: self.providers, - searchResults: self.searchResults, account: self.session.account) } self.networkSearchInProgress = false @@ -723,13 +775,17 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, } func unifiedSearchMore(metadataForSection: NCMetadataForSection?) async { - guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } + guard let metadataForSection = metadataForSection, + let lastSearchResult = metadataForSection.lastSearchResult, + let cursor = lastSearchResult.cursor, + let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in delegate.transferReloadData(serverUrl: nil) } + /* let results = await self.networking.unifiedSearchFilesProvider(providerId: lastSearchResult.id, term: term, limit: 20, @@ -759,6 +815,7 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in delegate.transferReloadData(serverUrl: nil) } + */ } // MARK: - Push metadata diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib index d0e5b012d8..95ea89d710 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFooter.xib @@ -1,8 +1,8 @@ - + - + @@ -13,12 +13,12 @@ -