From 073e391e4428174d789855053de23a3e0244e848 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 8 Aug 2023 07:35:44 +0200 Subject: [PATCH 01/62] feat: Upload API V3 Signed-off-by: Philippe Weidmann --- kDriveCore/Data/Models/Upload/UploadSession.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kDriveCore/Data/Models/Upload/UploadSession.swift b/kDriveCore/Data/Models/Upload/UploadSession.swift index 313142be8..821383ac9 100644 --- a/kDriveCore/Data/Models/Upload/UploadSession.swift +++ b/kDriveCore/Data/Models/Upload/UploadSession.swift @@ -34,6 +34,8 @@ public final class UploadSession: EmbeddedObject, Decodable { @Persisted public var token: String + @Persisted public var uploadHost: String + public required convenience init(from decoder: Decoder) throws { self.init() let container = try decoder.container(keyedBy: CodingKeys.self) @@ -43,6 +45,7 @@ public final class UploadSession: EmbeddedObject, Decodable { fileName = try container.decode(String.self, forKey: .fileName) result = try container.decode(Bool.self, forKey: .result) token = try container.decode(String.self, forKey: .token) + uploadHost = try container.decode(String.self, forKey: .uploadHost) } enum CodingKeys: String, CodingKey { @@ -52,5 +55,6 @@ public final class UploadSession: EmbeddedObject, Decodable { case fileName = "file_name" case result case token + case uploadHost = "upload_url" } } From 6752327492cc8fd3dbbc17e7ceb2e119c333e419 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 14 Sep 2023 11:33:11 +0200 Subject: [PATCH 02/62] feat: Migrate necessary endpoints to V3 Signed-off-by: Philippe Weidmann --- kDriveAPITests/kDriveCore/DriveApiTests.swift | 4 +- kDriveCore/Data/Api/DriveApiFetcher.swift | 4 +- kDriveCore/Data/Api/Endpoint.swift | 63 ++++++++++++------- 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/kDriveAPITests/kDriveCore/DriveApiTests.swift b/kDriveAPITests/kDriveCore/DriveApiTests.swift index 626fa3aaf..ed6afccaf 100644 --- a/kDriveAPITests/kDriveCore/DriveApiTests.swift +++ b/kDriveAPITests/kDriveCore/DriveApiTests.swift @@ -87,8 +87,8 @@ final class DriveApiTests: XCTestCase { // MARK: - Helping methods func getRootDirectory() async throws -> ProxyFile { - try await currentApiFetcher.fileInfo(ProxyFile(driveId: Env.driveId, id: DriveFileManager.constants.rootID)).data - .proxify() + let fileInfo = try await currentApiFetcher.rootFiles(drive: proxyDrive).data + return fileInfo.first { $0.visibility == .root }!.proxify() } func createTestDirectory(name: String, parentDirectory: ProxyFile) async throws -> ProxyFile { diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 40c982fe5..cdf28a53d 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -245,7 +245,7 @@ public class DriveApiFetcher: ApiFetcher { } public func delete(file: ProxyFile) async throws -> CancelableResponse { - try await perform(request: authenticatedRequest(.fileInfo(file), method: .delete)).data + try await perform(request: authenticatedRequest(.fileInfoV2(file), method: .delete)).data } public func emptyTrash(drive: AbstractDrive) async throws -> Bool { @@ -390,7 +390,7 @@ public class DriveApiFetcher: ApiFetcher { fileTypes: fileTypes, categories: categories, belongToAllCategories: belongToAllCategories - ).paginated(page: page).sorted(by: [.type, sortType]))).data + ).paginated(page: page).sorted(by: [sortType]))).data } public func add(category: Category, to file: ProxyFile) async throws -> CategoryResponse { diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index a9ee09b41..fda52b030 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -137,15 +137,21 @@ extension File: AbstractFile {} public extension Endpoint { private static var drive: Endpoint { - return Endpoint(hostKeypath: \.apiDriveHost, path: "/2/drive") + return Endpoint(hostKeypath: \.apiDriveHost, path: "/3/drive") } static var inAppReceipt: Endpoint { return Endpoint(path: "/invoicing/inapp/apple/link_receipt") } + // MARK: V2 + + private static var driveV2: Endpoint { + return Endpoint(hostKeypath: \.apiDriveHost, path: "/2/drive") + } + static var initData: Endpoint { - return .drive.appending(path: "/init", queryItems: [DriveInitWith.allCases.toQueryItem()]) + return .driveV2.appending(path: "/init", queryItems: [DriveInitWith.allCases.toQueryItem()]) } // MARK: Action @@ -193,7 +199,7 @@ public extension Endpoint { // MARK: Archive static func buildArchive(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/archives") + return .driveInfoV2(drive: drive).appending(path: "/files/archives") } static func getArchive(drive: AbstractDrive, uuid: String) -> Endpoint { @@ -203,7 +209,7 @@ public extension Endpoint { // MARK: Category static func categories(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/categories") + return .driveInfoV2(drive: drive).appending(path: "/categories") } static func category(drive: AbstractDrive, category: Category) -> Endpoint { @@ -211,17 +217,17 @@ public extension Endpoint { } static func fileCategory(file: AbstractFile, category: Category) -> Endpoint { - return .fileInfo(file).appending(path: "/categories/\(category.id)") + return .fileInfoV2(file).appending(path: "/categories/\(category.id)") } static func fileCategory(drive: AbstractDrive, category: Category) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/categories/\(category.id)") + return .driveInfoV2(drive: drive).appending(path: "/files/categories/\(category.id)") } // MARK: Comment static func comments(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/comments", queryItems: [ + return .fileInfoV2(file).appending(path: "/comments", queryItems: [ URLQueryItem(name: "with", value: "user,likes,responses,responses.user,responses.likes") ]) } @@ -246,6 +252,10 @@ public extension Endpoint { return .drive.appending(path: "/\(drive.id)") } + static func driveInfoV2(drive: AbstractDrive) -> Endpoint { + return .driveV2.appending(path: "/\(drive.id)") + } + static func driveUsers(drive: AbstractDrive) -> Endpoint { return .driveInfo(drive: drive).appending(path: "/account/user") } @@ -261,7 +271,7 @@ public extension Endpoint { } static func dropbox(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/dropbox", queryItems: [ + return .fileInfoV2(file).appending(path: "/dropbox", queryItems: [ URLQueryItem(name: "with", value: "user,capabilities") ]) } @@ -277,17 +287,17 @@ public extension Endpoint { } static func favorite(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/favorite") + return .fileInfoV2(file).appending(path: "/favorite") } // MARK: File access static func invitation(drive: AbstractDrive, id: Int) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/invitations/\(id)") + return .driveInfoV2(drive: drive).appending(path: "/files/invitations/\(id)") } static func access(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/access", queryItems: [ + return .fileInfoV2(file).appending(path: "/access", queryItems: [ URLQueryItem(name: "with", value: "user") ]) } @@ -371,6 +381,11 @@ public extension Endpoint { queryItems: [FileWith.fileExtra.toQueryItem()]) } + static func fileInfoV2(_ file: AbstractFile) -> Endpoint { + return .driveInfoV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem()]) + } + static func files(of directory: AbstractFile) -> Endpoint { return .fileInfo(directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) } @@ -384,13 +399,13 @@ public extension Endpoint { } static func thumbnail(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfo(file).appending(path: "/thumbnail", queryItems: [ + return .fileInfoV2(file).appending(path: "/thumbnail", queryItems: [ URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") ]) } static func preview(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfo(file).appending(path: "/preview", queryItems: [ + return .fileInfoV2(file).appending(path: "/preview", queryItems: [ URLQueryItem(name: "width", value: "2500"), URLQueryItem(name: "height", value: "1500"), URLQueryItem(name: "quality", value: "80"), @@ -405,11 +420,11 @@ public extension Endpoint { } else { queryItems = nil } - return .fileInfo(file).appending(path: "/download", queryItems: queryItems) + return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) } static func convert(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) + return .fileInfoV2(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) } static func move(file: AbstractFile, destination: AbstractFile) -> Endpoint { @@ -429,7 +444,7 @@ public extension Endpoint { } static func count(of directory: AbstractFile) -> Endpoint { - return .fileInfo(directory).appending(path: "/count") + return .fileInfoV2(directory).appending(path: "/count") } static func size(file: AbstractFile, depth: String) -> Endpoint { @@ -443,13 +458,13 @@ public extension Endpoint { } static func directoryColor(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/color") + return .fileInfoV2(file).appending(path: "/color") } // MARK: - Import static func cancelImport(drive: AbstractDrive, id: Int) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/imports/\(id)/cancel") + return .driveInfoV2(drive: drive).appending(path: "/imports/\(id)/cancel") } // MARK: Preferences @@ -469,11 +484,11 @@ public extension Endpoint { } static func rootFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + return .driveInfo(drive: drive).appending(path: "/files/1/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) } static func bulkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/bulk") + return .driveInfoV2(drive: drive).appending(path: "/files/bulk") } static func lastModifiedFiles(drive: AbstractDrive) -> Endpoint { @@ -549,17 +564,17 @@ public extension Endpoint { // MARK: Share link static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/links") + return .driveInfoV2(drive: drive).appending(path: "/files/links") } static func shareLink(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/link") + return .fileInfoV2(file).appending(path: "/link") } // MARK: Trash static func trash(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) + return .driveInfoV2(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) } static func trashCount(drive: AbstractDrive) -> Endpoint { @@ -633,7 +648,7 @@ public extension Endpoint { // MARK: User invitation static func userInvitations(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/user/invitation") + return .driveInfoV2(drive: drive).appending(path: "/user/invitation") } static func userInvitation(drive: AbstractDrive, id: Int) -> Endpoint { From 8a827999cd78adcc5a8fe29ee8a52d35c4cd5786 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 23 Oct 2023 10:38:50 +0200 Subject: [PATCH 03/62] feat: Cursored paging WIP Signed-off-by: Philippe Weidmann --- Project.swift | 3 +- .../File List/FileListViewController.swift | 16 ++-- .../Files/File List/FileListViewModel.swift | 10 +- kDriveCore/Data/Api/DriveApiFetcher.swift | 37 +++++--- kDriveCore/Data/Cache/DriveFileManager.swift | 92 ++++++++++--------- kDriveCore/Data/Models/File.swift | 4 +- .../FileProviderEnumerator.swift | 29 +++--- 7 files changed, 104 insertions(+), 87 deletions(-) diff --git a/Project.swift b/Project.swift index ace4d926c..be5ce25ee 100644 --- a/Project.swift +++ b/Project.swift @@ -24,8 +24,9 @@ let project = Project(name: "kDrive", packages: [ .package(url: "https://github.com/apple/swift-algorithms", .upToNextMajor(from: "1.2.0")), .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.2.2")), - .package(url: "https://github.com/Infomaniak/ios-core", .upToNextMajor(from: "4.1.29")), + .package(url: "https://github.com/Infomaniak/ios-core", .branch("cursored-api-response")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.5.5")), + .package(url: "https://github.com/Infomaniak/ios-core", .branch("cursored-api-response")), .package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "4.0.0")), .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "1.1.11")), .package(url: "https://github.com/Infomaniak/swift-concurrency", .upToNextMajor(from: "0.0.4")), diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index aeaf3bceb..dddad446a 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -97,26 +97,26 @@ class ConcreteFileListViewModel: FileListViewModel { files = AnyRealmCollection(AnyRealmCollection(currentDirectory.children).filesSorted(by: sortType)) } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } if currentDirectory.canLoadChildrenFromCache && !forceRefresh { - try await loadActivitiesIfNeeded() + // try await loadActivitiesIfNeeded() } else { - startRefreshing(page: page) + startRefreshing(cursor: cursor) defer { endRefreshing() } - let (_, moreComing) = try await driveFileManager.files( + let (_, response) = try await driveFileManager.files( in: currentDirectory.proxify(), - page: page, + cursor: cursor, sortType: sortType, forceRefresh: forceRefresh ) endRefreshing() - if moreComing { - try await loadFiles(page: page + 1, forceRefresh: forceRefresh) + if let nextCursor = response.cursor { + try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) } else if !forceRefresh { try await loadActivities() } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 87493bc21..fd9f64e45 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -328,10 +328,10 @@ class FileListViewModel: SelectDelegate { } } - func startRefreshing(page: Int) { + func startRefreshing(cursor: String?) { isLoading = true - if page == 1 { + if cursor == nil { showLoadingIndicatorIfNeeded() } } @@ -346,7 +346,7 @@ class FileListViewModel: SelectDelegate { // Implemented by subclasses } - func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { + func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { // Implemented by subclasses } @@ -425,7 +425,7 @@ class FileListViewModel: SelectDelegate { func forceRefresh() { endRefreshing() Task { - try await loadFiles(page: 1, forceRefresh: true) + try await loadFiles(cursor: nil, forceRefresh: true) } } @@ -434,7 +434,7 @@ class FileListViewModel: SelectDelegate { let responseAtDate = Date(timeIntervalSince1970: Double(currentDirectory.responseAt)) let now = Date() if responseAtDate.distance(to: now) > Constants.activitiesReloadTimeOut { - try await loadFiles(page: 1, forceRefresh: true) + try await loadFiles(cursor: nil, forceRefresh: true) } else { try await loadActivities() } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index cdf28a53d..15cdf9d8c 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -67,7 +67,7 @@ public class DriveApiFetcher: ApiFetcher { override public func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ( data: T, - responseAt: Int? + response: ApiResponse ) { do { return try await super.perform(request: request) @@ -121,19 +121,27 @@ public class DriveApiFetcher: ApiFetcher { try await perform(request: authenticatedRequest(.dropbox(file: directory), method: .delete)).data } - public func rootFiles(drive: AbstractDrive, page: Int = 1, - sortType: SortType = .nameAZ) async throws -> (data: [File], responseAt: Int?) { - try await perform(request: authenticatedRequest(.rootFiles(drive: drive).paginated(page: page) - .sorted(by: [.type, sortType]))) + public func rootFiles(drive: AbstractDrive, + cursor: String?, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest( + .rootFiles(drive: drive) + .sorted(by: [.type, sortType]), + method: .get + )) } - public func files(in directory: ProxyFile, page: Int = 1, - sortType: SortType = .nameAZ) async throws -> (data: [File], responseAt: Int?) { - try await perform(request: authenticatedRequest(.files(of: directory).paginated(page: page) - .sorted(by: [.type, sortType]))) + public func files(in directory: ProxyFile, + cursor: String?, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest( + .files(of: directory) + .sorted(by: [.type, sortType]), + method: .get + )) } - public func fileInfo(_ file: ProxyFile) async throws -> (data: File, responseAt: Int?) { + public func fileInfo(_ file: ProxyFile) async throws -> (data: File, response: ApiResponse) { try await perform(request: authenticatedRequest(.fileInfo(file))) } @@ -289,7 +297,7 @@ public class DriveApiFetcher: ApiFetcher { } public func fileActivities(file: ProxyFile, from date: Date, - page: Int) async throws -> (data: [FileActivity], responseAt: Int?) { + page: Int) async throws -> (data: [FileActivity], response: ApiResponse<[FileActivity]>) { var queryItems = [ FileWith.fileActivitiesWithExtra.toQueryItem(), URLQueryItem(name: "depth", value: "children"), @@ -303,7 +311,8 @@ public class DriveApiFetcher: ApiFetcher { } public func filesActivities(drive: AbstractDrive, files: [ProxyFile], - from date: Date) async throws -> (data: [ActivitiesForFile], responseAt: Int?) { + from date: Date) async throws + -> (data: [ActivitiesForFile], response: ApiResponse<[ActivitiesForFile]>) { try await perform(request: authenticatedRequest(.filesActivities(drive: drive, fileIds: files.map(\.id), from: date))) } @@ -358,8 +367,8 @@ public class DriveApiFetcher: ApiFetcher { try await perform(request: authenticatedRequest(.trashedInfo(file: file))).data } - public func trashedFiles(of directory: ProxyFile, page: Int = 1, sortType: SortType = .nameAZ) async throws -> [File] { - try await perform(request: authenticatedRequest(.trashedFiles(of: directory).paginated(page: page) + public func trashedFiles(of directory: ProxyFile, cursor: String? = nil, sortType: SortType = .nameAZ) async throws -> [File] { + try await perform(request: authenticatedRequest(.trashedFiles(of: directory).paginated(page: 1) .sorted(by: [sortType]))).data } diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index 845ecd5f4..5896a1eae 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -479,32 +479,36 @@ public final class DriveFileManager { _ = try await files(in: root.proxify()) } - public func files(in directory: ProxyFile, page: Int = 1, sortType: SortType = .nameAZ, - forceRefresh: Bool = false) async throws -> (files: [File], moreComing: Bool) { - let fetchFiles: () async throws -> ([File], Int?) + public func files(in directory: ProxyFile, cursor: String? = nil, sortType: SortType = .nameAZ, + forceRefresh: Bool = false) async throws -> (files: [File], cursor: String?) { + let fetchFiles: () async throws -> ([File], ApiResponse<[File]>) if directory.isRoot { fetchFiles = { - let (children, responseAt) = try await self.apiFetcher - .rootFiles(drive: self.drive, page: page, sortType: sortType) - return (children, responseAt) + let (children, response) = try await self.apiFetcher + .rootFiles(drive: self.drive, cursor: cursor, sortType: sortType) + return (children, response) } } else { fetchFiles = { - let (children, responseAt) = try await self.apiFetcher.files(in: directory, page: page, sortType: sortType) - return (children, responseAt) + let (children, response) = try await self.apiFetcher.files(in: directory, cursor: cursor, sortType: sortType) + return (children, response) } } - return try await files(in: directory, fetchFiles: fetchFiles, - page: page, sortType: sortType, keepProperties: [.standard, .extras], forceRefresh: forceRefresh) + return try await files(in: directory, + fetchFiles: fetchFiles, + cursor: cursor, + sortType: sortType, + keepProperties: [.standard, .extras], + forceRefresh: forceRefresh) } private func remoteFiles(in directory: ProxyFile, - fetchFiles: () async throws -> ([File], Int?), - page: Int, + fetchFiles: () async throws -> ([File], ApiResponse<[File]>?), + isInitialCursor: Bool, sortType: SortType, - keepProperties: FilePropertiesOptions) async throws -> (files: [File], moreComing: Bool) { + keepProperties: FilePropertiesOptions) async throws -> (files: [File], cursor: String?) { // Get children from API - let (children, responseAt) = try await fetchFiles() + let (children, response) = try await fetchFiles() let realm = getRealm() // Keep cached properties for children for child in children { @@ -514,7 +518,7 @@ public final class DriveFileManager { let managedParent = try directory.resolve(using: realm) // Update parent try realm.write { - managedParent.responseAt = responseAt ?? Int(Date().timeIntervalSince1970) + managedParent.responseAt = response?.responseAt ?? Int(Date().timeIntervalSince1970) if children.count < Endpoint.itemsPerPage { managedParent.versionCode = DriveFileManager.constants.currentVersionCode managedParent.fullyDownloaded = true @@ -530,26 +534,26 @@ public final class DriveFileManager { return ( getLocalSortedDirectoryFiles(directory: managedParent, sortType: sortType), - children.count == Endpoint.itemsPerPage + response?.cursor ) } private func files(in directory: ProxyFile, - fetchFiles: () async throws -> ([File], Int?), - page: Int, + fetchFiles: () async throws -> ([File], ApiResponse<[File]>), + cursor: String?, sortType: SortType, keepProperties: FilePropertiesOptions, - forceRefresh: Bool) async throws -> (files: [File], moreComing: Bool) { + forceRefresh: Bool) async throws -> (files: [File], String?) { if let cachedParent = getCachedFile(id: directory.id, freeze: false), // We have cache and we show it before fetching activities OR we are not connected to internet and we show what we have // anyway (cachedParent.canLoadChildrenFromCache && !forceRefresh) || ReachabilityListener.instance.currentStatus == .offline { - return (getLocalSortedDirectoryFiles(directory: cachedParent, sortType: sortType), false) + return (getLocalSortedDirectoryFiles(directory: cachedParent, sortType: sortType), nil) } else { return try await remoteFiles( in: directory, fetchFiles: fetchFiles, - page: page, + page: 1, sortType: sortType, keepProperties: keepProperties ) @@ -581,28 +585,30 @@ public final class DriveFileManager { public func favorites(page: Int = 1, sortType: SortType = .nameAZ, forceRefresh: Bool = false) async throws -> (files: [File], moreComing: Bool) { - try await files(in: getManagedFile(from: DriveFileManager.favoriteRootFile).proxify(), - fetchFiles: { - let favorites = try await apiFetcher.favorites(drive: drive, page: page, sortType: sortType) - return (favorites, nil) - }, - page: page, - sortType: sortType, - keepProperties: [.standard, .extras], - forceRefresh: forceRefresh) + fatalError("TODO") + /* try await files(in: getManagedFile(from: DriveFileManager.favoriteRootFile).proxify(), + fetchFiles: { + let favorites = try await apiFetcher.favorites(drive: drive, page: page, sortType: sortType) + return (favorites, nil) + }, + cursor: "", + sortType: sortType, + keepProperties: [.standard, .extras], + forceRefresh: forceRefresh) */ } public func mySharedFiles(page: Int = 1, sortType: SortType = .nameAZ, forceRefresh: Bool = false) async throws -> (files: [File], moreComing: Bool) { - try await files(in: getManagedFile(from: DriveFileManager.mySharedRootFile).proxify(), - fetchFiles: { - let mySharedFiles = try await apiFetcher.mySharedFiles(drive: drive, page: page, sortType: sortType) - return (mySharedFiles, nil) - }, - page: page, - sortType: sortType, - keepProperties: [.standard, .path, .version], - forceRefresh: forceRefresh) + fatalError("TODO") + /* try await files(in: getManagedFile(from: DriveFileManager.mySharedRootFile).proxify(), + fetchFiles: { + let mySharedFiles = try await apiFetcher.mySharedFiles(drive: drive, page: page, sortType: sortType) + return (mySharedFiles, nil) + }, + cursor: "", + sortType: sortType, + keepProperties: [.standard, .path, .version], + forceRefresh: forceRefresh) */ } public func getAvailableOfflineFiles(sortType: SortType = .nameAZ) -> [File] { @@ -876,14 +882,14 @@ public final class DriveFileManager { var responseAt = 0 while moreComing { // Get activities page - let (activities, pageResponseAt) = try await apiFetcher.fileActivities( + let (activities, response) = try await apiFetcher.fileActivities( file: file, from: Date(timeIntervalSince1970: timestamp), page: page ) moreComing = activities.count == Endpoint.itemsPerPage page += 1 - responseAt = pageResponseAt ?? Int(Date().timeIntervalSince1970) + responseAt = response.responseAt ?? Int(Date().timeIntervalSince1970) // Get file from Realm let realm = getRealm() let cachedFile = try file.resolve(using: realm) @@ -995,10 +1001,10 @@ public final class DriveFileManager { } public func filesActivities(files: [File], from date: Date) async throws -> [ActivitiesForFile] { - let (result, responseAt) = try await apiFetcher + let (result, response) = try await apiFetcher .filesActivities(drive: drive, files: files.map { $0.proxify() }, from: date) // Update last sync date - if let responseAt { + if let responseAt = response.responseAt { UserDefaults.shared.lastSyncDateOfflineFiles = responseAt } return result diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index 5e8c1601d..1491154db 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -189,14 +189,14 @@ public enum SortType: String { switch self { case .nameAZ: return SortTypeValue( - apiValue: "path", + apiValue: "name", order: "asc", translation: KDriveResourcesStrings.Localizable.sortNameAZ, realmKeyPath: \.sortedName ) case .nameZA: return SortTypeValue( - apiValue: "path", + apiValue: "name", order: "desc", translation: KDriveResourcesStrings.Localizable.sortNameZA, realmKeyPath: \.sortedName diff --git a/kDriveFileProvider/FileProviderEnumerator.swift b/kDriveFileProvider/FileProviderEnumerator.swift index 248429ca4..78da4d4ed 100644 --- a/kDriveFileProvider/FileProviderEnumerator.swift +++ b/kDriveFileProvider/FileProviderEnumerator.swift @@ -87,7 +87,7 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { observer.finishEnumeratingWithError(self.nsError(code: .noSuchItem)) return } - let pageIndex = page.isInitialPage ? 1 : page.toInt + let cursor = page.isInitialPage ? nil : page.toCursor var forceRefresh = false if let lastResponseAt = self.driveFileManager.getCachedFile(id: fileId)?.responseAt { @@ -99,7 +99,7 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { do { let file = try await self.driveFileManager.file(id: fileId, forceRefresh: forceRefresh) let (children, moreComing) = try await self.driveFileManager - .files(in: file.proxify(), page: pageIndex, forceRefresh: forceRefresh) + .files(in: file.proxify(), cursor: cursor, forceRefresh: forceRefresh) // No need to freeze $0 it should already be frozen var containerItems = [FileProviderItem]() for child in children { @@ -111,8 +111,8 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { containerItems.append(FileProviderItem(file: file, domain: self.domain)) observer.didEnumerate(containerItems) - if self.isDirectory && moreComing { - observer.finishEnumerating(upTo: NSFileProviderPage(pageIndex + 1)) + if self.isDirectory, let cursor { + observer.finishEnumerating(upTo: NSFileProviderPage(cursor)) } else { observer.finishEnumerating(upTo: nil) } @@ -123,7 +123,7 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { .trashedFile(ProxyFile(driveId: self.driveFileManager.drive.id, id: fileId)) let children = try await self.driveFileManager.apiFetcher.trashedFiles( of: file.proxify(), - page: pageIndex + cursor: cursor ) var containerItems = [FileProviderItem]() for child in children { @@ -135,11 +135,12 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { } containerItems.append(FileProviderItem(file: file, domain: self.domain)) observer.didEnumerate(containerItems) - if self.isDirectory && children.count == Endpoint.itemsPerPage { - observer.finishEnumerating(upTo: NSFileProviderPage(pageIndex + 1)) - } else { - observer.finishEnumerating(upTo: nil) - } + // FIXME: Cursors also for trash + /* if self.isDirectory && children.count == Endpoint.itemsPerPage { + observer.finishEnumerating(upTo: NSFileProviderPage(pageIndex + 1)) + } else { + observer.finishEnumerating(upTo: nil) + } */ } catch { if let error = error as? DriveError, error == .productMaintenance { observer.finishEnumeratingWithError(NSFileProviderError(.serverUnreachable)) @@ -235,12 +236,12 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { } extension NSFileProviderPage { - init(_ integer: Int) { - self.init(withUnsafeBytes(of: integer.littleEndian) { Data($0) }) + init(_ cursor: String) { + self.init(cursor.data(using: .utf8) ?? Data()) } - var toInt: Int { - return rawValue.withUnsafeBytes { $0.load(as: Int.self) }.littleEndian + var toCursor: String? { + return String(data: rawValue, encoding: .utf8) } var isInitialPage: Bool { From 5cf97d7555978008d1f4ad2d52e7980545eed4e1 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 31 Oct 2023 11:16:58 +0100 Subject: [PATCH 04/62] feat: Cursored file list Signed-off-by: Philippe Weidmann --- .../Files/File List/FileListViewController.swift | 4 ++-- kDriveCore/Data/Api/DriveApiFetcher.swift | 3 ++- kDriveCore/Data/Api/Endpoint.swift | 6 ++++++ kDriveCore/Data/Cache/DriveFileManager.swift | 12 ++++++------ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index dddad446a..c52b8a1ce 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -108,14 +108,14 @@ class ConcreteFileListViewModel: FileListViewModel { endRefreshing() } - let (_, response) = try await driveFileManager.files( + let (_, nextCursor) = try await driveFileManager.files( in: currentDirectory.proxify(), cursor: cursor, sortType: sortType, forceRefresh: forceRefresh ) endRefreshing() - if let nextCursor = response.cursor { + if let nextCursor { try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) } else if !forceRefresh { try await loadActivities() diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 15cdf9d8c..96bfa1f94 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -136,7 +136,8 @@ public class DriveApiFetcher: ApiFetcher { sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { try await perform(request: authenticatedRequest( .files(of: directory) - .sorted(by: [.type, sortType]), + .sorted(by: [.type, sortType]) + .cursored(cursor), method: .get )) } diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index fda52b030..c68490049 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -75,6 +75,12 @@ public extension Endpoint { return Endpoint(host: host, path: path, queryItems: (queryItems ?? []) + sortQueryItems) } + + func cursored(_ cursor: String?) -> Endpoint { + let perPage = URLQueryItem(name: "limit", value: "\(Endpoint.itemsPerPage)") + let cursorQueryItem = cursor != nil ? [URLQueryItem(name: "cursor", value: cursor), perPage] : [perPage] + return Endpoint(host: host, path: path, queryItems: (queryItems ?? []) + cursorQueryItem) + } } // MARK: - Proxies diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index 5896a1eae..87925be38 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -480,7 +480,7 @@ public final class DriveFileManager { } public func files(in directory: ProxyFile, cursor: String? = nil, sortType: SortType = .nameAZ, - forceRefresh: Bool = false) async throws -> (files: [File], cursor: String?) { + forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { let fetchFiles: () async throws -> ([File], ApiResponse<[File]>) if directory.isRoot { fetchFiles = { @@ -506,7 +506,7 @@ public final class DriveFileManager { fetchFiles: () async throws -> ([File], ApiResponse<[File]>?), isInitialCursor: Bool, sortType: SortType, - keepProperties: FilePropertiesOptions) async throws -> (files: [File], cursor: String?) { + keepProperties: FilePropertiesOptions) async throws -> (files: [File], nextCursor: String?) { // Get children from API let (children, response) = try await fetchFiles() let realm = getRealm() @@ -526,7 +526,7 @@ public final class DriveFileManager { realm.add(children, update: .modified) // ⚠️ this is important because we are going to add all the children again. However, failing to start the request with // the first page will result in an undefined behavior. - if page == 1 { + if isInitialCursor { managedParent.children.removeAll() } managedParent.children.insert(objectsIn: children) @@ -534,7 +534,7 @@ public final class DriveFileManager { return ( getLocalSortedDirectoryFiles(directory: managedParent, sortType: sortType), - response?.cursor + response?.hasMore == true ? response?.cursor : nil ) } @@ -543,7 +543,7 @@ public final class DriveFileManager { cursor: String?, sortType: SortType, keepProperties: FilePropertiesOptions, - forceRefresh: Bool) async throws -> (files: [File], String?) { + forceRefresh: Bool) async throws -> (files: [File], nextCursor: String?) { if let cachedParent = getCachedFile(id: directory.id, freeze: false), // We have cache and we show it before fetching activities OR we are not connected to internet and we show what we have // anyway @@ -553,7 +553,7 @@ public final class DriveFileManager { return try await remoteFiles( in: directory, fetchFiles: fetchFiles, - page: 1, + isInitialCursor: cursor == nil, sortType: sortType, keepProperties: keepProperties ) From 695146b2c72abb960beaac95f4aa4bd9f0cba519 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 10:35:04 +0100 Subject: [PATCH 05/62] feat: Cursored favorites Signed-off-by: Philippe Weidmann --- .../Controller/Favorite/FavoritesViewModel.swift | 12 ++++++------ kDriveCore/Data/Api/DriveApiFetcher.swift | 9 ++++++--- kDriveCore/Data/Cache/DriveFileManager.swift | 16 ++++++++-------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift b/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift index 1e8762147..96eda81e4 100644 --- a/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift +++ b/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift @@ -37,21 +37,21 @@ class FavoritesViewModel: FileListViewModel { .filesSorted(by: sortType)) } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } // Only show loading indicator if we have nothing in cache if !currentDirectory.canLoadChildrenFromCache { - startRefreshing(page: page) + startRefreshing(cursor: cursor) } defer { endRefreshing() } - let (_, moreComing) = try await driveFileManager.favorites(page: page, sortType: sortType, forceRefresh: true) + let (_, nextCursor) = try await driveFileManager.favorites(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() - if moreComing { - try await loadFiles(page: page + 1, forceRefresh: true) + if let nextCursor { + try await loadFiles(cursor: nextCursor, forceRefresh: true) } } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 96bfa1f94..8aabfbb56 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -146,9 +146,12 @@ public class DriveApiFetcher: ApiFetcher { try await perform(request: authenticatedRequest(.fileInfo(file))) } - public func favorites(drive: AbstractDrive, page: Int = 1, sortType: SortType = .nameAZ) async throws -> [File] { - try await perform(request: authenticatedRequest(.favorites(drive: drive).paginated(page: page) - .sorted(by: [.type, sortType]))).data + public func favorites(drive: AbstractDrive, + cursor: String? = nil, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest(.favorites(drive: drive) + .sorted(by: [.type, sortType]) + .cursored(cursor))) } public func mySharedFiles(drive: AbstractDrive, page: Int = 1, sortType: SortType = .nameAZ) async throws -> [File] { diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index 87925be38..4f798fcd3 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -583,18 +583,18 @@ public final class DriveFileManager { } } - public func favorites(page: Int = 1, sortType: SortType = .nameAZ, - forceRefresh: Bool = false) async throws -> (files: [File], moreComing: Bool) { - fatalError("TODO") - /* try await files(in: getManagedFile(from: DriveFileManager.favoriteRootFile).proxify(), + public func favorites(cursor: String? = nil, + sortType: SortType = .nameAZ, + forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { + try await files(in: getManagedFile(from: DriveFileManager.favoriteRootFile).proxify(), fetchFiles: { - let favorites = try await apiFetcher.favorites(drive: drive, page: page, sortType: sortType) - return (favorites, nil) + let favorites = try await apiFetcher.favorites(drive: drive, cursor: cursor, sortType: sortType) + return favorites }, - cursor: "", + cursor: cursor, sortType: sortType, keepProperties: [.standard, .extras], - forceRefresh: forceRefresh) */ + forceRefresh: forceRefresh) } public func mySharedFiles(page: Int = 1, sortType: SortType = .nameAZ, From 235b78431c262f57078039fffe9ac9a1fc324d1f Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 11:37:24 +0100 Subject: [PATCH 06/62] feat: Cursored trash Signed-off-by: Philippe Weidmann --- .../Menu/Trash/TrashListViewModel.swift | 23 +++++++++---------- kDriveCore/Data/Api/DriveApiFetcher.swift | 17 ++++++++++---- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift index 190b962e2..6fa571c0c 100644 --- a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift +++ b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift @@ -53,38 +53,37 @@ class TrashListViewModel: InMemoryFileListViewModel { files = AnyRealmCollection(files.sorted(by: [sortType.value.sortDescriptor])) } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } - startRefreshing(page: page) + startRefreshing(cursor: cursor) defer { endRefreshing() } - let fetchedFiles: [File] + let fetchResponse: (data: [File], response: ApiResponse<[File]>) if currentDirectory.id == DriveFileManager.trashRootFile.id { - fetchedFiles = try await driveFileManager.apiFetcher.trashedFiles( + fetchResponse = try await driveFileManager.apiFetcher.trashedFiles( drive: driveFileManager.drive, - page: page, + cursor: cursor, sortType: sortType ) } else { - fetchedFiles = try await driveFileManager.apiFetcher.trashedFiles( + fetchResponse = try await driveFileManager.apiFetcher.trashedFiles( of: currentDirectory.proxify(), - page: page, + cursor: cursor, sortType: sortType ) } - let moreComing = fetchedFiles.count == Endpoint.itemsPerPage - addPage(files: fetchedFiles, fullyDownloaded: !moreComing, page: page) + addPage(files: fetchResponse.data, fullyDownloaded: fetchResponse.response.hasMore, cursor: cursor) endRefreshing() if currentDirectory.id == DriveFileManager.trashRootFile.id { currentRightBarButtons = files.isEmpty ? nil : [.emptyTrash] } - if moreComing { - try await loadFiles(page: page + 1) + if let nextCursor = fetchResponse.response.cursor { + try await loadFiles(cursor: nextCursor) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 8aabfbb56..1b253fd3d 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -363,17 +363,24 @@ public class DriveApiFetcher: ApiFetcher { // MARK: - - public func trashedFiles(drive: AbstractDrive, page: Int = 1, sortType: SortType = .nameAZ) async throws -> [File] { - try await perform(request: authenticatedRequest(.trash(drive: drive).paginated(page: page).sorted(by: [sortType]))).data + public func trashedFiles(drive: AbstractDrive, + cursor: String? = nil, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest(.trash(drive: drive) + .sorted(by: [sortType]) + .cursored(cursor))) } public func trashedFile(_ file: ProxyFile) async throws -> File { try await perform(request: authenticatedRequest(.trashedInfo(file: file))).data } - public func trashedFiles(of directory: ProxyFile, cursor: String? = nil, sortType: SortType = .nameAZ) async throws -> [File] { - try await perform(request: authenticatedRequest(.trashedFiles(of: directory).paginated(page: 1) - .sorted(by: [sortType]))).data + public func trashedFiles(of directory: ProxyFile, + cursor: String? = nil, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest(.trashedFiles(of: directory) + .sorted(by: [sortType]) + .cursored(cursor))) } public func restore(file: ProxyFile, in directory: ProxyFile? = nil) async throws -> CancelableResponse { From cbae7a71832ff18c80d7c2d6d81d0b9b9b1bbcdd Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 12:32:13 +0100 Subject: [PATCH 07/62] feat: Cursored MyShares Signed-off-by: Philippe Weidmann --- .../File List/InMemoryFileListViewModel.swift | 4 ++-- .../RecentActivityFilesViewController.swift | 4 ++-- .../Menu/LastModificationsViewModel.swift | 18 ++++++++--------- .../Controller/Menu/MySharesViewModel.swift | 12 +++++------ kDriveCore/Data/Api/DriveApiFetcher.swift | 9 ++++++--- kDriveCore/Data/Cache/DriveFileManager.swift | 20 +++++++++++-------- 6 files changed, 37 insertions(+), 30 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift index 27877bdc2..7dfb815e8 100644 --- a/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/InMemoryFileListViewModel.swift @@ -55,7 +55,7 @@ class InMemoryFileListViewModel: FileListViewModel { /// - Parameters: /// - fetchedFiles: The list of files to add. /// - page: The page of the files. - final func addPage(files fetchedFiles: [File], fullyDownloaded: Bool, copyInRealm: Bool = false, page: Int) { + final func addPage(files fetchedFiles: [File], fullyDownloaded: Bool, copyInRealm: Bool = false, cursor: String?) { try? realm.write { var children = [File]() if copyInRealm { @@ -67,7 +67,7 @@ class InMemoryFileListViewModel: FileListViewModel { children = fetchedFiles } - if page == 1 { + if cursor == nil { currentDirectory.children.removeAll() } currentDirectory.children.insert(objectsIn: children) diff --git a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift index ec7ad9407..3f5cd1ae2 100644 --- a/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift +++ b/kDrive/UI/Controller/Files/RecentActivityFilesViewController.swift @@ -27,7 +27,7 @@ class RecentActivityFilesViewModel: InMemoryFileListViewModel { convenience init(driveFileManager: DriveFileManager, activities: [FileActivity]) { self.init(driveFileManager: driveFileManager) activity = activities.first - addPage(files: activities.compactMap(\.file).map { $0.detached() }, fullyDownloaded: true, page: 1) + addPage(files: activities.compactMap(\.file).map { $0.detached() }, fullyDownloaded: true, cursor: nil) } required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { @@ -57,7 +57,7 @@ class RecentActivityFilesViewModel: InMemoryFileListViewModel { let realm = driveFileManager.getRealm() activity = realm.object(ofType: FileActivity.self, forPrimaryKey: activityId)?.freeze() let cachedFiles = fileIds.compactMap { driveFileManager.getCachedFile(id: $0, using: realm) }.map { $0.detached() } - addPage(files: cachedFiles, fullyDownloaded: true, page: 1) + addPage(files: cachedFiles, fullyDownloaded: true, cursor: nil) forceRefresh() } diff --git a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift index 4e60a9610..f2e3464d8 100644 --- a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift +++ b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift @@ -50,22 +50,22 @@ class LastModificationsViewModel: FileListViewModel { files = AnyRealmCollection(files.sorted(by: [sortType.value.sortDescriptor])) } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } - startRefreshing(page: page) + startRefreshing(cursor: cursor) defer { endRefreshing() } - let (_, moreComing) = try await driveFileManager.lastModifiedFiles(page: page) - endRefreshing() - if moreComing { - try await loadFiles(page: page + 1, forceRefresh: forceRefresh) - } + /* let (_, moreComing) = try await driveFileManager.lastModifiedFiles(page: page) + endRefreshing() + if moreComing { + try await loadFiles(page: page + 1, forceRefresh: forceRefresh) + } */ } override func loadActivities() async throws { - try await loadFiles(page: 1, forceRefresh: true) + // try await loadFiles(page: 1, forceRefresh: true) } } diff --git a/kDrive/UI/Controller/Menu/MySharesViewModel.swift b/kDrive/UI/Controller/Menu/MySharesViewModel.swift index 27eb3b4da..fc3ba3044 100644 --- a/kDrive/UI/Controller/Menu/MySharesViewModel.swift +++ b/kDrive/UI/Controller/Menu/MySharesViewModel.swift @@ -35,21 +35,21 @@ class MySharesViewModel: FileListViewModel { .filesSorted(by: sortType)) } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } // Only show loading indicator if we have nothing in cache if !currentDirectory.canLoadChildrenFromCache { - startRefreshing(page: page) + startRefreshing(cursor: cursor) } defer { endRefreshing() } - let (_, moreComing) = try await driveFileManager.mySharedFiles(page: page, sortType: sortType, forceRefresh: true) + let (_, nextCursor) = try await driveFileManager.mySharedFiles(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() - if moreComing { - try await loadFiles(page: page + 1, forceRefresh: true) + if let nextCursor { + try await loadFiles(cursor: cursor, forceRefresh: true) } } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 1b253fd3d..3ead0f46a 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -154,9 +154,12 @@ public class DriveApiFetcher: ApiFetcher { .cursored(cursor))) } - public func mySharedFiles(drive: AbstractDrive, page: Int = 1, sortType: SortType = .nameAZ) async throws -> [File] { - try await perform(request: authenticatedRequest(.mySharedFiles(drive: drive).paginated(page: page) - .sorted(by: [.type, sortType]))).data + public func mySharedFiles(drive: AbstractDrive, + cursor: String? = nil, + sortType: SortType = .nameAZ) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest(.mySharedFiles(drive: drive) + .cursored(cursor) + .sorted(by: [.type, sortType]))) } public func lastModifiedFiles(drive: AbstractDrive, page: Int = 1) async throws -> [File] { diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index 4f798fcd3..c95315367 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -597,18 +597,22 @@ public final class DriveFileManager { forceRefresh: forceRefresh) } - public func mySharedFiles(page: Int = 1, sortType: SortType = .nameAZ, - forceRefresh: Bool = false) async throws -> (files: [File], moreComing: Bool) { - fatalError("TODO") - /* try await files(in: getManagedFile(from: DriveFileManager.mySharedRootFile).proxify(), + public func mySharedFiles(cursor: String? = nil, + sortType: SortType = .nameAZ, + forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { + try await files(in: getManagedFile(from: DriveFileManager.mySharedRootFile).proxify(), fetchFiles: { - let mySharedFiles = try await apiFetcher.mySharedFiles(drive: drive, page: page, sortType: sortType) - return (mySharedFiles, nil) + let mySharedFiles = try await apiFetcher.mySharedFiles( + drive: drive, + cursor: cursor, + sortType: sortType + ) + return mySharedFiles }, - cursor: "", + cursor: cursor, sortType: sortType, keepProperties: [.standard, .path, .version], - forceRefresh: forceRefresh) */ + forceRefresh: forceRefresh) } public func getAvailableOfflineFiles(sortType: SortType = .nameAZ) -> [File] { From 6ea273153e15003b1af2cb270d59a12b6a0abe89 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 12:52:43 +0100 Subject: [PATCH 08/62] feat: Cursored last modifications Signed-off-by: Philippe Weidmann --- .../Controller/Menu/LastModificationsViewModel.swift | 12 ++++++------ kDriveCore/Data/Api/DriveApiFetcher.swift | 5 +++-- kDriveCore/Data/Cache/DriveFileManager.swift | 11 ++++++----- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift index f2e3464d8..1bee5eccd 100644 --- a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift +++ b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift @@ -58,14 +58,14 @@ class LastModificationsViewModel: FileListViewModel { endRefreshing() } - /* let (_, moreComing) = try await driveFileManager.lastModifiedFiles(page: page) - endRefreshing() - if moreComing { - try await loadFiles(page: page + 1, forceRefresh: forceRefresh) - } */ + let (_, nextCursor) = try await driveFileManager.lastModifiedFiles(cursor: cursor) + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) + } } override func loadActivities() async throws { - // try await loadFiles(page: 1, forceRefresh: true) + try await loadFiles(cursor: nil, forceRefresh: true) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 3ead0f46a..dd5717139 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -162,8 +162,9 @@ public class DriveApiFetcher: ApiFetcher { .sorted(by: [.type, sortType]))) } - public func lastModifiedFiles(drive: AbstractDrive, page: Int = 1) async throws -> [File] { - try await perform(request: authenticatedRequest(.lastModifiedFiles(drive: drive).paginated(page: page))).data + public func lastModifiedFiles(drive: AbstractDrive, + cursor: String? = nil) async throws -> (data: [File], response: ApiResponse<[File]>) { + try await perform(request: authenticatedRequest(.lastModifiedFiles(drive: drive).cursored(cursor))) } public func shareLink(for file: ProxyFile) async throws -> ShareLink { diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index c95315367..a4eba672b 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -824,15 +824,16 @@ public final class DriveFileManager { } } - public func lastModifiedFiles(page: Int = 1) async throws -> (files: [File], moreComing: Bool) { + public func lastModifiedFiles(cursor: String? = nil) async throws -> (files: [File], nextCursor: String?) { do { - let files = try await apiFetcher.lastModifiedFiles(drive: drive, page: page) + let lastModifiedFilesResponse = try await apiFetcher.lastModifiedFiles(drive: drive, cursor: cursor) + let files = lastModifiedFilesResponse.data - setLocalFiles(files, root: DriveFileManager.lastModificationsRootFile, deleteOrphans: page == 1) - return (files.map { $0.freeze() }, files.count == Endpoint.itemsPerPage) + setLocalFiles(files, root: DriveFileManager.lastModificationsRootFile, deleteOrphans: cursor == nil) + return (files.map { $0.freeze() }, lastModifiedFilesResponse.response.cursor) } catch { if let files = getCachedFile(id: DriveFileManager.lastModificationsRootFile.id, freeze: true)?.children { - return (Array(files), false) + return (Array(files), nil) } else { throw error } From 6112bea5c2da49efa35d9a1507d95fbf52a81b10 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 13:12:01 +0100 Subject: [PATCH 09/62] feat: Cursored search + gallery Signed-off-by: Philippe Weidmann --- .../Files/Search/SearchViewController.swift | 24 +++++------ .../Home/HomePhotoListController.swift | 3 +- .../Menu/PhotoList/PhotoListViewModel.swift | 14 +++---- kDriveCore/Data/Api/DriveApiFetcher.swift | 8 ++-- kDriveCore/Data/Cache/DriveFileManager.swift | 42 +++++++++---------- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/kDrive/UI/Controller/Files/Search/SearchViewController.swift b/kDrive/UI/Controller/Files/Search/SearchViewController.swift index e8a5f3d60..0d1ff83bb 100644 --- a/kDrive/UI/Controller/Files/Search/SearchViewController.swift +++ b/kDrive/UI/Controller/Files/Search/SearchViewController.swift @@ -142,21 +142,21 @@ class SearchFilesViewModel: FileListViewModel { sortingChanged() } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { guard isDisplayingSearchResults else { return } - var moreComing = false + var nextCursor: String? if ReachabilityListener.instance.currentStatus == .offline { searchOffline() } else { do { - moreComing = try await driveFileManager.searchFile(query: currentSearchText, - date: filters.date?.dateInterval, - fileType: filters.fileType, - categories: Array(filters.categories), - belongToAllCategories: filters.belongToAllCategories, - page: page, - sortType: sortType) + (_, nextCursor) = try await driveFileManager.searchFile(query: currentSearchText, + date: filters.date?.dateInterval, + fileType: filters.fileType, + categories: Array(filters.categories), + belongToAllCategories: filters.belongToAllCategories, + cursor: cursor, + sortType: sortType) } catch { if let error = error as? DriveError, error == .networkError { @@ -173,8 +173,8 @@ class SearchFilesViewModel: FileListViewModel { } endRefreshing() - if moreComing { - try await loadFiles(page: page + 1) + if let nextCursor { + try await loadFiles(cursor: nextCursor) } } @@ -225,7 +225,7 @@ class SearchFilesViewModel: FileListViewModel { } if isDisplayingSearchResults { currentTask = Task { - try? await loadFiles(page: 1, forceRefresh: true) + try? await loadFiles(cursor: nil, forceRefresh: true) } } } diff --git a/kDrive/UI/Controller/Home/HomePhotoListController.swift b/kDrive/UI/Controller/Home/HomePhotoListController.swift index ab15fff45..97a3ca431 100644 --- a/kDrive/UI/Controller/Home/HomePhotoListController.swift +++ b/kDrive/UI/Controller/Home/HomePhotoListController.swift @@ -35,7 +35,8 @@ class HomePhotoListController: HomeRecentFilesController { } override func getFiles() async throws -> [File] { - return try await driveFileManager.lastPictures(page: page).files + // FIXME: Use cursor after having moved recent activities to cursor + return try await driveFileManager.lastPictures(cursor: nil).files } override func getLayout(for style: ListStyle, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { diff --git a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift index 444cb9aa2..8605d2e25 100644 --- a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift +++ b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift @@ -54,7 +54,7 @@ class PhotoListViewModel: FileListViewModel { var sections = emptySections private var moreComing = false - private var currentPage = 1 + private var nextCursor: String? private var sortMode: PhotoSortMode = UserDefaults.shared.photoSortMode { didSet { sortingChanged() } } @@ -79,7 +79,7 @@ class PhotoListViewModel: FileListViewModel { func loadNextPageIfNeeded() async throws { if !isLoading && moreComing { - try await loadFiles(page: currentPage + 1) + try await loadFiles(cursor: nextCursor) } } @@ -127,16 +127,16 @@ class PhotoListViewModel: FileListViewModel { } } - override func loadFiles(page: Int = 1, forceRefresh: Bool = false) async throws { - guard !isLoading || page > 1 else { return } + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + guard !isLoading || cursor != nil else { return } - startRefreshing(page: page) + startRefreshing(cursor: cursor) defer { endRefreshing() } - (_, moreComing) = try await driveFileManager.lastPictures(page: page) - currentPage = page + let (_, nextCursor) = try await driveFileManager.lastPictures(cursor: cursor) + self.nextCursor = nextCursor } override func barButtonPressed(type: FileListBarButtonType) { diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index dd5717139..b35f97696 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -404,9 +404,9 @@ public class DriveApiFetcher: ApiFetcher { fileTypes: [ConvertedType] = [], categories: [Category], belongToAllCategories: Bool, - page: Int = 1, + cursor: String? = nil, sortType: SortType = .nameAZ - ) async throws -> [File] { + ) async throws -> (data: [File], response: ApiResponse<[File]>) { try await perform(request: authenticatedRequest(.search( drive: drive, query: query, @@ -414,7 +414,9 @@ public class DriveApiFetcher: ApiFetcher { fileTypes: fileTypes, categories: categories, belongToAllCategories: belongToAllCategories - ).paginated(page: page).sorted(by: [sortType]))).data + ) + .cursored(cursor) + .sorted(by: [sortType]))) } public func add(category: Category, to file: ProxyFile) async throws -> CategoryResponse { diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index a4eba672b..aa9495cc7 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -587,13 +587,13 @@ public final class DriveFileManager { sortType: SortType = .nameAZ, forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { try await files(in: getManagedFile(from: DriveFileManager.favoriteRootFile).proxify(), - fetchFiles: { + fetchFiles: { let favorites = try await apiFetcher.favorites(drive: drive, cursor: cursor, sortType: sortType) return favorites - }, + }, cursor: cursor, - sortType: sortType, - keepProperties: [.standard, .extras], + sortType: sortType, + keepProperties: [.standard, .extras], forceRefresh: forceRefresh) } @@ -601,17 +601,17 @@ public final class DriveFileManager { sortType: SortType = .nameAZ, forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { try await files(in: getManagedFile(from: DriveFileManager.mySharedRootFile).proxify(), - fetchFiles: { + fetchFiles: { let mySharedFiles = try await apiFetcher.mySharedFiles( drive: drive, cursor: cursor, sortType: sortType ) return mySharedFiles - }, + }, cursor: cursor, - sortType: sortType, - keepProperties: [.standard, .path, .version], + sortType: sortType, + keepProperties: [.standard, .path, .version], forceRefresh: forceRefresh) } @@ -637,8 +637,8 @@ public final class DriveFileManager { fileType: ConvertedType? = nil, categories: [Category], belongToAllCategories: Bool, - page: Int = 1, - sortType: SortType = .nameAZ) async throws -> Bool { + cursor: String? = nil, + sortType: SortType = .nameAZ) async throws -> (files: [File], nextCursor: String?) { do { return try await remoteFiles(in: DriveFileManager.searchFilesRootFile.proxify(), fetchFiles: { @@ -649,14 +649,14 @@ public final class DriveFileManager { fileTypes: [fileType].compactMap { $0 }, categories: categories, belongToAllCategories: belongToAllCategories, - page: page, + cursor: cursor, sortType: sortType ) - return (searchResults, nil) + return searchResults }, - page: page, + isInitialCursor: cursor == nil, sortType: sortType, - keepProperties: [.standard, .extras]).moreComing + keepProperties: [.standard, .extras]) } catch { if error.asAFError?.isExplicitlyCancelledError == true { throw DriveError.searchCancelled @@ -840,22 +840,22 @@ public final class DriveFileManager { } } - public func lastPictures(page: Int = 1) async throws -> (files: [File], moreComing: Bool) { + public func lastPictures(cursor: String? = nil) async throws -> (files: [File], nextCursor: String?) { do { - let files = try await apiFetcher.searchFiles( + let lastPicturesResponse = try await apiFetcher.searchFiles( drive: drive, fileTypes: [.image, .video], categories: [], belongToAllCategories: false, - page: page, + cursor: cursor, sortType: .newer ) - - setLocalFiles(files, root: DriveFileManager.lastPicturesRootFile, deleteOrphans: page == 1) - return (files.map { $0.freeze() }, files.count == Endpoint.itemsPerPage) + let files = lastPicturesResponse.data + setLocalFiles(files, root: DriveFileManager.lastPicturesRootFile, deleteOrphans: cursor == nil) + return (files.map { $0.freeze() }, lastPicturesResponse.response.cursor) } catch { if let files = getCachedFile(id: DriveFileManager.lastPicturesRootFile.id, freeze: true)?.children { - return (Array(files), false) + return (Array(files), nil) } else { throw error } From d39bae64e7ba031d4b93180c5a8ae5c026f65606 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 13:33:39 +0100 Subject: [PATCH 10/62] feat: Cursored home Signed-off-by: Philippe Weidmann --- .../Home/HomeOfflineFilesController.swift | 4 ++-- .../Controller/Home/HomePhotoListController.swift | 5 ++--- .../Home/HomeRecentActivitiesController.swift | 15 ++++++++------- .../Home/HomeRecentFilesController.swift | 14 +++++++------- .../UI/Controller/Home/HomeViewController.swift | 2 +- kDriveCore/Data/Api/DriveApiFetcher.swift | 5 +++-- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/kDrive/UI/Controller/Home/HomeOfflineFilesController.swift b/kDrive/UI/Controller/Home/HomeOfflineFilesController.swift index c3fd4ddec..854296101 100644 --- a/kDrive/UI/Controller/Home/HomeOfflineFilesController.swift +++ b/kDrive/UI/Controller/Home/HomeOfflineFilesController.swift @@ -31,8 +31,8 @@ class HomeOfflineFilesController: HomeRecentFilesController { listStyleEnabled: true) } - override func getFiles() async throws -> [File] { - return driveFileManager.getAvailableOfflineFiles() + override func getFiles() async throws -> (files: [File], nextCursor: String?) { + return (driveFileManager.getAvailableOfflineFiles(), nil) } override func refreshIfNeeded(with file: File) { diff --git a/kDrive/UI/Controller/Home/HomePhotoListController.swift b/kDrive/UI/Controller/Home/HomePhotoListController.swift index 97a3ca431..6aed19ebc 100644 --- a/kDrive/UI/Controller/Home/HomePhotoListController.swift +++ b/kDrive/UI/Controller/Home/HomePhotoListController.swift @@ -34,9 +34,8 @@ class HomePhotoListController: HomeRecentFilesController { listStyleEnabled: false) } - override func getFiles() async throws -> [File] { - // FIXME: Use cursor after having moved recent activities to cursor - return try await driveFileManager.lastPictures(cursor: nil).files + override func getFiles() async throws -> (files: [File], nextCursor: String?) { + return try await driveFileManager.lastPictures(cursor: nextCursor) } override func getLayout(for style: ListStyle, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { diff --git a/kDrive/UI/Controller/Home/HomeRecentActivitiesController.swift b/kDrive/UI/Controller/Home/HomeRecentActivitiesController.swift index 36c1c8aa4..57ea980c8 100644 --- a/kDrive/UI/Controller/Home/HomeRecentActivitiesController.swift +++ b/kDrive/UI/Controller/Home/HomeRecentActivitiesController.swift @@ -56,16 +56,17 @@ class HomeRecentActivitiesController: HomeRecentFilesController { Task { do { - let activities = try await driveFileManager.apiFetcher.recentActivity(drive: driveFileManager.drive, page: page) - self.empty = self.page == 1 && activities.isEmpty - self.moreComing = activities.count == Endpoint.itemsPerPage + let activitiesResponse = try await driveFileManager.apiFetcher.recentActivity(drive: driveFileManager.drive, + cursor: nextCursor) + self.empty = self.nextCursor == nil && activitiesResponse.data.isEmpty + self.moreComing = activitiesResponse.response.hasMore - display(activities: activities) + display(activities: activitiesResponse.data) // Update cache - if self.page == 1 { - self.driveFileManager.setLocalRecentActivities(activities) + if nextCursor == nil { + self.driveFileManager.setLocalRecentActivities(activitiesResponse.data) } - self.page += 1 + self.nextCursor = activitiesResponse.response.cursor } catch { let activities = self.driveFileManager.getLocalRecentActivities() self.empty = activities.isEmpty diff --git a/kDrive/UI/Controller/Home/HomeRecentFilesController.swift b/kDrive/UI/Controller/Home/HomeRecentFilesController.swift index ed9179326..889c6b75d 100644 --- a/kDrive/UI/Controller/Home/HomeRecentFilesController.swift +++ b/kDrive/UI/Controller/Home/HomeRecentFilesController.swift @@ -42,7 +42,7 @@ class HomeRecentFilesController { var listStyle: ListStyle = .list var listStyleEnabled: Bool - var page = 1 + var nextCursor: String? var empty = false var loading = false var moreComing = true @@ -71,7 +71,7 @@ class HomeRecentFilesController { self.homeViewController = homeViewController } - func getFiles() async throws -> [File] { + func getFiles() async throws -> (files: [File], nextCursor: String?) { fatalError(#function + " needs to be overwritten") } @@ -104,7 +104,7 @@ class HomeRecentFilesController { func resetController() { files = [] - page = 1 + nextCursor = nil loading = false moreComing = true } @@ -123,10 +123,10 @@ class HomeRecentFilesController { Task { do { let fetchedFiles = try await getFiles() - self.files.append(contentsOf: fetchedFiles) - self.empty = self.page == 1 && fetchedFiles.isEmpty - self.moreComing = fetchedFiles.count == Endpoint.itemsPerPage - self.page += 1 + self.files.append(contentsOf: fetchedFiles.files) + self.empty = self.nextCursor == nil && fetchedFiles.files.isEmpty + self.moreComing = fetchedFiles.nextCursor == nil + self.nextCursor = fetchedFiles.nextCursor guard !self.invalidated else { return diff --git a/kDrive/UI/Controller/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift index c1a1fc122..33b138656 100644 --- a/kDrive/UI/Controller/Home/HomeViewController.swift +++ b/kDrive/UI/Controller/Home/HomeViewController.swift @@ -400,7 +400,7 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top headerView?.titleLabel.text = currentRecentFilesController.title headerView?.switchLayoutButton.isHidden = !currentRecentFilesController.listStyleEnabled - if currentRecentFilesController.page == 1 { + if currentRecentFilesController.nextCursor == nil { reload(newViewModel: HomeViewModel(topRows: viewModel.topRows, recentFiles: .file([]), recentFilesEmpty: false, diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index b35f97696..6d0cc7694 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -289,8 +289,9 @@ public class DriveApiFetcher: ApiFetcher { try await perform(request: authenticatedRequest(.move(file: file, destination: destination), method: .post)).data } - public func recentActivity(drive: AbstractDrive, page: Int = 1) async throws -> [FileActivity] { - try await perform(request: authenticatedRequest(.recentActivity(drive: drive).paginated(page: page))).data + public func recentActivity(drive: AbstractDrive, cursor: String? = nil) + async throws -> (data: [FileActivity], response: ApiResponse<[FileActivity]>) { + try await perform(request: authenticatedRequest(.recentActivity(drive: drive).cursored(cursor))) } public func fileActivities(file: ProxyFile, page: Int) async throws -> [FileActivity] { From 7eb3326165ed4d29215e3751c0bb8d67ed8b6430 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 13:42:49 +0100 Subject: [PATCH 11/62] fix: To compile trash file provider Signed-off-by: Philippe Weidmann --- kDriveFileProvider/FileProviderEnumerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveFileProvider/FileProviderEnumerator.swift b/kDriveFileProvider/FileProviderEnumerator.swift index 78da4d4ed..f4bcf74ef 100644 --- a/kDriveFileProvider/FileProviderEnumerator.swift +++ b/kDriveFileProvider/FileProviderEnumerator.swift @@ -124,7 +124,7 @@ final class FileProviderEnumerator: NSObject, NSFileProviderEnumerator { let children = try await self.driveFileManager.apiFetcher.trashedFiles( of: file.proxify(), cursor: cursor - ) + ).data var containerItems = [FileProviderItem]() for child in children { autoreleasepool { From 821661a02b316ab3741370c846b348d60ba5caa4 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 14:37:13 +0100 Subject: [PATCH 12/62] fix: Use AppLaunchCounter Signed-off-by: Philippe Weidmann --- .package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.package.resolved b/.package.resolved index 995d0664b..2b99b5499 100644 --- a/.package.resolved +++ b/.package.resolved @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-core", "state" : { - "revision" : "02f54f97557ea8340df63649081e39faddd8af26", - "version" : "4.1.29" + "branch" : "cursored-api-response", + "revision" : "782d650b330c0e92ac235646d96691c3993161dc" } }, { From b4a604c1d5cd078287cd4349cffc8e85e252455e Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 16 Nov 2023 10:02:49 +0100 Subject: [PATCH 13/62] feat: Basic listing with cursor Signed-off-by: Philippe Weidmann --- .../File List/FileListViewController.swift | 31 ++++----- .../Data/Api/DriveApiFetcher+Listing.swift | 51 +++++++++++++++ kDriveCore/Data/Api/Endpoint.swift | 11 ++++ kDriveCore/Data/Api/FileWith.swift | 22 +++++++ .../Data/Cache/DriveFileManager+Listing.swift | 64 +++++++++++++++++++ kDriveCore/Data/Cache/DriveFileManager.swift | 2 +- kDriveCore/Data/Models/File.swift | 4 ++ kDriveCore/Data/Models/ListingResult.swift | 23 +++++++ 8 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 kDriveCore/Data/Api/DriveApiFetcher+Listing.swift create mode 100644 kDriveCore/Data/Cache/DriveFileManager+Listing.swift create mode 100644 kDriveCore/Data/Models/ListingResult.swift diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index c52b8a1ce..e3808a5d3 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -100,26 +100,19 @@ class ConcreteFileListViewModel: FileListViewModel { override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { guard !isLoading || cursor != nil else { return } - if currentDirectory.canLoadChildrenFromCache && !forceRefresh { - // try await loadActivitiesIfNeeded() - } else { - startRefreshing(cursor: cursor) - defer { - endRefreshing() - } - - let (_, nextCursor) = try await driveFileManager.files( - in: currentDirectory.proxify(), - cursor: cursor, - sortType: sortType, - forceRefresh: forceRefresh - ) + startRefreshing(cursor: cursor) + defer { endRefreshing() - if let nextCursor { - try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) - } else if !forceRefresh { - try await loadActivities() - } + } + + let (_, nextCursor) = try await driveFileManager.fileListing( + in: currentDirectory.proxify(), + sortType: sortType, + forceRefresh: forceRefresh + ) + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift b/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift new file mode 100644 index 000000000..2fcf5a79b --- /dev/null +++ b/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift @@ -0,0 +1,51 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCore + +public extension DriveApiFetcher { + func files(in directory: ProxyFile, + sortType: SortType = .nameAZ) async throws -> (data: ListingResult, response: ApiResponse) { + try await perform(request: authenticatedRequest( + .fileListing(file: directory) + .sorted(by: [.type, sortType]), + method: .get + )) + } + + func files(in directory: ProxyFile, + listingCursor: FileCursor, + sortType: SortType = .nameAZ) async throws -> (data: ListingResult, response: ApiResponse) { + try await perform(request: authenticatedRequest( + .fileListingContinue(file: directory, cursor: listingCursor) + .sorted(by: [.type, sortType]), + method: .get + )) + } + + func files(in directory: ProxyFile, + listingCursor: FileCursor?, + sortType: SortType = .nameAZ) async throws -> (data: ListingResult, response: ApiResponse) { + if let listingCursor { + return try await files(in: directory, listingCursor: listingCursor, sortType: sortType) + } else { + return try await files(in: directory, sortType: sortType) + } + } +} diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index c68490049..a20c75ca7 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -166,6 +166,17 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/cancel") } + // MARK: Listing + + static func fileListing(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/listing", queryItems: [FileWith.fileListingMinimal.toQueryItem()]) + } + + static func fileListingContinue(file: AbstractFile, cursor: String) -> Endpoint { + return .fileInfo(file).appending(path: "/listing/continue", queryItems: [URLQueryItem(name: "cursor", value: cursor), + FileWith.fileListingMinimal.toQueryItem()]) + } + // MARK: Activities static func recentActivity(drive: AbstractDrive) -> Endpoint { diff --git a/kDriveCore/Data/Api/FileWith.swift b/kDriveCore/Data/Api/FileWith.swift index 0a75b9c8f..440e3d0d8 100644 --- a/kDriveCore/Data/Api/FileWith.swift +++ b/kDriveCore/Data/Api/FileWith.swift @@ -43,6 +43,18 @@ enum FileWith: String, CaseIterable { case user case version + case files + case filesCapabilities = "files.capabilities" + case filesPath = "files.path" + case filesSortedName = "files.sorted_name" + case filesDropbox = "files.dropbox" + case filesDropboxCapabilities = "files.dropbox.capabilities" + case filesIsFavorite = "files.is_favorite" + case filesShareLink = "files.sharelink" + case filesCategories = "files.categories" + case filesConversionCapabilities = "files.conversion_capabilities" + case filesExternalImport = "files.external_import" + static let fileMinimal: [FileWith] = [.capabilities, .categories, .conversionCapabilities, @@ -52,6 +64,16 @@ enum FileWith: String, CaseIterable { .isFavorite, .shareLink, .sortedName] + static let fileListingMinimal: [FileWith] = [.files, + .filesCapabilities, + .filesCategories, + .filesConversionCapabilities, + .filesDropbox, + .filesDropboxCapabilities, + .filesExternalImport, + .filesIsFavorite, + .filesShareLink, + .filesSortedName] static let fileExtra: [FileWith] = fileMinimal + [.path, .users, .version] static let fileActivities: [FileWith] = [.file, .fileCapabilities, diff --git a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift new file mode 100644 index 000000000..8ad6df53d --- /dev/null +++ b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift @@ -0,0 +1,64 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCore + +public extension DriveFileManager { + func fileListing(in directory: ProxyFile, + sortType: SortType = .nameAZ, + forceRefresh: Bool = false) async throws -> (files: [File], nextCursor: String?) { + guard !directory.isRoot else { + return try await files(in: directory, cursor: nil, sortType: sortType, forceRefresh: forceRefresh) + } + + let lastCursor = forceRefresh ? nil : try directory.resolve(using: getRealm()).lastCursor + + let result = try await apiFetcher.files(in: directory, listingCursor: lastCursor, sortType: sortType) + + let children = result.data.files + let nextCursor = result.response.cursor + let hasMore = result.response.hasMore + + let realm = getRealm() + // Keep cached properties for children + for child in children { + keepCacheAttributesForFile(newFile: child, keepProperties: [.standard, .extras], using: realm) + } + + let managedParent = try directory.resolve(using: realm) + + try realm.write { + managedParent.lastCursor = nextCursor + managedParent.versionCode = DriveFileManager.constants.currentVersionCode + + realm.add(children, update: .modified) + // ⚠️ this is important because we are going to add all the children again. However, failing to start the request with + // the first page will result in an undefined behavior. + if lastCursor == nil { + managedParent.children.removeAll() + } + managedParent.children.insert(objectsIn: children) + } + + return ( + getLocalSortedDirectoryFiles(directory: managedParent, sortType: sortType), + hasMore ? nextCursor : nil + ) + } +} diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index aa9495cc7..fc31658d6 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -1509,7 +1509,7 @@ public final class DriveFileManager { static let all: FilePropertiesOptions = [.fullyDownloaded, .children, .responseAt, .path, .users, .version, .capabilities] } - private func keepCacheAttributesForFile(newFile: File, keepProperties: FilePropertiesOptions, using realm: Realm? = nil) { + func keepCacheAttributesForFile(newFile: File, keepProperties: FilePropertiesOptions, using realm: Realm? = nil) { let realm = realm ?? getRealm() realm.refresh() diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index 1491154db..ae8c5e4ce 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -337,6 +337,8 @@ public final class FileVersion: EmbeddedObject, Codable { } } +public typealias FileCursor = String + public final class File: Object, Codable { private let fileManager = FileManager.default @@ -404,6 +406,8 @@ public final class File: Object, Codable { /// File can be converted to another extension @Persisted public var conversion: FileConversion? + @Persisted public var lastCursor: FileCursor? + // Other @Persisted public var children: MutableSet @Persisted(originProperty: "children") var parentLink: LinkingObjects diff --git a/kDriveCore/Data/Models/ListingResult.swift b/kDriveCore/Data/Models/ListingResult.swift new file mode 100644 index 000000000..78506785b --- /dev/null +++ b/kDriveCore/Data/Models/ListingResult.swift @@ -0,0 +1,23 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +public struct ListingResult: Codable { + public let files: [File] +} From f7166ecffb992e07869a5a762c97650723b37ed6 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 16 Nov 2023 12:29:47 +0100 Subject: [PATCH 14/62] fix: Rename is on api V2 Signed-off-by: Philippe Weidmann --- kDriveCore/Data/Api/Endpoint.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index a20c75ca7..af6871c18 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -457,7 +457,7 @@ public extension Endpoint { } static func rename(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) + return .fileInfoV2(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) } static func count(of directory: AbstractFile) -> Endpoint { From e73623f10caaf397f3fafb12f5365391e6eea54d Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 16 Nov 2023 13:23:07 +0100 Subject: [PATCH 15/62] feat: Listing file actions Signed-off-by: Philippe Weidmann --- .package.resolved | 2 +- .../File List/FileListViewController.swift | 2 +- .../Data/Cache/DriveFileManager+Listing.swift | 64 +++++++++++++++++-- kDriveCore/Data/Cache/DriveFileManager.swift | 6 +- kDriveCore/Data/Models/FileAction.swift | 29 +++++++++ kDriveCore/Data/Models/ListingResult.swift | 8 +++ 6 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 kDriveCore/Data/Models/FileAction.swift diff --git a/.package.resolved b/.package.resolved index 2b99b5499..4921912c7 100644 --- a/.package.resolved +++ b/.package.resolved @@ -87,7 +87,7 @@ "location" : "https://github.com/Infomaniak/ios-core", "state" : { "branch" : "cursored-api-response", - "revision" : "782d650b330c0e92ac235646d96691c3993161dc" + "revision" : "bf243e8117da74c3559b407c99d625b99e21431f" } }, { diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index e3808a5d3..331790eb6 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -117,7 +117,7 @@ class ConcreteFileListViewModel: FileListViewModel { } override func loadActivities() async throws { - _ = try await driveFileManager.fileActivities(file: currentDirectory.proxify()) + try await loadFiles() } override func barButtonPressed(type: FileListBarButtonType) { diff --git a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift index 8ad6df53d..692051563 100644 --- a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift +++ b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift @@ -18,6 +18,7 @@ import Foundation import InfomaniakCore +import RealmSwift public extension DriveFileManager { func fileListing(in directory: ProxyFile, @@ -44,16 +45,17 @@ public extension DriveFileManager { let managedParent = try directory.resolve(using: realm) try realm.write { - managedParent.lastCursor = nextCursor - managedParent.versionCode = DriveFileManager.constants.currentVersionCode - realm.add(children, update: .modified) - // ⚠️ this is important because we are going to add all the children again. However, failing to start the request with - // the first page will result in an undefined behavior. + if lastCursor == nil { managedParent.children.removeAll() } managedParent.children.insert(objectsIn: children) + + handleActions(result.data.actions, actionsFiles: result.data.actionsFiles, directory: managedParent, using: realm) + + managedParent.lastCursor = nextCursor + managedParent.versionCode = DriveFileManager.constants.currentVersionCode } return ( @@ -61,4 +63,56 @@ public extension DriveFileManager { hasMore ? nextCursor : nil ) } + + func handleActions(_ actions: [FileAction], actionsFiles: [File], directory: File, using realm: Realm) { + let mappedActionsFiles = Dictionary(grouping: actionsFiles, by: \.id) + + for fileAction in actions { + guard let actionFile = mappedActionsFiles[fileAction.fileId]?.first else { continue } + + switch fileAction.action { + case .fileDelete, .fileTrash: + removeFileInDatabase(fileId: fileAction.fileId, cascade: true, withTransaction: false, using: realm) + + case .fileMoveOut: + guard let movedOutFile: File = realm.getObject(id: fileAction.fileId), + let oldParent = movedOutFile.parent else { continue } + + oldParent.children.remove(movedOutFile) + + case .fileRename: + guard let oldFile: File = realm.getObject(id: fileAction.fileId) else { continue } + try? renameCachedFile(updatedFile: actionFile, oldFile: oldFile) + // If the file is a folder we have to copy the old attributes which are not returned by the API + keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) + realm.add(actionFile, update: .modified) + actionFile.applyLastModifiedDateToLocalFile() + + case .fileMoveIn, .fileRestore, .fileCreate: + keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) + realm.add(actionFile, update: .modified) + + if let existingFile: File = realm.getObject(id: fileAction.fileId), + let oldParent = existingFile.parent { + oldParent.children.remove(existingFile) + } + directory.children.insert(actionFile) + + case .fileFavoriteCreate, .fileFavoriteRemove, .fileUpdate, .fileShareCreate, .fileShareUpdate, .fileShareDelete, + .collaborativeFolderCreate, .collaborativeFolderUpdate, .collaborativeFolderDelete, .fileColorUpdate, + .fileColorDelete: + guard actionFile.isTrashed else { + removeFileInDatabase(fileId: fileAction.fileId, cascade: true, withTransaction: false, using: realm) + continue + } + + keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) + realm.add(actionFile, update: .modified) + directory.children.insert(actionFile) + + default: + break + } + } + } } diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index fc31658d6..8e1103d03 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -1414,7 +1414,7 @@ public final class DriveFileManager { return Array(children.freeze()) } - private func removeFileInDatabase(fileId: Int, cascade: Bool, withTransaction: Bool, using realm: Realm? = nil) { + func removeFileInDatabase(fileId: Int, cascade: Bool, withTransaction: Bool, using realm: Realm? = nil) { let realm = realm ?? getRealm() realm.refresh() @@ -1583,6 +1583,10 @@ public extension Realm { try write(block) } } + + func getObject(id: KeyType) -> RealmObject? { + object(ofType: RealmObject.self, forPrimaryKey: id) + } } // MARK: - Observation diff --git a/kDriveCore/Data/Models/FileAction.swift b/kDriveCore/Data/Models/FileAction.swift new file mode 100644 index 000000000..fab011d18 --- /dev/null +++ b/kDriveCore/Data/Models/FileAction.swift @@ -0,0 +1,29 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +public struct FileAction: Codable { + let action: FileActivityType + let fileId: Int + + enum CodingKeys: String, CodingKey { + case action + case fileId = "file_id" + } +} diff --git a/kDriveCore/Data/Models/ListingResult.swift b/kDriveCore/Data/Models/ListingResult.swift index 78506785b..2e5ea9fb3 100644 --- a/kDriveCore/Data/Models/ListingResult.swift +++ b/kDriveCore/Data/Models/ListingResult.swift @@ -19,5 +19,13 @@ import Foundation public struct ListingResult: Codable { + public let actions: [FileAction] public let files: [File] + public let actionsFiles: [File] + + enum CodingKeys: String, CodingKey { + case actions + case files + case actionsFiles = "actions_files" + } } From 6d7e3ad0743a2a6a68605da44b82d2fe0043e0bc Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 5 Dec 2023 11:43:16 +0100 Subject: [PATCH 16/62] fix: Correctly add InfomaniakCoreUI Signed-off-by: Philippe Weidmann --- .package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.package.resolved b/.package.resolved index 4921912c7..aedd6ebc2 100644 --- a/.package.resolved +++ b/.package.resolved @@ -87,7 +87,7 @@ "location" : "https://github.com/Infomaniak/ios-core", "state" : { "branch" : "cursored-api-response", - "revision" : "bf243e8117da74c3559b407c99d625b99e21431f" + "revision" : "60313af3e40796b05dea4faf99df5bee15ec6e24" } }, { From 6c74e5fc4f0ceb441529743be1caf5778286260e Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 15 Dec 2023 10:31:15 +0100 Subject: [PATCH 17/62] refactor: Renamed listingCursor to advancedListingCursor Signed-off-by: Philippe Weidmann --- kDriveCore/Data/Api/DriveApiFetcher+Listing.swift | 10 +++++----- kDriveCore/Data/Cache/DriveFileManager+Listing.swift | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift b/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift index 2fcf5a79b..34bbaae4d 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher+Listing.swift @@ -30,20 +30,20 @@ public extension DriveApiFetcher { } func files(in directory: ProxyFile, - listingCursor: FileCursor, + advancedListingCursor: FileCursor, sortType: SortType = .nameAZ) async throws -> (data: ListingResult, response: ApiResponse) { try await perform(request: authenticatedRequest( - .fileListingContinue(file: directory, cursor: listingCursor) + .fileListingContinue(file: directory, cursor: advancedListingCursor) .sorted(by: [.type, sortType]), method: .get )) } func files(in directory: ProxyFile, - listingCursor: FileCursor?, + advancedListingCursor: FileCursor?, sortType: SortType = .nameAZ) async throws -> (data: ListingResult, response: ApiResponse) { - if let listingCursor { - return try await files(in: directory, listingCursor: listingCursor, sortType: sortType) + if let advancedListingCursor { + return try await files(in: directory, advancedListingCursor: advancedListingCursor, sortType: sortType) } else { return try await files(in: directory, sortType: sortType) } diff --git a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift index 692051563..982469fba 100644 --- a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift +++ b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift @@ -30,7 +30,7 @@ public extension DriveFileManager { let lastCursor = forceRefresh ? nil : try directory.resolve(using: getRealm()).lastCursor - let result = try await apiFetcher.files(in: directory, listingCursor: lastCursor, sortType: sortType) + let result = try await apiFetcher.files(in: directory, advancedListingCursor: lastCursor, sortType: sortType) let children = result.data.files let nextCursor = result.response.cursor From 1691f64e14e60c265dc53486336466445698f7c3 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 19 Dec 2023 07:47:04 +0100 Subject: [PATCH 18/62] fix: Keep lastCursor for children Signed-off-by: Philippe Weidmann --- kDriveCore/Data/Cache/DriveFileManager.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager.swift index 8e1103d03..377bffd30 100644 --- a/kDriveCore/Data/Cache/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager.swift @@ -1503,10 +1503,20 @@ public final class DriveFileManager { static let users = FilePropertiesOptions(rawValue: 1 << 4) static let version = FilePropertiesOptions(rawValue: 1 << 5) static let capabilities = FilePropertiesOptions(rawValue: 1 << 6) + static let lastCursor = FilePropertiesOptions(rawValue: 1 << 7) - static let standard: FilePropertiesOptions = [.fullyDownloaded, .children, .responseAt] + static let standard: FilePropertiesOptions = [.fullyDownloaded, .children, .responseAt, .lastCursor] static let extras: FilePropertiesOptions = [.path, .users, .version] - static let all: FilePropertiesOptions = [.fullyDownloaded, .children, .responseAt, .path, .users, .version, .capabilities] + static let all: FilePropertiesOptions = [ + .fullyDownloaded, + .children, + .responseAt, + .lastCursor, + .path, + .users, + .version, + .capabilities + ] } func keepCacheAttributesForFile(newFile: File, keepProperties: FilePropertiesOptions, using realm: Realm? = nil) { @@ -1526,6 +1536,9 @@ public final class DriveFileManager { if keepProperties.contains(.responseAt) { newFile.responseAt = savedChild.responseAt } + if keepProperties.contains(.lastCursor) { + newFile.lastCursor = savedChild.lastCursor + } if keepProperties.contains(.path) { newFile.path = savedChild.path } From 13cc197a5376ba2b172bc558a52f51463bd5bed3 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 19 Dec 2023 08:04:56 +0100 Subject: [PATCH 19/62] fix: Only force refresh first call Signed-off-by: Philippe Weidmann --- kDrive/UI/Controller/Favorite/FavoritesViewModel.swift | 2 +- .../UI/Controller/Files/File List/FileListViewController.swift | 2 +- kDrive/UI/Controller/Menu/LastModificationsViewModel.swift | 2 +- kDrive/UI/Controller/Menu/MySharesViewModel.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift b/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift index 96eda81e4..6cca6fbab 100644 --- a/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift +++ b/kDrive/UI/Controller/Favorite/FavoritesViewModel.swift @@ -51,7 +51,7 @@ class FavoritesViewModel: FileListViewModel { let (_, nextCursor) = try await driveFileManager.favorites(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() if let nextCursor { - try await loadFiles(cursor: nextCursor, forceRefresh: true) + try await loadFiles(cursor: nextCursor) } } } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 331790eb6..78e53f83d 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -112,7 +112,7 @@ class ConcreteFileListViewModel: FileListViewModel { ) endRefreshing() if let nextCursor { - try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) + try await loadFiles(cursor: nextCursor) } } diff --git a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift index 1bee5eccd..760181d77 100644 --- a/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift +++ b/kDrive/UI/Controller/Menu/LastModificationsViewModel.swift @@ -61,7 +61,7 @@ class LastModificationsViewModel: FileListViewModel { let (_, nextCursor) = try await driveFileManager.lastModifiedFiles(cursor: cursor) endRefreshing() if let nextCursor { - try await loadFiles(cursor: nextCursor, forceRefresh: forceRefresh) + try await loadFiles(cursor: nextCursor) } } diff --git a/kDrive/UI/Controller/Menu/MySharesViewModel.swift b/kDrive/UI/Controller/Menu/MySharesViewModel.swift index fc3ba3044..c9dd896be 100644 --- a/kDrive/UI/Controller/Menu/MySharesViewModel.swift +++ b/kDrive/UI/Controller/Menu/MySharesViewModel.swift @@ -49,7 +49,7 @@ class MySharesViewModel: FileListViewModel { let (_, nextCursor) = try await driveFileManager.mySharedFiles(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() if let nextCursor { - try await loadFiles(cursor: cursor, forceRefresh: true) + try await loadFiles(cursor: nextCursor) } } } From e68f9ddbeb5b601aa029e2b57416c9b75d6628a5 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 19 Dec 2023 11:32:53 +0100 Subject: [PATCH 20/62] feat: Use filesSupportedBy Signed-off-by: Philippe Weidmann --- .../Files/FileDetailViewController.swift | 9 ++++--- .../Files/Preview/PreviewViewController.swift | 2 +- .../View/Files/FileCollectionViewCell.swift | 5 ++-- .../Files/FileGridCollectionViewCell.swift | 2 +- .../RecentActivityCollectionViewCell.swift | 3 ++- kDriveCore/Data/Api/FileWith.swift | 4 ++- kDriveCore/Data/Models/File+Image.swift | 2 +- kDriveCore/Data/Models/File.swift | 25 ++++++++++++------- kDriveCore/UI/UIConstants.swift | 2 +- 9 files changed, 33 insertions(+), 21 deletions(-) diff --git a/kDrive/UI/Controller/Files/FileDetailViewController.swift b/kDrive/UI/Controller/Files/FileDetailViewController.swift index 32efc7837..99e401007 100644 --- a/kDrive/UI/Controller/Files/FileDetailViewController.swift +++ b/kDrive/UI/Controller/Files/FileDetailViewController.swift @@ -124,7 +124,8 @@ class FileDetailViewController: UIViewController { } override var preferredStatusBarStyle: UIStatusBarStyle { - if (tableView != nil && tableView.contentOffset.y > 0) || UIDevice.current.orientation.isLandscape || !file.hasThumbnail { + if (tableView != nil && tableView.contentOffset.y > 0) || UIDevice.current.orientation.isLandscape || !file.supportedBy + .contains(.thumbnail) { return .default } else { return .lightContent @@ -134,7 +135,7 @@ class FileDetailViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.navigationBar.tintColor = tableView.contentOffset.y == 0 && UIDevice.current.orientation - .isPortrait && file.hasThumbnail ? .white : nil + .isPortrait && file.supportedBy.contains(.thumbnail) ? .white : nil let navigationBarAppearanceStandard = UINavigationBarAppearance() navigationBarAppearanceStandard.configureWithTransparentBackground() navigationBarAppearanceStandard.backgroundColor = KDriveResourcesAsset.backgroundColor.color @@ -551,7 +552,7 @@ extension FileDetailViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 0 { - if !file.hasThumbnail { + if !file.supportedBy.contains(.thumbnail) { let cell = tableView.dequeueReusableCell(type: FileDetailHeaderAltTableViewCell.self, for: indexPath) cell.delegate = self cell.configureWith(file: file) @@ -798,7 +799,7 @@ extension FileDetailViewController { navigationController?.navigationBar.tintColor = nil } else { title = "" - navigationController?.navigationBar.tintColor = file.hasThumbnail ? .white : nil + navigationController?.navigationBar.tintColor = file.supportedBy.contains(.thumbnail) ? .white : nil } } else { title = scrollView.contentOffset.y > 200 ? file.name : "" diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 90e52243d..3ab0fbd5c 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -720,7 +720,7 @@ extension PreviewViewController: UICollectionViewDataSource { cell.previewDelegate = self return cell } - } else if file.hasThumbnail && !ConvertedType.ignoreThumbnailTypes.contains(file.convertedType) { + } else if file.supportedBy.contains(.thumbnail) && !ConvertedType.ignoreThumbnailTypes.contains(file.convertedType) { let cell = collectionView.dequeueReusableCell(type: DownloadingPreviewCollectionViewCell.self, for: indexPath) if let downloadOperation = currentDownloadOperation, let progress = downloadOperation.task?.progress, diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 548cb3c02..8d0f54194 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -41,7 +41,7 @@ protocol FileCellDelegate: AnyObject { "isAvailableOffline", "categories", "size", - "hasThumbnail", + "supportedBy", "color", "externalImport.status" ] @@ -138,7 +138,8 @@ protocol FileCellDelegate: AnyObject { func setThumbnail(on imageView: UIImageView) { guard !file.isInvalidated, - (file.convertedType == .image || file.convertedType == .video) && file.hasThumbnail else { return } + (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) + else { return } // Configure placeholder imageView.image = nil imageView.contentMode = .scaleAspectFill diff --git a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift index 9d8b28ca3..b22365d3e 100644 --- a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift @@ -23,7 +23,7 @@ import UIKit final class FileGridViewModel: FileViewModel { var iconImageHidden: Bool { file.isDirectory } - var hasThumbnail: Bool { !file.isDirectory && file.hasThumbnail } + var hasThumbnail: Bool { !file.isDirectory && file.supportedBy.contains(.thumbnail) } var shouldCenterTitle: Bool { file.isDirectory } diff --git a/kDrive/UI/View/Home/RecentActivityCollectionViewCell.swift b/kDrive/UI/View/Home/RecentActivityCollectionViewCell.swift index 52a4a4ce6..cc4075d47 100644 --- a/kDrive/UI/View/Home/RecentActivityCollectionViewCell.swift +++ b/kDrive/UI/View/Home/RecentActivityCollectionViewCell.swift @@ -156,7 +156,8 @@ class RecentActivityCollectionViewCell: InsetCollectionViewCell, UICollectionVie } else { let activity = activities[indexPath.item] let more = indexPath.item == 2 && activities.count > 3 ? activities.count - 2 : nil - if let file = activity.file, file.hasThumbnail && (file.convertedType == .image || file.convertedType == .video) { + if let file = activity.file, + file.supportedBy.contains(.thumbnail) && (file.convertedType == .image || file.convertedType == .video) { cell.configureWithPreview(file: file, more: more) } else { cell.configureWithoutPreview(file: activity.file, more: more) diff --git a/kDriveCore/Data/Api/FileWith.swift b/kDriveCore/Data/Api/FileWith.swift index 440e3d0d8..e31fc256c 100644 --- a/kDriveCore/Data/Api/FileWith.swift +++ b/kDriveCore/Data/Api/FileWith.swift @@ -54,6 +54,7 @@ enum FileWith: String, CaseIterable { case filesCategories = "files.categories" case filesConversionCapabilities = "files.conversion_capabilities" case filesExternalImport = "files.external_import" + case filesSupportedBy = "files.supported_by" static let fileMinimal: [FileWith] = [.capabilities, .categories, @@ -73,7 +74,8 @@ enum FileWith: String, CaseIterable { .filesExternalImport, .filesIsFavorite, .filesShareLink, - .filesSortedName] + .filesSortedName, + .filesSupportedBy] static let fileExtra: [FileWith] = fileMinimal + [.path, .users, .version] static let fileActivities: [FileWith] = [.file, .fileCapabilities, diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index f19a82b2e..87fab6c8e 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -22,7 +22,7 @@ import UIKit public extension File { @discardableResult func getThumbnail(completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { - if hasThumbnail, let currentDriveFileManager = accountManager.currentDriveFileManager { + if supportedBy.contains(.thumbnail), let currentDriveFileManager = accountManager.currentDriveFileManager { return KingfisherManager.shared.retrieveImage(with: thumbnailURL, options: [.requestModifier(currentDriveFileManager.apiFetcher .authenticatedKF)]) { result in diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index ae8c5e4ce..aa5ce5158 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -337,6 +337,13 @@ public final class FileVersion: EmbeddedObject, Codable { } } +public enum FileSupportedBy: String, PersistableEnum, Codable { + /// This file can have a thumbnail generated + case thumbnail + /// This file can be read by OnlyOffice + case onlyOffice = "onlyoffice" +} + public typealias FileCursor = String public final class File: Object, Codable { @@ -395,10 +402,10 @@ public final class File: Object, Codable { // File only /// Size of File (byte unit) @Persisted public var size: Int? - /// File has thumbnail, if so you can request thumbnail route - @Persisted public var hasThumbnail: Bool - /// File can be handled by only-office - @Persisted public var hasOnlyoffice: Bool + + /// Contains all the services that supports this file, for available services see *FileSupportedBy* + @Persisted public var supportedBy: MutableSet + /// File type @Persisted public var extensionType: String? /// Information when file has multi-version @@ -444,12 +451,11 @@ public final class File: Object, Codable { case color case dropbox case size - case hasThumbnail = "has_thumbnail" - case hasOnlyoffice = "has_onlyoffice" case extensionType = "extension_type" case externalImport = "external_import" case version case conversion = "conversion_capabilities" + case supportedBy = "supported_by" } public var parent: File? { @@ -532,7 +538,7 @@ public final class File: Object, Codable { } public var isOfficeFile: Bool { - return hasOnlyoffice || conversion?.whenOnlyoffice == true + return supportedBy.contains(.onlyOffice) || conversion?.whenOnlyoffice == true } public var isBookmark: Bool { @@ -752,8 +758,9 @@ public final class File: Object, Codable { dropbox = try container.decodeIfPresent(DropBox.self, forKey: .dropbox) externalImport = try container.decodeIfPresent(FileExternalImport.self, forKey: .externalImport) size = try container.decodeIfPresent(Int.self, forKey: .size) - hasThumbnail = try container.decodeIfPresent(Bool.self, forKey: .hasThumbnail) ?? false - hasOnlyoffice = try container.decodeIfPresent(Bool.self, forKey: .hasOnlyoffice) ?? false + let rawSupportedBy = try container.decodeIfPresent([String].self, forKey: .supportedBy) ?? [] + supportedBy = MutableSet() + supportedBy.insert(objectsIn: rawSupportedBy.compactMap { FileSupportedBy(rawValue: $0) }) extensionType = try container.decodeIfPresent(String.self, forKey: .extensionType) version = try container.decodeIfPresent(FileVersion.self, forKey: .version) conversion = try container.decodeIfPresent(FileConversion.self, forKey: .conversion) diff --git a/kDriveCore/UI/UIConstants.swift b/kDriveCore/UI/UIConstants.swift index 260edf705..86b762e36 100644 --- a/kDriveCore/UI/UIConstants.swift +++ b/kDriveCore/UI/UIConstants.swift @@ -129,7 +129,7 @@ public enum UIConstants { } private static func createLinkPreviewForFile(_ file: File, link: URL, completion: @escaping (LPLinkMetadata) -> Void) { - if ConvertedType.ignoreThumbnailTypes.contains(file.convertedType) || !file.hasThumbnail { + if ConvertedType.ignoreThumbnailTypes.contains(file.convertedType) || !file.supportedBy.contains(.thumbnail) { completion(createLinkMetadata(file: file, url: link, thumbnail: file.icon)) } else { file.getThumbnail { thumbnail, _ in From 6d912898fd1f77d4d75a1593c75d630d7463c44b Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 20 Dec 2023 09:18:44 +0100 Subject: [PATCH 21/62] fix: Only handle latest action Signed-off-by: Philippe Weidmann --- kDriveCore/Data/Cache/DriveFileManager+Listing.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift index 982469fba..94899c772 100644 --- a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift +++ b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift @@ -66,9 +66,13 @@ public extension DriveFileManager { func handleActions(_ actions: [FileAction], actionsFiles: [File], directory: File, using realm: Realm) { let mappedActionsFiles = Dictionary(grouping: actionsFiles, by: \.id) + var alreadyHandledActionIds = Set() - for fileAction in actions { - guard let actionFile = mappedActionsFiles[fileAction.fileId]?.first else { continue } + // We reverse actions to handle the most recent one first + for fileAction in actions.reversed() { + guard let actionFile = mappedActionsFiles[fileAction.fileId]?.first, + !alreadyHandledActionIds.contains(fileAction.fileId) else { continue } + alreadyHandledActionIds.insert(fileAction.fileId) switch fileAction.action { case .fileDelete, .fileTrash: From b4b2637daa2710fa454052a9800faac15098f140 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 20 Dec 2023 09:21:19 +0100 Subject: [PATCH 22/62] fix: Use same code for rename and update actions Signed-off-by: Philippe Weidmann --- .../Data/Cache/DriveFileManager+Listing.swift | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift index 94899c772..5b18c1b9e 100644 --- a/kDriveCore/Data/Cache/DriveFileManager+Listing.swift +++ b/kDriveCore/Data/Cache/DriveFileManager+Listing.swift @@ -83,15 +83,6 @@ public extension DriveFileManager { let oldParent = movedOutFile.parent else { continue } oldParent.children.remove(movedOutFile) - - case .fileRename: - guard let oldFile: File = realm.getObject(id: fileAction.fileId) else { continue } - try? renameCachedFile(updatedFile: actionFile, oldFile: oldFile) - // If the file is a folder we have to copy the old attributes which are not returned by the API - keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) - realm.add(actionFile, update: .modified) - actionFile.applyLastModifiedDateToLocalFile() - case .fileMoveIn, .fileRestore, .fileCreate: keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) realm.add(actionFile, update: .modified) @@ -102,18 +93,22 @@ public extension DriveFileManager { } directory.children.insert(actionFile) - case .fileFavoriteCreate, .fileFavoriteRemove, .fileUpdate, .fileShareCreate, .fileShareUpdate, .fileShareDelete, - .collaborativeFolderCreate, .collaborativeFolderUpdate, .collaborativeFolderDelete, .fileColorUpdate, - .fileColorDelete: - guard actionFile.isTrashed else { - removeFileInDatabase(fileId: fileAction.fileId, cascade: true, withTransaction: false, using: realm) - continue + case .fileRename, + .fileFavoriteCreate, .fileUpdate, .fileFavoriteRemove, + .fileShareCreate, .fileShareUpdate, .fileShareDelete, + .collaborativeFolderCreate, .collaborativeFolderUpdate, .collaborativeFolderDelete, + .fileColorUpdate, .fileColorDelete, + .fileCategorize, .fileUncategorize: + + if let oldFile: File = realm.getObject(id: fileAction.fileId), + oldFile.name != actionFile.name { + try? renameCachedFile(updatedFile: actionFile, oldFile: oldFile) } keepCacheAttributesForFile(newFile: actionFile, keepProperties: [.standard, .extras], using: realm) realm.add(actionFile, update: .modified) directory.children.insert(actionFile) - + actionFile.applyLastModifiedDateToLocalFile() default: break } From fab04afef85f5e494f832cb33ed98967f101a80d Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 20 Dec 2023 09:28:01 +0100 Subject: [PATCH 23/62] fix: Observe supportedBy instead of hasThumbnail Signed-off-by: Philippe Weidmann --- kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift index 8605d2e25..556769941 100644 --- a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift +++ b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift @@ -96,7 +96,7 @@ class PhotoListViewModel: FileListViewModel { override func updateRealmObservation() { realmObservationToken?.invalidate() - realmObservationToken = files.observe(keyPaths: ["lastModifiedAt", "hasThumbnail"], on: .main) { [weak self] change in + realmObservationToken = files.observe(keyPaths: ["lastModifiedAt", "supportedBy"], on: .main) { [weak self] change in guard let self else { return } From a4c47ad6e9780b59158b0e3454d8f5638811fa18 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Fri, 3 Nov 2023 14:37:13 +0100 Subject: [PATCH 24/62] fix: Use AppLaunchCounter Signed-off-by: Philippe Weidmann --- .package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.package.resolved b/.package.resolved index aedd6ebc2..2b99b5499 100644 --- a/.package.resolved +++ b/.package.resolved @@ -87,7 +87,7 @@ "location" : "https://github.com/Infomaniak/ios-core", "state" : { "branch" : "cursored-api-response", - "revision" : "60313af3e40796b05dea4faf99df5bee15ec6e24" + "revision" : "782d650b330c0e92ac235646d96691c3993161dc" } }, { From 9a5fa2a46249e97f46fd65ff81e67bbcfdbb6303 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 5 Dec 2023 11:43:16 +0100 Subject: [PATCH 25/62] fix: Correctly add InfomaniakCoreUI Signed-off-by: Philippe Weidmann --- .package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.package.resolved b/.package.resolved index 2b99b5499..aedd6ebc2 100644 --- a/.package.resolved +++ b/.package.resolved @@ -87,7 +87,7 @@ "location" : "https://github.com/Infomaniak/ios-core", "state" : { "branch" : "cursored-api-response", - "revision" : "782d650b330c0e92ac235646d96691c3993161dc" + "revision" : "60313af3e40796b05dea4faf99df5bee15ec6e24" } }, { From 928234d1642215a7c91dedfbe3ac98e2c3a703ae Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Mon, 18 Sep 2023 14:47:26 +0200 Subject: [PATCH 26/62] feat: New root menu Signed-off-by: Philippe Weidmann --- .../Files/RootMenuViewController.swift | 165 ++++++++++++++++++ .../UI/Controller/MainTabViewController.swift | 28 +-- kDrive/UI/View/Menu/RootMenuCell.swift | 163 +++++++++++++++++ 3 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 kDrive/UI/Controller/Files/RootMenuViewController.swift create mode 100644 kDrive/UI/View/Menu/RootMenuCell.swift diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift new file mode 100644 index 000000000..6448c5f47 --- /dev/null +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -0,0 +1,165 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import InfomaniakCoreUI +import kDriveCore +import kDriveResources +import RealmSwift +import UIKit + +class RootMenuViewController: UICollectionViewController { + private struct RootMenuItem: Equatable { + var id: Int { + return destinationFile.id + } + let name: String + let image: UIImage + let destinationFile: File + } + + private static let baseItems: [RootMenuItem] = [RootMenuItem(name: KDriveResourcesStrings.Localizable.favoritesTitle, + image: KDriveResourcesAsset.favorite.image, + destinationFile: DriveFileManager.favoriteRootFile), + RootMenuItem(name: KDriveResourcesStrings.Localizable.lastEditsTitle, + image: KDriveResourcesAsset.clock.image, + destinationFile: DriveFileManager.lastModificationsRootFile), + RootMenuItem(name: KDriveResourcesStrings.Localizable.sharedWithMeTitle, + image: KDriveResourcesAsset.folderSelect2.image, + destinationFile: DriveFileManager.sharedWithMeRootFile), + RootMenuItem(name: KDriveResourcesStrings.Localizable.mySharesTitle, + image: KDriveResourcesAsset.folderSelect.image, + destinationFile: DriveFileManager.mySharedRootFile), + RootMenuItem(name: KDriveResourcesStrings.Localizable.offlineFileTitle, + image: KDriveResourcesAsset.availableOffline.image, + destinationFile: DriveFileManager.offlineRoot), + RootMenuItem(name: KDriveResourcesStrings.Localizable.trashTitle, + image: KDriveResourcesAsset.delete.image, + destinationFile: DriveFileManager.trashRootFile)] + + private let driveFileManager: DriveFileManager + private var rootChildrenObservationToken: NotificationToken? + private var rootViewChildren: [File]? + + private var items: [RootMenuItem] { + let userRootFolders = rootViewChildren?.compactMap { + RootMenuItem(name: $0.name, image: $0.icon, destinationFile: $0) + } ?? [] + + return userRootFolders + RootMenuViewController.baseItems + } + + init(driveFileManager: DriveFileManager) { + self.driveFileManager = driveFileManager + super.init(collectionViewLayout: RootMenuViewController.createListLayout()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = driveFileManager.drive.name + navigationController?.navigationBar.prefersLargeTitles = true + collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color + collectionView.register(RootMenuCell.self, forCellWithReuseIdentifier: RootMenuCell.identifier) + + let rootChildren = driveFileManager.getRealm() + .object(ofType: File.self, forPrimaryKey: DriveFileManager.constants.rootID)?.children + rootChildrenObservationToken = rootChildren?.observe { [weak self] changes in + guard let self else { return } + switch changes { + case .initial(let children): + rootViewChildren = Array(children) + collectionView.reloadData() + case .update(let children, _, _,_): + rootViewChildren = Array(children) + // TODO: Maybe use insert/remove instead of reload + collectionView.reloadData() + case .error: + break + } + } + } + static func createListLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(60)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(60)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, + subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + + let layout = UICollectionViewCompositionalLayout(section: section) + return layout + } + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + override func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + override func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let rootMenuCell = collectionView.dequeueReusableCell( + withReuseIdentifier: RootMenuCell.identifier, + for: indexPath + ) as? RootMenuCell else { + fatalError("Failed to dequeue cell") + } + + let menuItem = items[indexPath.row] + rootMenuCell.configure(title: menuItem.name, icon: menuItem.image) + rootMenuCell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == items.count - 1) + return rootMenuCell + } + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let selectedRootFile = items[indexPath.row].destinationFile + + let destinationViewModel: FileListViewModel + switch selectedRootFile.id { + case DriveFileManager.favoriteRootFile.id: + destinationViewModel = FavoritesViewModel(driveFileManager: driveFileManager) + case DriveFileManager.lastModificationsRootFile.id: + destinationViewModel = LastModificationsViewModel(driveFileManager: driveFileManager) + case DriveFileManager.sharedWithMeRootFile.id: + navigationController?.pushViewController(SharedDrivesViewController.instantiate(), animated: true) + return + case DriveFileManager.offlineRoot.id: + destinationViewModel = OfflineFilesViewModel(driveFileManager: driveFileManager) + case DriveFileManager.trashRootFile.id: + destinationViewModel = TrashListViewModel(driveFileManager: driveFileManager) + default: + destinationViewModel = ConcreteFileListViewModel( + driveFileManager: driveFileManager, + currentDirectory: selectedRootFile + ) + } + + let destinationViewController = FileListViewController.instantiate(viewModel: destinationViewModel) + navigationController?.pushViewController(destinationViewController, animated: true) + } +} diff --git a/kDrive/UI/Controller/MainTabViewController.swift b/kDrive/UI/Controller/MainTabViewController.swift index e74857a7b..805105259 100644 --- a/kDrive/UI/Controller/MainTabViewController.swift +++ b/kDrive/UI/Controller/MainTabViewController.swift @@ -38,15 +38,9 @@ class MainTabViewController: UITabBarController, Restorable { self.driveFileManager = driveFileManager var rootViewControllers = [UIViewController]() rootViewControllers.append(Self.initHomeViewController(driveFileManager: driveFileManager)) - rootViewControllers.append(Self.initRootViewController(with: ConcreteFileListViewModel( - driveFileManager: driveFileManager, - currentDirectory: nil - ))) + rootViewControllers.append(Self.initRootMenuViewController(driveFileManager: driveFileManager)) rootViewControllers.append(UIViewController()) - rootViewControllers.append(Self.initRootViewController(with: FavoritesViewModel( - driveFileManager: driveFileManager, - currentDirectory: nil - ))) + rootViewControllers.append(Self.initPhotoListViewController(with: PhotoListViewModel(driveFileManager: driveFileManager))) rootViewControllers.append(Self.initMenuViewController(driveFileManager: driveFileManager)) super.init(nibName: nil, bundle: nil) viewControllers = rootViewControllers @@ -108,6 +102,16 @@ class MainTabViewController: UITabBarController, Restorable { return navigationViewController } + private static func initRootMenuViewController(driveFileManager: DriveFileManager) -> UIViewController { + let homeViewController = RootMenuViewController(driveFileManager: driveFileManager) + let navigationViewController = TitleSizeAdjustingNavigationController(rootViewController: homeViewController) + navigationViewController.navigationBar.prefersLargeTitles = true + navigationViewController.tabBarItem.accessibilityLabel = KDriveResourcesStrings.Localizable.homeTitle + navigationViewController.tabBarItem.image = KDriveResourcesAsset.folder.image + navigationViewController.tabBarItem.selectedImage = KDriveResourcesAsset.folderFilledTab.image + return navigationViewController + } + private static func initMenuViewController(driveFileManager: DriveFileManager) -> UIViewController { let menuViewController = MenuViewController(driveFileManager: driveFileManager) let navigationViewController = TitleSizeAdjustingNavigationController(rootViewController: menuViewController) @@ -119,10 +123,10 @@ class MainTabViewController: UITabBarController, Restorable { return navigationViewController } - private static func initRootViewController(with viewModel: FileListViewModel) -> UIViewController { - let fileListViewController = FileListViewController.instantiate(viewModel: viewModel) - let navigationViewController = TitleSizeAdjustingNavigationController(rootViewController: fileListViewController) - navigationViewController.restorationIdentifier = String(describing: type(of: viewModel)) + private static func initPhotoListViewController(with viewModel: FileListViewModel) -> UIViewController { + let photoListViewController = PhotoListViewController.instantiate(viewModel: viewModel) + let navigationViewController = TitleSizeAdjustingNavigationController(rootViewController: photoListViewController) + navigationViewController.restorationIdentifier = String(describing: PhotoListViewController.self) navigationViewController.navigationBar.prefersLargeTitles = true navigationViewController.tabBarItem.accessibilityLabel = viewModel.title navigationViewController.tabBarItem.image = viewModel.configuration.tabBarIcon.image diff --git a/kDrive/UI/View/Menu/RootMenuCell.swift b/kDrive/UI/View/Menu/RootMenuCell.swift new file mode 100644 index 000000000..04b18b47a --- /dev/null +++ b/kDrive/UI/View/Menu/RootMenuCell.swift @@ -0,0 +1,163 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2023 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import InfomaniakCoreUI +import kDriveCore +import kDriveResources +import RealmSwift +import UIKit + +class RootMenuCell: UICollectionViewCell { + static let identifier = String(describing: RootMenuCell.self) + + private var topConstraint: NSLayoutConstraint? + private var bottomConstraint: NSLayoutConstraint? + + private let separatorView: UIView = { + let view = UIView() + view.backgroundColor = KDriveResourcesAsset.separatorColor.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let contentInsetView: UIView = { + let view = UIView() + view.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private let iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.tintColor = KDriveResourcesAsset.iconColor.color + return imageView + }() + + private let titleLabel: UILabel = { + let label = IKLabel() + label.style = .subtitle2 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let chevronImageView: UIImageView = { + let imageView = UIImageView(image: KDriveResourcesAsset.chevronRight.image) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.tintColor = KDriveResourcesAsset.secondaryTextColor.color + return imageView + }() + + override var isSelected: Bool { + didSet { + contentInsetView.backgroundColor = isSelected ? + InfomaniakCoreAsset.backgroundCardViewSelected.color : InfomaniakCoreAsset.backgroundCardView.color + } + } + + override var isHighlighted: Bool { + didSet { + contentInsetView.backgroundColor = isHighlighted ? + InfomaniakCoreAsset.backgroundCardViewSelected.color : InfomaniakCoreAsset.backgroundCardView.color + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + contentView.addSubview(contentInsetView) + contentInsetView.addSubview(iconImageView) + contentInsetView.addSubview(titleLabel) + contentInsetView.addSubview(chevronImageView) + contentInsetView.addSubview(separatorView) + + topConstraint = contentInsetView.topAnchor.constraint(equalTo: contentView.topAnchor) + bottomConstraint = contentInsetView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + + NSLayoutConstraint.activate([ + separatorView.leadingAnchor.constraint(equalTo: contentInsetView.leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: contentInsetView.trailingAnchor), + separatorView.bottomAnchor.constraint(equalTo: contentInsetView.bottomAnchor), + separatorView.heightAnchor.constraint(equalToConstant: 1), + + contentInsetView.heightAnchor.constraint(equalToConstant: 60), + contentInsetView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), + contentInsetView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24), + contentInsetView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24), + topConstraint!, + bottomConstraint!, + + iconImageView.leadingAnchor.constraint(equalTo: contentInsetView.leadingAnchor, constant: 16), + iconImageView.centerYAnchor.constraint(equalTo: contentInsetView.centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 26), + iconImageView.heightAnchor.constraint(equalToConstant: 26), + + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16), + titleLabel.centerYAnchor.constraint(equalTo: contentInsetView.centerYAnchor), + + chevronImageView.trailingAnchor.constraint(equalTo: contentInsetView.trailingAnchor, constant: -24), + chevronImageView.centerYAnchor.constraint(equalTo: contentInsetView.centerYAnchor), + chevronImageView.widthAnchor.constraint(equalToConstant: 12), + chevronImageView.heightAnchor.constraint(equalToConstant: 14) + ]) + } + + func configure(title: String, icon: UIImage) { + titleLabel.text = title + iconImageView.image = icon + } + + open func initWithPositionAndShadow(isFirst: Bool = false, isLast: Bool = false, elevation: Double = 0, radius: CGFloat = 6) { + if isLast && isFirst { + separatorView.isHidden = true + topConstraint?.constant = 8 + bottomConstraint?.constant = 8 + contentInsetView.roundCorners( + corners: [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner], + radius: radius + ) + } else if isFirst { + separatorView.isHidden = false + topConstraint?.constant = 8 + bottomConstraint?.constant = 0 + contentInsetView.roundCorners(corners: [.layerMaxXMinYCorner, .layerMinXMinYCorner], radius: radius) + } else if isLast { + separatorView.isHidden = true + topConstraint?.constant = 0 + bottomConstraint?.constant = 8 + contentInsetView.roundCorners(corners: [.layerMaxXMaxYCorner, .layerMinXMaxYCorner], radius: radius) + } else { + separatorView.isHidden = false + topConstraint?.constant = 0 + bottomConstraint?.constant = 0 + contentInsetView.roundCorners( + corners: [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMinXMinYCorner], + radius: 0 + ) + } + contentInsetView.addShadow(elevation: elevation) + } +} From 9826c1c6529af4c558713dcd558331544b941887 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 3 Oct 2023 15:08:39 +0200 Subject: [PATCH 27/62] feat: Diffable datasource RootMenuViewController Signed-off-by: Philippe Weidmann --- .../Files/RootMenuViewController.swift | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift index 6448c5f47..5339fa717 100644 --- a/kDrive/UI/Controller/Files/RootMenuViewController.swift +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -23,13 +23,29 @@ import RealmSwift import UIKit class RootMenuViewController: UICollectionViewController { - private struct RootMenuItem: Equatable { + private typealias MenuDataSource = UICollectionViewDiffableDataSource + private typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot + + private enum RootMenuSection { + case main + } + + private struct RootMenuItem: Equatable, Hashable { var id: Int { return destinationFile.id } + let name: String let image: UIImage let destinationFile: File + var isFirst = false + var isLast = false + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(isFirst) + hasher.combine(isLast) + } } private static let baseItems: [RootMenuItem] = [RootMenuItem(name: KDriveResourcesStrings.Localizable.favoritesTitle, @@ -54,13 +70,23 @@ class RootMenuViewController: UICollectionViewController { private let driveFileManager: DriveFileManager private var rootChildrenObservationToken: NotificationToken? private var rootViewChildren: [File]? + private var dataSource: MenuDataSource? - private var items: [RootMenuItem] { + private var itemsSnapshot: DataSourceSnapshot { let userRootFolders = rootViewChildren?.compactMap { RootMenuItem(name: $0.name, image: $0.icon, destinationFile: $0) } ?? [] - return userRootFolders + RootMenuViewController.baseItems + var menuItems = userRootFolders + RootMenuViewController.baseItems + if !menuItems.isEmpty { + menuItems[0].isFirst = true + menuItems[menuItems.count - 1].isLast = true + } + + var snapshot = DataSourceSnapshot() + snapshot.appendSections([RootMenuSection.main]) + snapshot.appendItems(menuItems) + return snapshot } init(driveFileManager: DriveFileManager) { @@ -80,6 +106,20 @@ class RootMenuViewController: UICollectionViewController { collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color collectionView.register(RootMenuCell.self, forCellWithReuseIdentifier: RootMenuCell.identifier) + dataSource = MenuDataSource(collectionView: collectionView) { collectionView, indexPath, menuItem -> RootMenuCell? in + guard let rootMenuCell = collectionView.dequeueReusableCell( + withReuseIdentifier: RootMenuCell.identifier, + for: indexPath + ) as? RootMenuCell else { + fatalError("Failed to dequeue cell") + } + + rootMenuCell.configure(title: menuItem.name, icon: menuItem.image) + rootMenuCell.initWithPositionAndShadow(isFirst: menuItem.isFirst, isLast: menuItem.isLast) + return rootMenuCell + } + dataSource?.apply(itemsSnapshot, animatingDifferences: false) + let rootChildren = driveFileManager.getRealm() .object(ofType: File.self, forPrimaryKey: DriveFileManager.constants.rootID)?.children rootChildrenObservationToken = rootChildren?.observe { [weak self] changes in @@ -87,16 +127,16 @@ class RootMenuViewController: UICollectionViewController { switch changes { case .initial(let children): rootViewChildren = Array(children) - collectionView.reloadData() - case .update(let children, _, _,_): + dataSource?.apply(itemsSnapshot, animatingDifferences: false) + case .update(let children, _, _, _): rootViewChildren = Array(children) - // TODO: Maybe use insert/remove instead of reload - collectionView.reloadData() + dataSource?.apply(itemsSnapshot, animatingDifferences: true) case .error: break } } } + static func createListLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(60)) @@ -113,31 +153,8 @@ class RootMenuViewController: UICollectionViewController { return layout } - override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return items.count - } - - override func numberOfSections(in collectionView: UICollectionView) -> Int { - return 1 - } - - override func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let rootMenuCell = collectionView.dequeueReusableCell( - withReuseIdentifier: RootMenuCell.identifier, - for: indexPath - ) as? RootMenuCell else { - fatalError("Failed to dequeue cell") - } - - let menuItem = items[indexPath.row] - rootMenuCell.configure(title: menuItem.name, icon: menuItem.image) - rootMenuCell.initWithPositionAndShadow(isFirst: indexPath.row == 0, isLast: indexPath.row == items.count - 1) - return rootMenuCell - } - override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - let selectedRootFile = items[indexPath.row].destinationFile + guard let selectedRootFile = dataSource?.itemIdentifier(for: indexPath)?.destinationFile else { return } let destinationViewModel: FileListViewModel switch selectedRootFile.id { From 56eb8e7fd26612b89f7472409ad8ec1fdb5f54a7 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 4 Oct 2023 13:56:21 +0200 Subject: [PATCH 28/62] feat: CustomLargeTitleCollectionViewController Signed-off-by: Philippe Weidmann --- .../Files/RootMenuViewController.swift | 79 +++++++++++++++---- .../Controller/Home/HomeViewController.swift | 60 ++------------ .../HomeLargeTitleHeaderView.swift | 12 +++ .../Header view/HomeLargeTitleHeaderView.xib | 14 ++-- ...omLargeTitleCollectionViewController.swift | 66 ++++++++++++++++ 5 files changed, 153 insertions(+), 78 deletions(-) create mode 100644 kDrive/Utils/CustomLargeTitleCollectionViewController.swift diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift index 5339fa717..7c7c427c9 100644 --- a/kDrive/UI/Controller/Files/RootMenuViewController.swift +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -17,12 +17,13 @@ */ import InfomaniakCoreUI +import InfomaniakDI import kDriveCore import kDriveResources import RealmSwift import UIKit -class RootMenuViewController: UICollectionViewController { +class RootMenuViewController: CustomLargeTitleCollectionViewController { private typealias MenuDataSource = UICollectionViewDiffableDataSource private typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot @@ -67,10 +68,13 @@ class RootMenuViewController: UICollectionViewController { image: KDriveResourcesAsset.delete.image, destinationFile: DriveFileManager.trashRootFile)] - private let driveFileManager: DriveFileManager + @LazyInjectService private var accountManager: AccountManageable + + let driveFileManager: DriveFileManager private var rootChildrenObservationToken: NotificationToken? private var rootViewChildren: [File]? private var dataSource: MenuDataSource? + private let refreshControl = UIRefreshControl() private var itemsSnapshot: DataSourceSnapshot { let userRootFolders = rootViewChildren?.compactMap { @@ -102,23 +106,15 @@ class RootMenuViewController: UICollectionViewController { override func viewDidLoad() { super.viewDidLoad() navigationItem.title = driveFileManager.drive.name - navigationController?.navigationBar.prefersLargeTitles = true + collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color - collectionView.register(RootMenuCell.self, forCellWithReuseIdentifier: RootMenuCell.identifier) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + collectionView.refreshControl = refreshControl - dataSource = MenuDataSource(collectionView: collectionView) { collectionView, indexPath, menuItem -> RootMenuCell? in - guard let rootMenuCell = collectionView.dequeueReusableCell( - withReuseIdentifier: RootMenuCell.identifier, - for: indexPath - ) as? RootMenuCell else { - fatalError("Failed to dequeue cell") - } + collectionView.register(RootMenuCell.self, forCellWithReuseIdentifier: RootMenuCell.identifier) + collectionView.register(supplementaryView: HomeLargeTitleHeaderView.self, forSupplementaryViewOfKind: .header) - rootMenuCell.configure(title: menuItem.name, icon: menuItem.image) - rootMenuCell.initWithPositionAndShadow(isFirst: menuItem.isFirst, isLast: menuItem.isLast) - return rootMenuCell - } - dataSource?.apply(itemsSnapshot, animatingDifferences: false) + configureDataSource() let rootChildren = driveFileManager.getRealm() .object(ofType: File.self, forPrimaryKey: DriveFileManager.constants.rootID)?.children @@ -137,6 +133,48 @@ class RootMenuViewController: UICollectionViewController { } } + func configureDataSource() { + dataSource = MenuDataSource(collectionView: collectionView) { collectionView, indexPath, menuItem -> RootMenuCell? in + guard let rootMenuCell = collectionView.dequeueReusableCell( + withReuseIdentifier: RootMenuCell.identifier, + for: indexPath + ) as? RootMenuCell else { + fatalError("Failed to dequeue cell") + } + + rootMenuCell.configure(title: menuItem.name, icon: menuItem.image) + rootMenuCell.initWithPositionAndShadow(isFirst: menuItem.isFirst, isLast: menuItem.isLast) + return rootMenuCell + } + + dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in + guard let self else { return UICollectionReusableView() } + + let homeLargeTitleHeaderView = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + view: HomeLargeTitleHeaderView.self, + for: indexPath + ) + homeLargeTitleHeaderView.isEnabled = accountManager.drives.count > 1 + homeLargeTitleHeaderView.text = driveFileManager.drive.name + homeLargeTitleHeaderView.titleButtonPressedHandler = { [weak self] _ in + guard let self else { return } + let drives = accountManager.drives + let floatingPanelViewController = FloatingPanelSelectOptionViewController.instantiatePanel( + options: drives, + selectedOption: driveFileManager.drive, + headerTitle: KDriveResourcesStrings.Localizable.buttonSwitchDrive, + delegate: nil + ) + present(floatingPanelViewController, animated: true) + } + headerViewHeight = homeLargeTitleHeaderView.frame.height + return homeLargeTitleHeaderView + } + + dataSource?.apply(itemsSnapshot, animatingDifferences: false) + } + static func createListLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(60)) @@ -147,7 +185,16 @@ class RootMenuViewController: UICollectionViewController { let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(40)) + let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( + layoutSize: headerSize, + elementKind: UICollectionView.elementKindSectionHeader, alignment: .top + ) + sectionHeader.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24) + let section = NSCollectionLayoutSection(group: group) + section.boundarySupplementaryItems = [sectionHeader] let layout = UICollectionViewCompositionalLayout(section: section) return layout diff --git a/kDrive/UI/Controller/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift index 33b138656..07a1699c2 100644 --- a/kDrive/UI/Controller/Home/HomeViewController.swift +++ b/kDrive/UI/Controller/Home/HomeViewController.swift @@ -24,7 +24,7 @@ import kDriveCore import kDriveResources import UIKit -class HomeViewController: UICollectionViewController, UpdateAccountDelegate, TopScrollable, +class HomeViewController: CustomLargeTitleCollectionViewController, UpdateAccountDelegate, TopScrollable, SelectSwitchDriveDelegate { private static let loadingCellCount = 12 @@ -157,10 +157,6 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top } } - private var navbarHeight: CGFloat { - return navigationController?.navigationBar.frame.height ?? 0 - } - private var floatingPanelViewController: DriveFloatingPanelController? private var fileInformationsViewController: FileActionsFloatingPanelViewController! private lazy var filePresenter = FilePresenter(viewController: self) @@ -183,7 +179,7 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top private var showInsufficientStorage = true private var filesObserver: ObservationToken? - private var refreshControl = UIRefreshControl() + private let refreshControl = UIRefreshControl() init(driveFileManager: DriveFileManager) { self.driveFileManager = driveFileManager @@ -197,8 +193,8 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top override func viewDidLoad() { super.viewDidLoad() + navigationItem.title = driveFileManager.drive.name - collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color collectionView.register(supplementaryView: HomeRecentFilesHeaderView.self, forSupplementaryViewOfKind: .header) collectionView.register(supplementaryView: HomeLargeTitleHeaderView.self, forSupplementaryViewOfKind: .header) collectionView.register(cellView: HomeRecentFilesSelectorCollectionViewCell.self) @@ -232,34 +228,11 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top setSelectedHomeIndex(UserDefaults.shared.selectedHomeIndex) } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationItem.largeTitleDisplayMode = .never - navigationController?.navigationBar.isTranslucent = true - navigationController?.navigationBar.shadowImage = UIImage() - navigationController?.navigationBar.setBackgroundImage(nil, for: .default) - navigationController?.navigationBar.barTintColor = KDriveResourcesAsset.backgroundColor.color - navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: KDriveResourcesAsset.titleColor.color] - updateNavbarAppearance() - } - override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - updateNavbarAppearance() MatomoUtils.track(view: ["Home"]) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - navigationController?.navigationBar.shadowImage = nil - navigationController?.navigationBar.setBackgroundImage(nil, for: .default) - navigationController?.navigationBar.barTintColor = nil - navigationController?.navigationBar.titleTextAttributes = nil - navigationController?.navigationBar.alpha = 1 - navigationController?.navigationBar.isUserInteractionEnabled = true - navigationController?.navigationBar.layoutIfNeeded() - } - func observeFileUpdated() { guard driveFileManager != nil else { return } filesObserver?.cancel() @@ -415,24 +388,6 @@ class HomeViewController: UICollectionViewController, UpdateAccountDelegate, Top } } - private func updateNavbarAppearance() { - let scrollOffset = collectionView.contentOffset.y - guard let navigationBar = navigationController?.navigationBar else { - return - } - - if view.window?.windowScene?.interfaceOrientation.isPortrait ?? true { - navigationItem.title = driveFileManager?.drive.name ?? "" - navigationBar.alpha = min(1, max(0, (scrollOffset + collectionView.contentInset.top) / navbarHeight)) - navigationBar.isUserInteractionEnabled = navigationBar.alpha > 0.5 - } else { - navigationBar.isUserInteractionEnabled = false - navigationItem.title = "" - navigationBar.alpha = 0 - } - navigationBar.layoutIfNeeded() - } - private func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout { [weak self] section, layoutEnvironment in guard let self else { return nil } @@ -636,10 +591,7 @@ extension HomeViewController { for: indexPath ) driveHeaderView.isEnabled = accountManager.drives.count > 1 - UIView.performWithoutAnimation { - driveHeaderView.titleButton.setTitle(driveFileManager.drive.name, for: .normal) - driveHeaderView.titleButton.layoutIfNeeded() - } + driveHeaderView.text = driveFileManager.drive.name driveHeaderView.titleButtonPressedHandler = { [weak self] _ in guard let self else { return } let drives = accountManager.drives @@ -651,6 +603,7 @@ extension HomeViewController { ) present(floatingPanelViewController, animated: true) } + headerViewHeight = driveHeaderView.frame.height return driveHeaderView case .recentFiles: let headerView = collectionView.dequeueReusableSupplementaryView( @@ -689,9 +642,6 @@ extension HomeViewController { // MARK: - UICollectionViewDelegate extension HomeViewController { - override func scrollViewDidScroll(_ scrollView: UIScrollView) { - updateNavbarAppearance() - } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { switch HomeSection.allCases[indexPath.section] { diff --git a/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.swift b/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.swift index 32f18207b..d99eca119 100644 --- a/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.swift +++ b/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.swift @@ -24,6 +24,18 @@ class HomeLargeTitleHeaderView: UICollectionReusableView { @IBOutlet weak var titleButton: IKButton! var titleButtonPressedHandler: ((UIButton) -> Void)? + var text: String? { + get { + titleButton.titleLabel?.text + } + set { + UIView.performWithoutAnimation { + titleButton.setTitle(newValue, for: .normal) + titleButton.layoutIfNeeded() + } + } + } + var isEnabled = true { didSet { chevronButton.isHidden = !isEnabled diff --git a/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.xib b/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.xib index 2c4eaa540..0f8165839 100644 --- a/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.xib +++ b/kDrive/UI/View/Header view/HomeLargeTitleHeaderView.xib @@ -1,9 +1,9 @@ - + - + @@ -16,10 +16,10 @@ - +