From 3b54164a5a42c538a2abc83dd77d3dce3c919c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 26 Jul 2024 11:21:57 +0200 Subject: [PATCH 001/129] chore: WIP endpoints --- kDriveCore/Data/Api/Endpoint.swift | 59 +++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index dd93b038a..1057f552c 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -626,16 +626,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/files/search", queryItems: queryItems) } - // MARK: Share link - - static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/links") - } - - static func shareLink(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/link") - } - // MARK: Trash static func trash(drive: AbstractDrive) -> Endpoint { @@ -737,4 +727,53 @@ public extension Endpoint { static func sendUserInvitation(drive: AbstractDrive, id: Int) -> Endpoint { return .userInvitation(drive: drive, id: id).appending(path: "/send") } + + // MARK: Share Links + + static var shareUrlV2: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/2/app") + } + + static var shareUrlV3: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/3/app") + } + + static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/links") + } + + static func shareLink(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/link") + } + + // Share link info + // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" + var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") + } + + // Share link file + // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" + func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + // Share link file children + // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { + func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} + + val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" + return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" + + // Share link file thumbnail + // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { + + // Share mink file preview + // fun getShareLinkFilePreview(driveId: Int, linkUvid: String, file: File): String { + + // Download share link file + // fun downloadShareLinkFile(driveld: Int, linkUvid: String, file: File): String { + + // Share link file + // private fun shareLinkFile(driveld: Int, linkUvid: String, file: File): String { } From 20466806087eb010f5236c45ecb05dc966a602e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 16 Aug 2024 09:37:14 +0200 Subject: [PATCH 002/129] feat: In memory realm for external share context --- .../DriveFileManager+Transactionable.swift | 5 +++++ .../Data/Cache/DriveFileManager/DriveFileManager.swift | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index 8133f6d0b..3b3a4f967 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -29,6 +29,9 @@ public enum DriveFileManagerContext { /// Dedicated dataset to store shared with me case sharedWithMe + /// Dedicated in memory dataset for a public share link + case publicShare(shareId: String) + func realmURL(driveId: Int, driveUserId: Int) -> URL { switch self { case .drive: @@ -37,6 +40,8 @@ public enum DriveFileManagerContext { return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-shared.realm") case .fileProvider: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId)-fp.realm") + case .publicShare: + fatalError("noop") } } } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index c7a25ee92..8c848e359 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -87,8 +87,17 @@ public final class DriveFileManager { /// Build a realm configuration for a specific Drive public static func configuration(context: DriveFileManagerContext, driveId: Int, driveUserId: Int) -> Realm.Configuration { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) + + let inMemoryIdentifier: String? + if case let .publicShare(identifier) = context { + inMemoryIdentifier = "inMemory:\(identifier)" + } else { + inMemoryIdentifier = nil + } + return Realm.Configuration( fileURL: realmURL, + inMemoryIdentifier: inMemoryIdentifier, schemaVersion: RealmSchemaVersion.drive, migrationBlock: { migration, oldSchemaVersion in let currentDriveSchemeVersion = RealmSchemaVersion.drive From 001219f50472edd0794fc0a3b23145e1172ced71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 16 Aug 2024 09:39:18 +0200 Subject: [PATCH 003/129] fix: Project builds --- kDriveCore/Data/Api/Endpoint.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 1057f552c..9230a7614 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -748,9 +748,9 @@ public extension Endpoint { // Share link info // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" - var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") - } +// var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { +// Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") +// } // Share link file // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" @@ -760,10 +760,10 @@ public extension Endpoint { // Share link file children // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { - func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} +// func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} - val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" - return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" +// val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" +// return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" // Share link file thumbnail // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { From 31eafba505772bcb795f54fd50c775b2c10780d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 19 Aug 2024 15:06:23 +0200 Subject: [PATCH 004/129] feat: Public share metadata WIP --- kDrive/AppDelegate.swift | 12 ++++++ kDrive/SceneDelegate.swift | 14 ++++--- kDrive/Utils/UniversalLinksHelper.swift | 44 +++++++++++++++++++- kDriveCore/Data/Api/DriveApiFetcher.swift | 47 ++++++++++++++++++++++ kDriveCore/Data/Api/Endpoint.swift | 6 +-- kDriveCore/Data/Cache/AccountManager.swift | 33 +++++++++++++++ 6 files changed, 145 insertions(+), 11 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index b61288e25..e366d0867 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,6 +103,18 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() + // swiftlint:disable force_try + Task { + try! await Task.sleep(nanoseconds:5_000_000_000) + print("coucou") + let somePublicShare = URL(string: "") + //await UIApplication.shared.open(somePublicShare!) // opens safari + + let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) + await UniversalLinksHelper.handlePath(components!.path) + } + + return true } diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index 6f23a8f33..317186adf 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -230,13 +230,15 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { Log.sceneDelegate("scene continue userActivity") - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL, - let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { - return - } + Task { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let incomingURL = userActivity.webpageURL, + let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { + return + } - UniversalLinksHelper.handlePath(components.path) + await UniversalLinksHelper.handlePath(components.path) + } } func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 50b68adf5..10158ff9d 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -35,26 +35,44 @@ enum UniversalLinksHelper { regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-9]+)$"#)!, displayMode: .file ) + + /// Matches a public share link + static let publicShareLink = Link( + regex: Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#)!, + displayMode: .file + ) + /// Matches a directory list link static let directoryLink = Link(regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+)$"#)!, displayMode: .file) + /// Matches a file preview link static let filePreview = Link( regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+/)?preview/[a-z]+/([0-9]+)$"#)!, displayMode: .file ) + /// Matches an office file link static let officeLink = Link(regex: Regex(pattern: #"^/app/office/([0-9]+)/([0-9]+)$"#)!, displayMode: .office) - static let all = [privateShareLink, directoryLink, filePreview, officeLink] + static let all = [privateShareLink, publicShareLink, directoryLink, filePreview, officeLink] } private enum DisplayMode { case office, file } - static func handlePath(_ path: String) -> Bool { + @discardableResult + static func handlePath(_ path: String) async -> Bool { DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") + // Public share link regex + let shareLink = Link.publicShareLink + let matches = shareLink.regex.matches(in: path) + if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode) { + return true + } + + // Common regex for link in Link.all { let matches = link.regex.matches(in: path) if processRegex(matches: matches, displayMode: link.displayMode) { @@ -66,6 +84,28 @@ enum UniversalLinksHelper { return false } + private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool { + @InjectService var accountManager: AccountManageable + + guard let firstMatch = matches.first, + let driveId = firstMatch[safe: 1], + let driveIdInt = Int(driveId), + let shareLinkUid = firstMatch[safe: 2] else { + return false + } + + // request metadata + guard let metadata = try? await PublicShareApiFetcher().getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + else { + return false + } + + // get file ID from metadata + let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) + openFile(id: metadata.file_id, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + return true + } + private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool { @InjectService var accountManager: AccountManageable diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 0d75d0e84..de0cc6ca8 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -52,6 +52,53 @@ public class AuthenticatedImageRequestModifier: ImageDownloadRequestModifier { } } +public struct PublicShareMetadata: Decodable { + public let url: URL + public let fileId: Int + public let right: String + // public let validUntil: Date? + // public let capabilities: Rights + +// public let createdBy: TimeInterval +// public let createdAt: TimeInterval +// public let updatedAt: TimeInterval +// public let accessBlocked: Bool + + enum CodingKeys: String, CodingKey { + case url + case fileId = "file_id" + case right + // case validUntil = "valid_until" + // case capabilities +// case createdBy = "created_by" +// case createdAt = "created_at" +// case updatedAt = "updated_at" +// case accessBlocked = "access_blocked" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + url = try container.decode(URL.self, forKey: .url) + fileId = try container.decode(Int.self, forKey: .fileId) + right = try container.decode(String.self, forKey: .right) + } +} + +public class PublicShareApiFetcher: ApiFetcher { + override public init() { + super.init() + } + + public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { + let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + let request = Session.default.request(shareLinkInfoUrl) + let metadata: PublicShareMetadata = try await perform(request: request) + print("metadata\(metadata)") + return metadata + } +} + public class DriveApiFetcher: ApiFetcher { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var tokenable: InfomaniakTokenable diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 9230a7614..6d7156c01 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -748,9 +748,9 @@ public extension Endpoint { // Share link info // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" -// var shareLinkInfo(driveId: Int, linkUuid: String) -> Endpoint { -// Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/init") -// } + static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { + Self.shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") + } // Share link file // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 5e780e683..f58e1603e 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -24,6 +24,19 @@ import InfomaniakLogin import RealmSwift import Sentry +// TODO: Delete +public class SomeRefreshTokenDelegate: RefreshTokenDelegate { + public init() {} + + public func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) { + print("noop") + } + + public func didFailRefreshToken(_ token: ApiToken) { + print("noop") + } +} + public protocol UpdateAccountDelegate: AnyObject { @MainActor func didUpdateCurrentAccountInformations(_ currentAccount: Account) } @@ -63,6 +76,10 @@ public protocol AccountManageable: AnyObject { func reloadTokensAndAccounts() func getDriveFileManager(for driveId: Int, userId: Int) -> DriveFileManager? func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager + + /// Create on the fly an "in memory" DriveFileManager for a specific share + func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager + func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -194,6 +211,22 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } + public func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager { + if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { + return inMemoryDriveFileManager + } + + // Big hack, refactor to allow for non authenticated requests + guard let someToken = apiFetchers.values.first?.currentToken else { + fatalError("probably no account availlable") + } + + let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) + let context = DriveFileManagerContext.publicShare(shareId: publicShareId) + let noopDrive = Drive() + return DriveFileManager(drive: noopDrive, apiFetcher: apiFetcher, context: context) + } + public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { let userDrives = driveInfosManager.getDrives(for: userId) From 98cffecda82fb2bae7a96df709c32349841e1078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 20 Aug 2024 14:23:55 +0200 Subject: [PATCH 005/129] feat(Rights): Updated to be compatible with public share --- .../DriveFileManagerConstants.swift | 2 +- kDriveCore/Data/Models/Rights.swift | 52 ++++++++++++++----- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift index 9ce515899..b65c3f6d2 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift @@ -28,7 +28,7 @@ public enum RealmSchemaVersion { static let upload: UInt64 = 21 /// Current version of the Drive Realm - static let drive: UInt64 = 11 + static let drive: UInt64 = 12 } public class DriveFileManagerConstants { diff --git a/kDriveCore/Data/Models/Rights.swift b/kDriveCore/Data/Models/Rights.swift index 6d599a289..322db9750 100644 --- a/kDriveCore/Data/Models/Rights.swift +++ b/kDriveCore/Data/Models/Rights.swift @@ -44,7 +44,8 @@ public class Rights: EmbeddedObject, Codable { /// Right to use and give team access @Persisted public var canUseTeam: Bool - // Directory capabilities + // MARK: Directory capabilities + /// Right to add new child directory @Persisted public var canCreateDirectory: Bool /// Right to add new child file @@ -56,6 +57,21 @@ public class Rights: EmbeddedObject, Codable { /// Right to use convert directory into dropbox @Persisted public var canBecomeDropbox: Bool + // MARK: Public share + + /// Can edit + @Persisted public var canEdit: Bool + /// Can see stats + @Persisted public var canSeeStats: Bool + /// Can see info + @Persisted public var canSeeInfo: Bool + /// Can download + @Persisted public var canDownload: Bool + /// Can comment + @Persisted public var canComment: Bool + /// Can request access + @Persisted public var canRequestAccess: Bool + enum CodingKeys: String, CodingKey { case canShow case canRead @@ -73,26 +89,38 @@ public class Rights: EmbeddedObject, Codable { case canUpload case canMoveInto case canBecomeDropbox + case canEdit + case canSeeStats + case canSeeInfo + case canDownload + case canComment + case canRequestAccess } public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - canShow = try container.decode(Bool.self, forKey: .canShow) - canRead = try container.decode(Bool.self, forKey: .canRead) - canWrite = try container.decode(Bool.self, forKey: .canWrite) - canShare = try container.decode(Bool.self, forKey: .canShare) - canLeave = try container.decode(Bool.self, forKey: .canLeave) - canDelete = try container.decode(Bool.self, forKey: .canDelete) - canRename = try container.decode(Bool.self, forKey: .canRename) - canMove = try container.decode(Bool.self, forKey: .canMove) - canBecomeSharelink = try container.decode(Bool.self, forKey: .canBecomeSharelink) - canUseFavorite = try container.decode(Bool.self, forKey: .canUseFavorite) - canUseTeam = try container.decode(Bool.self, forKey: .canUseTeam) + canShow = try container.decodeIfPresent(Bool.self, forKey: .canShow) ?? true + canRead = try container.decodeIfPresent(Bool.self, forKey: .canRead) ?? true + canWrite = try container.decodeIfPresent(Bool.self, forKey: .canWrite) ?? false + canShare = try container.decodeIfPresent(Bool.self, forKey: .canShare) ?? false + canLeave = try container.decodeIfPresent(Bool.self, forKey: .canLeave) ?? false + canDelete = try container.decodeIfPresent(Bool.self, forKey: .canDelete) ?? false + canRename = try container.decodeIfPresent(Bool.self, forKey: .canRename) ?? false + canMove = try container.decodeIfPresent(Bool.self, forKey: .canMove) ?? false + canBecomeSharelink = try container.decodeIfPresent(Bool.self, forKey: .canBecomeSharelink) ?? false + canUseFavorite = try container.decodeIfPresent(Bool.self, forKey: .canUseFavorite) ?? false + canUseTeam = try container.decodeIfPresent(Bool.self, forKey: .canUseTeam) ?? false canCreateDirectory = try container.decodeIfPresent(Bool.self, forKey: .canCreateDirectory) ?? false canCreateFile = try container.decodeIfPresent(Bool.self, forKey: .canCreateFile) ?? false canUpload = try container.decodeIfPresent(Bool.self, forKey: .canUpload) ?? false canMoveInto = try container.decodeIfPresent(Bool.self, forKey: .canMoveInto) ?? false canBecomeDropbox = try container.decodeIfPresent(Bool.self, forKey: .canBecomeDropbox) ?? false + canEdit = try container.decodeIfPresent(Bool.self, forKey: .canEdit) ?? false + canSeeStats = try container.decodeIfPresent(Bool.self, forKey: .canSeeStats) ?? false + canSeeInfo = try container.decodeIfPresent(Bool.self, forKey: .canSeeInfo) ?? false + canDownload = try container.decodeIfPresent(Bool.self, forKey: .canDownload) ?? false + canComment = try container.decodeIfPresent(Bool.self, forKey: .canComment) ?? false + canRequestAccess = try container.decodeIfPresent(Bool.self, forKey: .canRequestAccess) ?? false } override public init() { From fe5772f34a1e57915a4c81cfe74845e95b53a6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 20 Aug 2024 14:32:08 +0200 Subject: [PATCH 006/129] feat(PublicShareMetadata): Parsing pass --- kDrive/Utils/UniversalLinksHelper.swift | 2 +- kDriveCore/Data/Api/DriveApiFetcher.swift | 48 ++++++++++++------- .../DriveFileManager+Transactionable.swift | 5 +- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 10158ff9d..c4dc11fc9 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,7 @@ enum UniversalLinksHelper { // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openFile(id: metadata.file_id, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + openFile(id: metadata.fileId, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) return true } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index de0cc6ca8..a4ced9060 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -56,32 +56,47 @@ public struct PublicShareMetadata: Decodable { public let url: URL public let fileId: Int public let right: String - // public let validUntil: Date? - // public let capabilities: Rights -// public let createdBy: TimeInterval -// public let createdAt: TimeInterval -// public let updatedAt: TimeInterval -// public let accessBlocked: Bool + public let validUntil: TimeInterval? + public let capabilities: Rights + + public let createdBy: TimeInterval + public let createdAt: TimeInterval + public let updatedAt: TimeInterval + public let accessBlocked: Bool enum CodingKeys: String, CodingKey { case url - case fileId = "file_id" + case fileId case right - // case validUntil = "valid_until" - // case capabilities -// case createdBy = "created_by" -// case createdAt = "created_at" -// case updatedAt = "updated_at" -// case accessBlocked = "access_blocked" + case validUntil + case capabilities + case createdBy + case createdAt + case updatedAt + case accessBlocked } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - url = try container.decode(URL.self, forKey: .url) - fileId = try container.decode(Int.self, forKey: .fileId) - right = try container.decode(String.self, forKey: .right) + do { + url = try container.decode(URL.self, forKey: .url) + fileId = try container.decode(Int.self, forKey: .fileId) + right = try container.decode(String.self, forKey: .right) + + validUntil = try container.decodeIfPresent(TimeInterval.self, forKey: .validUntil) + capabilities = try container.decode(Rights.self, forKey: .capabilities) + + createdBy = try container.decode(TimeInterval.self, forKey: .createdBy) + createdAt = try container.decode(TimeInterval.self, forKey: .createdAt) + updatedAt = try container.decode(TimeInterval.self, forKey: .updatedAt) + + accessBlocked = try container.decode(Bool.self, forKey: .accessBlocked) + } catch { + // TODO: remove + fatalError("error:\(error)") + } } } @@ -94,7 +109,6 @@ public class PublicShareApiFetcher: ApiFetcher { let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url let request = Session.default.request(shareLinkInfoUrl) let metadata: PublicShareMetadata = try await perform(request: request) - print("metadata\(metadata)") return metadata } } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index 3b3a4f967..f733d2743 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -32,7 +32,7 @@ public enum DriveFileManagerContext { /// Dedicated in memory dataset for a public share link case publicShare(shareId: String) - func realmURL(driveId: Int, driveUserId: Int) -> URL { + func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { case .drive: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId).realm") @@ -41,7 +41,8 @@ public enum DriveFileManagerContext { case .fileProvider: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId)-fp.realm") case .publicShare: - fatalError("noop") + // Public share are stored in memory only + return nil } } } From 1aff48b17d627a28624d17603e75c4eddd249428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 22 Aug 2024 18:21:25 +0200 Subject: [PATCH 007/129] refactor(Endpoint): Split files into dedicated extensiions feat(Endpoint): Finish the external links endpoint implementation --- kDrive/Utils/UniversalLinksHelper.swift | 15 +- kDriveCore/Data/Api/Endpoint+Files.swift | 306 ++++++++++++++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 87 ++++++ kDriveCore/Data/Api/Endpoint+Trash.swift | 70 +++++ kDriveCore/Data/Api/Endpoint.swift | 379 +---------------------- 5 files changed, 478 insertions(+), 379 deletions(-) create mode 100644 kDriveCore/Data/Api/Endpoint+Files.swift create mode 100644 kDriveCore/Data/Api/Endpoint+Share.swift create mode 100644 kDriveCore/Data/Api/Endpoint+Trash.swift diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index c4dc11fc9..0c40cee6f 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,7 @@ enum UniversalLinksHelper { // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openFile(id: metadata.fileId, driveFileManager: publicShareDriveFileManager, office: displayMode == .office) + openPublicShare(id: metadata.fileId, driveFileManager: publicShareDriveFileManager) return true } @@ -123,6 +123,19 @@ enum UniversalLinksHelper { return true } + private static func openPublicShare(id: Int, driveFileManager: DriveFileManager) { + Task { + do { + let file = try await driveFileManager.file(id: id) + @InjectService var appNavigable: AppNavigable + await appNavigable.present(file: file, driveFileManager: driveFileManager, office: false) + } catch { + DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") + await UIConstants.showSnackBarIfNeeded(error: error) + } + } + } + private static func openFile(id: Int, driveFileManager: DriveFileManager, office: Bool) { Task { do { diff --git a/kDriveCore/Data/Api/Endpoint+Files.swift b/kDriveCore/Data/Api/Endpoint+Files.swift new file mode 100644 index 000000000..251ff0792 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Files.swift @@ -0,0 +1,306 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 +import RealmSwift + +// MARK: - Files + +public extension Endpoint { + // MARK: Dropbox + + static func dropboxes(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/dropboxes") + } + + static func dropbox(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/dropbox", queryItems: [ + URLQueryItem(name: "with", value: "user,capabilities") + ]) + } + + static func dropboxInvite(file: AbstractFile) -> Endpoint { + return .dropbox(file: file).appending(path: "/invite") + } + + // MARK: Favorite + + static func favorites(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/favorites", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func favorite(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/favorite") + } + + // MARK: File access + + static func invitation(drive: AbstractDrive, id: Int) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/invitations/\(id)") + } + + static func access(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/access", queryItems: [ + URLQueryItem(name: "with", value: "user"), + noAvatarDefault() + ]) + } + + static func checkAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/check") + } + + static func invitationsAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/invitations") + } + + static func teamsAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/teams") + } + + static func teamAccess(file: AbstractFile, id: Int) -> Endpoint { + return .teamsAccess(file: file).appending(path: "/\(id)") + } + + static func usersAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/users") + } + + static func userAccess(file: AbstractFile, id: Int) -> Endpoint { + return .usersAccess(file: file).appending(path: "/\(id)") + } + + static func forceAccess(file: AbstractFile) -> Endpoint { + return .access(file: file).appending(path: "/force") + } + + // MARK: File permission + + static func acl(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/acl") + } + + static func permissions(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/permission") + } + + static func userPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/user") + } + + static func teamPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/team") + } + + static func inheritPermission(file: AbstractFile) -> Endpoint { + return .permissions(file: file).appending(path: "/inherit") + } + + static func permission(file: AbstractFile, id: Int) -> Endpoint { + return .permissions(file: file).appending(path: "/\(id)") + } + + // MARK: File version + + static func versions(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/versions") + } + + static func version(file: AbstractFile, id: Int) -> Endpoint { + return .versions(file: file).appending(path: "/\(id)") + } + + static func downloadVersion(file: AbstractFile, id: Int) -> Endpoint { + return .version(file: file, id: id).appending(path: "/download") + } + + static func restoreVersion(file: AbstractFile, id: Int) -> Endpoint { + return .version(file: file, id: id).appending(path: "/restore") + } + + // MARK: File/directory + + static func file(_ file: AbstractFile) -> Endpoint { + return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem()]) + } + + static func fileInfo(_ file: AbstractFile) -> Endpoint { + return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending( + path: "/files/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] + ) + } + + 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()]) + } + + static func createDirectory(in file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func createFile(in file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/file", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func thumbnail(file: AbstractFile, at date: Date) -> Endpoint { + return .fileInfoV2(file).appending(path: "/thumbnail", queryItems: [ + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func preview(file: AbstractFile, at date: Date) -> Endpoint { + return .fileInfoV2(file).appending(path: "/preview", queryItems: [ + URLQueryItem(name: "width", value: "2500"), + URLQueryItem(name: "height", value: "1500"), + URLQueryItem(name: "quality", value: "80"), + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { + let queryItems: [URLQueryItem]? + if let asType { + queryItems = [URLQueryItem(name: "as", value: asType)] + } else { + queryItems = nil + } + return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + } + + static func convert(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func move(file: AbstractFile, destination: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/move/\(destination.id)") + } + + static func duplicate(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/duplicate", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func copy(file: AbstractFile, destination: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/copy/\(destination.id)", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func rename(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func count(of directory: AbstractFile) -> Endpoint { + return .fileInfoV2(directory).appending(path: "/count") + } + + static func size(file: AbstractFile, depth: String) -> Endpoint { + return .fileInfo(file).appending(path: "/size", queryItems: [ + URLQueryItem(name: "depth", value: depth) + ]) + } + + static func unlock(file: AbstractFile) -> Endpoint { + return .fileInfo(file).appending(path: "/lock") + } + + static func directoryColor(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/color") + } + + // MARK: Root directory + + static func lockedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/lock") + } + + static func rootFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/1/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func bulkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/bulk") + } + + static func lastModifiedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/last_modified", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func largestFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/largest") + } + + static func mostVersionedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/most_versions") + } + + static func countByTypeFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/file_types") + } + + static func createTeamDirectory(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/team_directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func existFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/exists") + } + + static func sharedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/shared") + } + + static func mySharedFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending( + path: "/files/my_shared", + queryItems: [(FileWith.fileMinimal + [.users]).toQueryItem(), noAvatarDefault()] + ) + } + + static func sharedWithMeFiles(drive: AbstractDrive) -> Endpoint { + return .driveV3.appending(path: "/files/shared_with_me", + queryItems: [(FileWith.fileMinimal).toQueryItem()]) + } + + static func countInRoot(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/files/count") + } + + // 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()]) + } + + static func filePartialListing(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending( + path: "/files/listing/partial", + queryItems: [URLQueryItem(name: "with", value: "file")] + ) + } +} diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift new file mode 100644 index 000000000..f12998f60 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -0,0 +1,87 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 +import RealmSwift + +// MARK: - Share Links + +public extension Endpoint { + private static let sharedFileWithQuery = "with=capabilities,conversion_capabilities,supported_by" + + /// It is necessary to keep V1 here for backward compatibility of old links + static var shareUrlV1: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/app") + } + + static var shareUrlV2: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/2/app") + } + + static var shareUrlV3: Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/3/app") + } + + static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/files/links") + } + + static func shareLink(file: AbstractFile) -> Endpoint { + return .fileInfoV2(file).appending(path: "/link") + } + + /// Share link info + static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { + shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") + } + + /// Share link file + func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + /// Share link file children + func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { + let orderQuery = "order_by=\(sortType.value.apiValue)&order=\(sortType.value.order)" + return Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files?\(Self.sharedFileWithQuery)&\(orderQuery)") + } + + /// Share link file thumbnail + func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") + } + + /// Share mink file preview + func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") + } + + /// Download share link file + func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") + } + + func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return Self.shareUrlV1.appending(path: "share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") + } + + func importShareLinkFiles(driveId: Int) -> Endpoint { + return Self.shareUrlV2.appending(path: "\(driveId)/imports/sharelink") + } +} diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift new file mode 100644 index 000000000..d90a1a6d6 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Trash.swift @@ -0,0 +1,70 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 +import RealmSwift + +// MARK: - Trash + +public extension Endpoint { + static func trash(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func trashV2(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/trash") + } + + static func emptyTrash(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: "/trash") + } + + static func trashCount(drive: AbstractDrive) -> Endpoint { + return .trash(drive: drive).appending(path: "/count") + } + + static func trashedInfo(file: AbstractFile) -> Endpoint { + return .trash(drive: ProxyDrive(id: file.driveId)).appending( + path: "/\(file.id)", + queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] + ) + } + + static func trashedInfoV2(file: AbstractFile) -> Endpoint { + return .trashV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/\(file.id)") + } + + static func trashedFiles(of directory: AbstractFile) -> Endpoint { + return .trashedInfo(file: directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func restore(file: AbstractFile) -> Endpoint { + return .trashedInfoV2(file: file).appending(path: "/restore") + } + + static func trashThumbnail(file: AbstractFile, at date: Date) -> Endpoint { + return .trashedInfoV2(file: file).appending(path: "/thumbnail", queryItems: [ + URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") + ]) + } + + static func trashCount(of directory: AbstractFile) -> Endpoint { + return .trashedInfo(file: directory).appending(path: "/count") + } +} diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index 6d7156c01..9485a410d 100644 --- a/kDriveCore/Data/Api/Endpoint.swift +++ b/kDriveCore/Data/Api/Endpoint.swift @@ -167,7 +167,7 @@ extension File: AbstractFile {} // MARK: - Endpoints public extension Endpoint { - private static var driveV3: Endpoint { + static var driveV3: Endpoint { return Endpoint(hostKeypath: \.apiDriveHost, path: "/3/drive") } @@ -195,24 +195,6 @@ public extension Endpoint { return .driveInfoV2(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()]) - } - - static func filePartialListing(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending( - path: "/files/listing/partial", - queryItems: [URLQueryItem(name: "with", value: "file")] - ) - } - // MARK: Activities static func recentActivity(drive: AbstractDrive) -> Endpoint { @@ -310,211 +292,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/settings") } - // MARK: Dropbox - - static func dropboxes(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/dropboxes") - } - - static func dropbox(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/dropbox", queryItems: [ - URLQueryItem(name: "with", value: "user,capabilities") - ]) - } - - static func dropboxInvite(file: AbstractFile) -> Endpoint { - return .dropbox(file: file).appending(path: "/invite") - } - - // MARK: Favorite - - static func favorites(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/favorites", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func favorite(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/favorite") - } - - // MARK: File access - - static func invitation(drive: AbstractDrive, id: Int) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/invitations/\(id)") - } - - static func access(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/access", queryItems: [ - URLQueryItem(name: "with", value: "user"), - noAvatarDefault() - ]) - } - - static func checkAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/check") - } - - static func invitationsAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/invitations") - } - - static func teamsAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/teams") - } - - static func teamAccess(file: AbstractFile, id: Int) -> Endpoint { - return .teamsAccess(file: file).appending(path: "/\(id)") - } - - static func usersAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/users") - } - - static func userAccess(file: AbstractFile, id: Int) -> Endpoint { - return .usersAccess(file: file).appending(path: "/\(id)") - } - - static func forceAccess(file: AbstractFile) -> Endpoint { - return .access(file: file).appending(path: "/force") - } - - // MARK: File permission - - static func acl(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/acl") - } - - static func permissions(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/permission") - } - - static func userPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/user") - } - - static func teamPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/team") - } - - static func inheritPermission(file: AbstractFile) -> Endpoint { - return .permissions(file: file).appending(path: "/inherit") - } - - static func permission(file: AbstractFile, id: Int) -> Endpoint { - return .permissions(file: file).appending(path: "/\(id)") - } - - // MARK: File version - - static func versions(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/versions") - } - - static func version(file: AbstractFile, id: Int) -> Endpoint { - return .versions(file: file).appending(path: "/\(id)") - } - - static func downloadVersion(file: AbstractFile, id: Int) -> Endpoint { - return .version(file: file, id: id).appending(path: "/download") - } - - static func restoreVersion(file: AbstractFile, id: Int) -> Endpoint { - return .version(file: file, id: id).appending(path: "/restore") - } - - // MARK: File/directory - - static func file(_ file: AbstractFile) -> Endpoint { - return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending(path: "/files/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem()]) - } - - static func fileInfo(_ file: AbstractFile) -> Endpoint { - return .driveInfo(drive: ProxyDrive(id: file.driveId)).appending( - path: "/files/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] - ) - } - - 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()]) - } - - static func createDirectory(in file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func createFile(in file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/file", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func thumbnail(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfoV2(file).appending(path: "/thumbnail", queryItems: [ - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func preview(file: AbstractFile, at date: Date) -> Endpoint { - return .fileInfoV2(file).appending(path: "/preview", queryItems: [ - URLQueryItem(name: "width", value: "2500"), - URLQueryItem(name: "height", value: "1500"), - URLQueryItem(name: "quality", value: "80"), - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { - let queryItems: [URLQueryItem]? - if let asType { - queryItems = [URLQueryItem(name: "as", value: asType)] - } else { - queryItems = nil - } - return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) - } - - static func convert(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/convert", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func move(file: AbstractFile, destination: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/move/\(destination.id)") - } - - static func duplicate(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/duplicate", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func copy(file: AbstractFile, destination: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/copy/\(destination.id)", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func rename(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/rename", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func count(of directory: AbstractFile) -> Endpoint { - return .fileInfoV2(directory).appending(path: "/count") - } - - static func size(file: AbstractFile, depth: String) -> Endpoint { - return .fileInfo(file).appending(path: "/size", queryItems: [ - URLQueryItem(name: "depth", value: depth) - ]) - } - - static func unlock(file: AbstractFile) -> Endpoint { - return .fileInfo(file).appending(path: "/lock") - } - - static func directoryColor(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/color") - } - // MARK: - Import static func cancelImport(drive: AbstractDrive, id: Int) -> Endpoint { @@ -531,64 +308,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/preference") } - // MARK: Root directory - - static func lockedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/lock") - } - - static func rootFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/1/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func bulkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/bulk") - } - - static func lastModifiedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/last_modified", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func largestFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/largest") - } - - static func mostVersionedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/most_versions") - } - - static func countByTypeFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/file_types") - } - - static func createTeamDirectory(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/team_directory", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func existFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/exists") - } - - static func sharedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/shared") - } - - static func mySharedFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending( - path: "/files/my_shared", - queryItems: [(FileWith.fileMinimal + [.users]).toQueryItem(), noAvatarDefault()] - ) - } - - static func sharedWithMeFiles(drive: AbstractDrive) -> Endpoint { - return .driveV3.appending(path: "/files/shared_with_me", - queryItems: [(FileWith.fileMinimal).toQueryItem()]) - } - - static func countInRoot(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/files/count") - } - // MARK: Search static func search( @@ -626,53 +345,6 @@ public extension Endpoint { return .driveInfo(drive: drive).appending(path: "/files/search", queryItems: queryItems) } - // MARK: Trash - - static func trash(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func trashV2(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") - } - - static func emptyTrash(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") - } - - static func trashCount(drive: AbstractDrive) -> Endpoint { - return .trash(drive: drive).appending(path: "/count") - } - - static func trashedInfo(file: AbstractFile) -> Endpoint { - return .trash(drive: ProxyDrive(id: file.driveId)).appending( - path: "/\(file.id)", - queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()] - ) - } - - static func trashedInfoV2(file: AbstractFile) -> Endpoint { - return .trashV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/\(file.id)") - } - - static func trashedFiles(of directory: AbstractFile) -> Endpoint { - return .trashedInfo(file: directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()]) - } - - static func restore(file: AbstractFile) -> Endpoint { - return .trashedInfoV2(file: file).appending(path: "/restore") - } - - static func trashThumbnail(file: AbstractFile, at date: Date) -> Endpoint { - return .trashedInfoV2(file: file).appending(path: "/thumbnail", queryItems: [ - URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))") - ]) - } - - static func trashCount(of directory: AbstractFile) -> Endpoint { - return .trashedInfo(file: directory).appending(path: "/count") - } - // MARK: Upload // Direct Upload @@ -727,53 +399,4 @@ public extension Endpoint { static func sendUserInvitation(drive: AbstractDrive, id: Int) -> Endpoint { return .userInvitation(drive: drive, id: id).appending(path: "/send") } - - // MARK: Share Links - - static var shareUrlV2: Endpoint { - return Endpoint(hostKeypath: \.driveHost, path: "/2/app") - } - - static var shareUrlV3: Endpoint { - return Endpoint(hostKeypath: \.driveHost, path: "/3/app") - } - - static func shareLinkFiles(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/files/links") - } - - static func shareLink(file: AbstractFile) -> Endpoint { - return .fileInfoV2(file).appending(path: "/link") - } - - // Share link info - // fun getShareLinkInfo(driveld: Int, linkUvid: String) = "$SHARE_URL_V2$driveId/share/$linkUuid/init" - static func shareLinkInfo(driveId: Int, shareLinkUid: String) -> Endpoint { - Self.shareUrlV2.appending(path: "/\(driveId)/share/\(shareLinkUid)/init") - } - - // Share link file - // fun getShareLinkFile(driveld: Int, linkUvid: String, fileId: Int) = "$SHARE_URL_V3$driveId/share/$linkUuid/files/$fileId" - func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") - } - - // Share link file children - // fun getShareLinkFileChildren(driveld: Int, linkUuid: String, fileId: Int, sortType: SortType): String { -// func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int /* , sortType: SortType */ ) -> Endpoint {} - -// val orderQuery = "?order_by=${sortType.orderBy}&order=${sortType.order}" -// return "$SHARE_URL_V3$driveId/share/$linkUvid/files/$fileId/files$orderQuery" - - // Share link file thumbnail - // fun getShareLinkFileThumbnail(driveld: Int, linkUvid: String, file: File): String { - - // Share mink file preview - // fun getShareLinkFilePreview(driveId: Int, linkUvid: String, file: File): String { - - // Download share link file - // fun downloadShareLinkFile(driveld: Int, linkUvid: String, file: File): String { - - // Share link file - // private fun shareLinkFile(driveld: Int, linkUvid: String, file: File): String { } From 772ac0d8484ab0190e89f3c2dd80c5ec480e8d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 29 Aug 2024 17:28:03 +0200 Subject: [PATCH 008/129] feat: Fetch root folder for a public share --- kDrive/AppRouter.swift | 15 ++++++++++++ .../UI/Controller/Files/FilePresenter.swift | 20 ++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 23 ++++++++++++++----- kDriveCore/Data/Api/DriveApiFetcher.swift | 7 ++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 10 ++++---- kDriveCore/Utils/AppNavigable.swift | 3 +++ 6 files changed, 67 insertions(+), 11 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 3b2f05caf..5b090f536 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -584,6 +584,21 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable + @MainActor public func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) { + // TODO: Present on top of existing views + guard let window, + let rootViewController = window.rootViewController else { + fatalError("TODO: lazy load a rootViewController") + } + + let filePresenter = FilePresenter(viewController: rootViewController) + filePresenter.presentPublicShareDirectory( + rootFolder: rootFolder, + rootViewController: rootViewController, + driveFileManager: driveFileManager + ) + } + @MainActor public func present(file: File, driveFileManager: DriveFileManager) { present(file: file, driveFileManager: driveFileManager, office: false) } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 0d398e815..48dfeb1ec 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -132,6 +132,26 @@ final class FilePresenter { } } + public func presentPublicShareDirectory( + rootFolder: File, + rootViewController: UIViewController, + driveFileManager: DriveFileManager + ) { + let viewModel: FileListViewModel // TODO: use InMemoryFileListViewModel + viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: rootFolder) + + // TODO: Fix access right +// guard !rootFolder.isDisabled else { +// return +// } + + let nextVC = FileListViewController(viewModel: viewModel) + print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") +// navigationController?.pushViewController(nextVC, animated: true) + + rootViewController.present(nextVC, animated: true) + } + public func presentDirectory( for file: File, driveFileManager: DriveFileManager, diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 0c40cee6f..720e4a882 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -95,14 +95,19 @@ enum UniversalLinksHelper { } // request metadata - guard let metadata = try? await PublicShareApiFetcher().getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + let apiFetcher = PublicShareApiFetcher() + guard let metadata = try? await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) else { return false } // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) - openPublicShare(id: metadata.fileId, driveFileManager: publicShareDriveFileManager) + openPublicShare(driveId: driveIdInt, + linkUuid: shareLinkUid, + fileId: metadata.fileId, + driveFileManager: publicShareDriveFileManager, + apiFetcher: apiFetcher) return true } @@ -123,14 +128,20 @@ enum UniversalLinksHelper { return true } - private static func openPublicShare(id: Int, driveFileManager: DriveFileManager) { + private static func openPublicShare(driveId: Int, + linkUuid: String, + fileId: Int, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher) { Task { do { - let file = try await driveFileManager.file(id: id) + let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, + linkUuid: linkUuid, + fileId: fileId) @InjectService var appNavigable: AppNavigable - await appNavigable.present(file: file, driveFileManager: driveFileManager, office: false) + await appNavigable.presentPublicShare(rootFolder: rootFolder, driveFileManager: driveFileManager) } catch { - DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") + DDLogError("[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)") await UIConstants.showSnackBarIfNeeded(error: error) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index a4ced9060..3bb1c1463 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -111,6 +111,13 @@ public class PublicShareApiFetcher: ApiFetcher { let metadata: PublicShareMetadata = try await perform(request: request) return metadata } + + public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { + let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url + let request = Session.default.request(shareLinkFileUrl) + let shareLinkFile: File = try await perform(request: request) + return shareLinkFile + } } public class DriveApiFetcher: ApiFetcher { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index f12998f60..11dbed94b 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -52,8 +52,8 @@ public extension Endpoint { } /// Share link file - func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files/\(fileId)") + static func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } /// Share link file children @@ -63,17 +63,17 @@ public extension Endpoint { } /// Share link file thumbnail - func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } /// Share mink file preview - func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file - func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + static func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index dc54ce51a..31489e230 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -64,6 +64,9 @@ public protocol RouterFileNavigable { /// - office: Open in only office @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) + /// Present a file list for a public share + @MainActor func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) + /// Present a list of files from a folder /// - Parameters: /// - frozenFolder: Folder to display From e1c1644ba5fbc2e7e080c34b31dca4ee3534e4ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 2 Sep 2024 15:29:39 +0200 Subject: [PATCH 009/129] feat: Cursored public share children query --- kDrive/AppRouter.swift | 17 +++-- .../UI/Controller/Files/FilePresenter.swift | 13 ++-- .../SharedWithMe/SharedWithMeViewModel.swift | 63 +++++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 16 +++-- kDriveCore/Data/Api/DriveApiFetcher.swift | 20 ------ kDriveCore/Data/Api/Endpoint+Share.swift | 16 +++-- .../Data/Api/PublicShareApiFetcher.swift | 62 ++++++++++++++++++ .../DriveFileManager/DriveFileManager.swift | 23 ++++++- kDriveCore/Data/Models/File.swift | 13 ++++ kDriveCore/Utils/AppNavigable.swift | 7 ++- 10 files changed, 208 insertions(+), 42 deletions(-) create mode 100644 kDriveCore/Data/Api/PublicShareApiFetcher.swift diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 5b090f536..ad648d76d 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -584,7 +584,12 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable - @MainActor public func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) { + @MainActor public func presentPublicShare( + rootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) { // TODO: Present on top of existing views guard let window, let rootViewController = window.rootViewController else { @@ -592,11 +597,11 @@ public struct AppRouter: AppNavigable { } let filePresenter = FilePresenter(viewController: rootViewController) - filePresenter.presentPublicShareDirectory( - rootFolder: rootFolder, - rootViewController: rootViewController, - driveFileManager: driveFileManager - ) + filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, + rootFolder: rootFolder, + rootViewController: rootViewController, + driveFileManager: driveFileManager, + apiFetcher: apiFetcher) } @MainActor public func present(file: File, driveFileManager: DriveFileManager) { diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 48dfeb1ec..7dbba6db7 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -133,12 +133,17 @@ final class FilePresenter { } public func presentPublicShareDirectory( + publicShareProxy: PublicShareProxy, rootFolder: File, rootViewController: UIViewController, - driveFileManager: DriveFileManager + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher ) { - let viewModel: FileListViewModel // TODO: use InMemoryFileListViewModel - viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: rootFolder) + let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: rootFolder, + apiFetcher: apiFetcher) // TODO: Fix access right // guard !rootFolder.isDisabled else { @@ -148,7 +153,7 @@ final class FilePresenter { let nextVC = FileListViewController(viewModel: viewModel) print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") // navigationController?.pushViewController(nextVC, animated: true) - + rootViewController.present(nextVC, animated: true) } diff --git a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift index 0f8a41ce3..3410a6a1c 100644 --- a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift +++ b/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift @@ -20,6 +20,69 @@ import kDriveCore import RealmSwift import UIKit +/// Public share view model, loading content from memory realm +final class PublicShareViewModel: InMemoryFileListViewModel { + var publicShareProxy: PublicShareProxy? + let rootProxy: ProxyFile + var publicShareApiFetcher: PublicShareApiFetcher? + + required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { + guard let currentDirectory else { + fatalError("woops") + } + + let configuration = Configuration(selectAllSupported: false, + rootTitle: "public share", + emptyViewType: .emptyFolder, + supportsDrop: false, + matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) + + rootProxy = currentDirectory.proxify() + super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) + observedFiles = AnyRealmCollection(currentDirectory.children) + print("• observedFiles :\(observedFiles.count)") + } + + convenience init( + publicShareProxy: PublicShareProxy, + sortType: SortType, + driveFileManager: DriveFileManager, + currentDirectory: File, + apiFetcher: PublicShareApiFetcher + ) { + self.init(driveFileManager: driveFileManager, currentDirectory: currentDirectory) + self.publicShareProxy = publicShareProxy + self.sortType = sortType + publicShareApiFetcher = apiFetcher + } + + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + print("• loadFiles:\(cursor):\(forceRefresh)") + guard !isLoading || cursor != nil, + let publicShareProxy, + let publicShareApiFetcher else { + return + } + + // Only show loading indicator if we have nothing in cache + if !currentDirectory.canLoadChildrenFromCache { + startRefreshing(cursor: cursor) + } + defer { + endRefreshing() + } + + let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy, + publicShareProxy: publicShareProxy, + publicShareApiFetcher: publicShareApiFetcher) + print("• nextCursor:\(nextCursor)") + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor) + } + } +} + class SharedWithMeViewModel: FileListViewModel { required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 720e4a882..32e7734a1 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -136,12 +136,20 @@ enum UniversalLinksHelper { Task { do { let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, - linkUuid: linkUuid, - fileId: fileId) + linkUuid: linkUuid, + fileId: fileId) @InjectService var appNavigable: AppNavigable - await appNavigable.presentPublicShare(rootFolder: rootFolder, driveFileManager: driveFileManager) + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid) + await appNavigable.presentPublicShare( + rootFolder: rootFolder, + publicShareProxy: publicShareProxy, + driveFileManager: driveFileManager, + apiFetcher: apiFetcher + ) } catch { - DDLogError("[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)") + DDLogError( + "[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)" + ) await UIConstants.showSnackBarIfNeeded(error: error) } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 3bb1c1463..108cebef3 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -100,26 +100,6 @@ public struct PublicShareMetadata: Decodable { } } -public class PublicShareApiFetcher: ApiFetcher { - override public init() { - super.init() - } - - public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { - let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url - let request = Session.default.request(shareLinkInfoUrl) - let metadata: PublicShareMetadata = try await perform(request: request) - return metadata - } - - public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { - let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url - let request = Session.default.request(shareLinkFileUrl) - let shareLinkFile: File = try await perform(request: request) - return shareLinkFile - } -} - public class DriveApiFetcher: ApiFetcher { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var tokenable: InfomaniakTokenable diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 11dbed94b..21b5d4e4b 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -23,7 +23,6 @@ import RealmSwift // MARK: - Share Links public extension Endpoint { - private static let sharedFileWithQuery = "with=capabilities,conversion_capabilities,supported_by" /// It is necessary to keep V1 here for backward compatibility of old links static var shareUrlV1: Endpoint { @@ -57,9 +56,14 @@ public extension Endpoint { } /// Share link file children - func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { - let orderQuery = "order_by=\(sortType.value.apiValue)&order=\(sortType.value.order)" - return Self.shareUrlV3.appending(path: "\(driveId)/share/\(linkUuid)/files?\(Self.sharedFileWithQuery)&\(orderQuery)") + static func shareLinkFileChildren(driveId: Int, linkUuid: String, fileId: Int, sortType: SortType) -> Endpoint { + let orderByQuery = URLQueryItem(name: "order_by", value: sortType.value.apiValue) + let orderQuery = URLQueryItem(name: "order", value: sortType.value.order) + let withQuery = URLQueryItem(name: "with", value: "capabilities,conversion_capabilities,supported_by") + + let shareLinkQueryItems = [orderByQuery, orderQuery, withQuery] + let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files") + return fileChildrenEndpoint.appending(path: "", queryItems: shareLinkQueryItems) } /// Share link file thumbnail @@ -78,10 +82,10 @@ public extension Endpoint { } func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return Self.shareUrlV1.appending(path: "share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") + return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } func importShareLinkFiles(driveId: Int) -> Endpoint { - return Self.shareUrlV2.appending(path: "\(driveId)/imports/sharelink") + return Self.shareUrlV2.appending(path: "/\(driveId)/imports/sharelink") } } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift new file mode 100644 index 000000000..5804f362b --- /dev/null +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -0,0 +1,62 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 Alamofire +import InfomaniakCore +import InfomaniakDI +import InfomaniakLogin +import Kingfisher + +public class PublicShareApiFetcher: ApiFetcher { + override public init() { + super.init() + } + + public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { + let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + let request = Session.default.request(shareLinkInfoUrl) + let metadata: PublicShareMetadata = try await perform(request: request) + return metadata + } + + public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { + let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url + let request = Session.default.request(shareLinkFileUrl) + let shareLinkFile: File = try await perform(request: request) + return shareLinkFile + } + + /// Query a specific page + public func shareLinkFileChildren(publicShareProxy: PublicShareProxy, + sortType: SortType, + cursor: String? = nil) async throws -> ValidServerResponse<[File]> { + let shareLinkFileChildren = Endpoint.shareLinkFileChildren( + driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + fileId: publicShareProxy.fileId, + sortType: sortType + ) + .cursored(cursor) + .sorted(by: [sortType]) + + let shareLinkFileChildrenUrl = shareLinkFileChildren.url + let request = Session.default.request(shareLinkFileChildrenUrl) + let shareLinkFiles: ValidServerResponse<[File]> = try await perform(request: request) + return shareLinkFiles + } +} diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 8c848e359..f66d4ec21 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -89,7 +89,7 @@ public final class DriveFileManager { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) let inMemoryIdentifier: String? - if case let .publicShare(identifier) = context { + if case .publicShare(let identifier) = context { inMemoryIdentifier = "inMemory:\(identifier)" } else { inMemoryIdentifier = nil @@ -392,6 +392,27 @@ public final class DriveFileManager { forceRefresh: forceRefresh) } + public func publicShareFiles(rootProxy: ProxyFile, + publicShareProxy: PublicShareProxy, + cursor: String? = nil, + sortType: SortType = .nameAZ, + forceRefresh: Bool = false, + publicShareApiFetcher: PublicShareApiFetcher) async throws + -> (files: [File], nextCursor: String?) { + try await files(in: rootProxy, + fetchFiles: { + let mySharedFiles = try await publicShareApiFetcher.shareLinkFileChildren( + publicShareProxy: publicShareProxy, + sortType: sortType + ) + return mySharedFiles + }, + cursor: cursor, + sortType: sortType, + keepProperties: [.standard, .path, .version], + forceRefresh: forceRefresh) + } + public func searchFile(query: String? = nil, date: DateInterval? = nil, fileType: ConvertedType? = nil, diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index ffdc71d8c..c3f24eea1 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -182,6 +182,19 @@ public enum ConvertedType: String, CaseIterable { public static let ignoreThumbnailTypes = downloadableTypes } +/// Minimal data needed to query a PublicShare +public struct PublicShareProxy { + let driveId: Int + let fileId: Int + let shareLinkUid: String + + public init(driveId: Int, fileId: Int, shareLinkUid: String) { + self.driveId = driveId + self.fileId = fileId + self.shareLinkUid = shareLinkUid + } +} + public enum SortType: String { case nameAZ case nameZA diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index 31489e230..7a002b42f 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -65,7 +65,12 @@ public protocol RouterFileNavigable { @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) /// Present a file list for a public share - @MainActor func presentPublicShare(rootFolder: File, driveFileManager: DriveFileManager) + @MainActor func presentPublicShare( + rootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) /// Present a list of files from a folder /// - Parameters: From 84fa7fd1367fc9ade03b50029c43284eceec7004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Sep 2024 18:03:12 +0200 Subject: [PATCH 010/129] feat: Can natively show a public share within the app --- kDrive/AppRouter.swift | 4 +- .../UI/Controller/Files/FilePresenter.swift | 5 +- .../PublicShareViewModel.swift} | 40 ++------------ .../Menu/Share/SharedWithMeViewModel.swift | 53 +++++++++++++++++++ kDrive/Utils/UniversalLinksHelper.swift | 9 +++- kDriveCore/Data/Api/Endpoint+Share.swift | 2 +- kDriveCore/Data/Cache/AccountManager.swift | 19 +++++-- .../DriveFileManager/DriveFileManager.swift | 15 ++++++ .../DriveInfosManager/DriveInfosManager.swift | 8 +++ kDriveCore/Data/Models/Drive/Drive.swift | 3 +- kDriveCore/Utils/AppNavigable.swift | 4 +- 11 files changed, 112 insertions(+), 50 deletions(-) rename kDrive/UI/Controller/Menu/{SharedWithMe/SharedWithMeViewModel.swift => Share/PublicShareViewModel.swift} (63%) create mode 100644 kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index ad648d76d..6c0f38556 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -585,7 +585,7 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable @MainActor public func presentPublicShare( - rootFolder: File, + frozenRootFolder: File, publicShareProxy: PublicShareProxy, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher @@ -598,7 +598,7 @@ public struct AppRouter: AppNavigable { let filePresenter = FilePresenter(viewController: rootViewController) filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, - rootFolder: rootFolder, + frozenRootFolder: frozenRootFolder, rootViewController: rootViewController, driveFileManager: driveFileManager, apiFetcher: apiFetcher) diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 7dbba6db7..d2b50c691 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -134,7 +134,7 @@ final class FilePresenter { public func presentPublicShareDirectory( publicShareProxy: PublicShareProxy, - rootFolder: File, + frozenRootFolder: File, rootViewController: UIViewController, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher @@ -142,7 +142,7 @@ final class FilePresenter { let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, sortType: .nameAZ, driveFileManager: driveFileManager, - currentDirectory: rootFolder, + currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) // TODO: Fix access right @@ -150,6 +150,7 @@ final class FilePresenter { // return // } + // TODO: Build clean context aware navigation let nextVC = FileListViewController(viewModel: viewModel) print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") // navigationController?.pushViewController(nextVC, animated: true) diff --git a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift similarity index 63% rename from kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift rename to kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 3410a6a1c..0898ce34d 100644 --- a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -1,6 +1,6 @@ /* Infomaniak kDrive - iOS App - Copyright (C) 2021 Infomaniak Network SA + Copyright (C) 2024 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 @@ -28,9 +28,10 @@ final class PublicShareViewModel: InMemoryFileListViewModel { required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { guard let currentDirectory else { - fatalError("woops") + fatalError("PublicShareViewModel requires a currentDirectory to work") } + // TODO: i18n let configuration = Configuration(selectAllSupported: false, rootTitle: "public share", emptyViewType: .emptyFolder, @@ -40,7 +41,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { rootProxy = currentDirectory.proxify() super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) observedFiles = AnyRealmCollection(currentDirectory.children) - print("• observedFiles :\(observedFiles.count)") } convenience init( @@ -57,7 +57,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { - print("• loadFiles:\(cursor):\(forceRefresh)") guard !isLoading || cursor != nil, let publicShareProxy, let publicShareApiFetcher else { @@ -75,39 +74,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy, publicShareProxy: publicShareProxy, publicShareApiFetcher: publicShareApiFetcher) - print("• nextCursor:\(nextCursor)") - endRefreshing() - if let nextCursor { - try await loadFiles(cursor: nextCursor) - } - } -} - -class SharedWithMeViewModel: FileListViewModel { - required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { - let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) - let configuration = Configuration(selectAllSupported: false, - rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, - emptyViewType: .noSharedWithMe, - supportsDrop: false, - matomoViewPath: [MatomoUtils.Views.menu.displayName, "SharedWithMe"]) - - super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: sharedWithMeRootFile) - observedFiles = AnyRealmCollection(AnyRealmCollection(sharedWithMeRootFile.children).filesSorted(by: sortType)) - } - - 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(cursor: cursor) - } - defer { - endRefreshing() - } - - let (_, nextCursor) = try await driveFileManager.sharedWithMeFiles(cursor: cursor, sortType: sortType, forceRefresh: true) endRefreshing() if let nextCursor { try await loadFiles(cursor: nextCursor) diff --git a/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift new file mode 100644 index 000000000..0f8a41ce3 --- /dev/null +++ b/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift @@ -0,0 +1,53 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2021 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 kDriveCore +import RealmSwift +import UIKit + +class SharedWithMeViewModel: FileListViewModel { + required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { + let sharedWithMeRootFile = driveFileManager.getManagedFile(from: DriveFileManager.sharedWithMeRootFile) + let configuration = Configuration(selectAllSupported: false, + rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, + emptyViewType: .noSharedWithMe, + supportsDrop: false, + matomoViewPath: [MatomoUtils.Views.menu.displayName, "SharedWithMe"]) + + super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: sharedWithMeRootFile) + observedFiles = AnyRealmCollection(AnyRealmCollection(sharedWithMeRootFile.children).filesSorted(by: sortType)) + } + + 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(cursor: cursor) + } + defer { + endRefreshing() + } + + let (_, nextCursor) = try await driveFileManager.sharedWithMeFiles(cursor: cursor, sortType: sortType, forceRefresh: true) + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor) + } + } +} diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 32e7734a1..3c8ea1dd0 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -138,10 +138,17 @@ enum UniversalLinksHelper { let rootFolder = try await apiFetcher.getShareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId) + // Root folder must be in database for the FileListViewModel to work + try driveFileManager.database.writeTransaction { writableRealm in + writableRealm.add(rootFolder) + } + + let frozenRootFolder = rootFolder.freeze() + @InjectService var appNavigable: AppNavigable let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid) await appNavigable.presentPublicShare( - rootFolder: rootFolder, + frozenRootFolder: frozenRootFolder, publicShareProxy: publicShareProxy, driveFileManager: driveFileManager, apiFetcher: apiFetcher diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 21b5d4e4b..765077eb3 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -62,7 +62,7 @@ public extension Endpoint { let withQuery = URLQueryItem(name: "with", value: "capabilities,conversion_capabilities,supported_by") let shareLinkQueryItems = [orderByQuery, orderQuery, withQuery] - let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files") + let fileChildrenEndpoint = Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)/files") return fileChildrenEndpoint.appending(path: "", queryItems: shareLinkQueryItems) } diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index f58e1603e..3f81233e7 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -216,15 +216,26 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { return inMemoryDriveFileManager } - // Big hack, refactor to allow for non authenticated requests + // TODO: Big hack, refactor to allow for non authenticated requests guard let someToken = apiFetchers.values.first?.currentToken else { - fatalError("probably no account availlable") + fatalError("probably no account available") } + // FileViewModel K.O. without a valid drive in Realm, therefore add one + let publicShareDrive = Drive() + publicShareDrive.objectId = publicShareId + @LazyInjectService var driveInfosManager: DriveInfosManager + do { + try driveInfosManager.storePublicShareDrive(drive: publicShareDrive) + } catch { + fatalError("unable to update public share drive in base, \(error)") + } + let forzenPublicShareDrive = publicShareDrive.freeze() + let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) let context = DriveFileManagerContext.publicShare(shareId: publicShareId) - let noopDrive = Drive() - return DriveFileManager(drive: noopDrive, apiFetcher: apiFetcher, context: context) + + return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index f66d4ec21..c022ca1a7 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -26,6 +26,21 @@ import InfomaniakLogin import RealmSwift import SwiftRegex +// TODO: Move to core +extension TransactionExecutor: CustomStringConvertible { + public var description: String { + var render = "TransactionExecutor: realm access issue" + try? writeTransaction { realm in + render = """ + TransactionExecutor: + realmURL:\(realm.configuration.fileURL) + inMemory:\(realm.configuration.inMemoryIdentifier) + """ + } + return render + } +} + // MARK: - Transactionable public final class DriveFileManager { diff --git a/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift index 744bf2232..aeee624b1 100644 --- a/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift +++ b/kDriveCore/Data/Cache/DriveInfosManager/DriveInfosManager.swift @@ -134,6 +134,14 @@ public final class DriveInfosManager: DriveInfosManagerQueryable { drive.sharedWithMe = sharedWithMe } + // TODO: Add a flag that this drive can be cleaned + /// Store a specific public share Drive in realm for use by FileListViewControllers + public func storePublicShareDrive(drive: Drive) throws { + try driveInfoDatabase.writeTransaction { writableRealm in + writableRealm.add(drive, update: .modified) + } + } + @discardableResult func storeDriveResponse(user: InfomaniakCore.UserProfile, driveResponse: DriveResponse) -> [Drive] { var driveList = [Drive]() diff --git a/kDriveCore/Data/Models/Drive/Drive.swift b/kDriveCore/Data/Models/Drive/Drive.swift index c6ff0c515..d6d8bbef9 100644 --- a/kDriveCore/Data/Models/Drive/Drive.swift +++ b/kDriveCore/Data/Models/Drive/Drive.swift @@ -190,8 +190,9 @@ public final class Drive: Object, Codable { // File is not managed by Realm: cannot use the `.sorted(by:)` method :( fileCategoriesIds = file.categories.sorted { $0.addedAt.compare($1.addedAt) == .orderedAscending }.map(\.categoryId) } - let filteredCategories = categories.filter(NSPredicate(format: "id IN %@", fileCategoriesIds)) + // Sort the categories + let filteredCategories = categories.filter("id IN %@", fileCategoriesIds) return fileCategoriesIds.compactMap { id in filteredCategories.first { $0.id == id } } } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index 7a002b42f..bcd52b59e 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -64,9 +64,9 @@ public protocol RouterFileNavigable { /// - office: Open in only office @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) - /// Present a file list for a public share + /// Present a file list for a public share, regardless of authenticated state @MainActor func presentPublicShare( - rootFolder: File, + frozenRootFolder: File, publicShareProxy: PublicShareProxy, driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher From 130305f6e2605a6c55ac8cead0f93bd18754a188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 16 Sep 2024 16:52:18 +0200 Subject: [PATCH 011/129] fix(FilePresenter): Present public share in context --- kDrive/AppRouter.swift | 36 +++++++++++++++---- .../UI/Controller/Files/FilePresenter.swift | 26 -------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 6c0f38556..d5ddfe1d5 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -284,7 +284,8 @@ public struct AppRouter: AppNavigable { let fileIds = sceneUserInfo[SceneRestorationValues.Carousel.filesIds.rawValue] as? [Int], let currentIndex = sceneUserInfo[SceneRestorationValues.Carousel.currentIndex.rawValue] as? Int, let normalFolderHierarchy = sceneUserInfo[SceneRestorationValues.Carousel.normalFolderHierarchy.rawValue] as? Bool, - let presentationOrigin = sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else { + let presentationOrigin = + sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else { Log.sceneDelegate("metadata issue for PreviewController :\(sceneUserInfo)", level: .error) return } @@ -596,12 +597,33 @@ public struct AppRouter: AppNavigable { fatalError("TODO: lazy load a rootViewController") } - let filePresenter = FilePresenter(viewController: rootViewController) - filePresenter.presentPublicShareDirectory(publicShareProxy: publicShareProxy, - frozenRootFolder: frozenRootFolder, - rootViewController: rootViewController, - driveFileManager: driveFileManager, - apiFetcher: apiFetcher) + guard let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("Root is not a MainTabViewController") + return + } + + // TODO: Fix access right + // guard !rootFolder.isDisabled else { + // return + // } + + rootViewController.dismiss(animated: false) { + rootViewController.selectedIndex = MainTabBarIndex.files.rawValue + + guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { + return + } + + let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: frozenRootFolder, + apiFetcher: apiFetcher) + let viewController = FileListViewController(viewModel: viewModel) + print("viewController:\(viewController) viewModel:\(viewModel) navigationController:\(navigationController)") + + navigationController.pushViewController(viewController, animated: true) + } } @MainActor public func present(file: File, driveFileManager: DriveFileManager) { diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index d2b50c691..0d398e815 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -132,32 +132,6 @@ final class FilePresenter { } } - public func presentPublicShareDirectory( - publicShareProxy: PublicShareProxy, - frozenRootFolder: File, - rootViewController: UIViewController, - driveFileManager: DriveFileManager, - apiFetcher: PublicShareApiFetcher - ) { - let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, - sortType: .nameAZ, - driveFileManager: driveFileManager, - currentDirectory: frozenRootFolder, - apiFetcher: apiFetcher) - - // TODO: Fix access right -// guard !rootFolder.isDisabled else { -// return -// } - - // TODO: Build clean context aware navigation - let nextVC = FileListViewController(viewModel: viewModel) - print("nextVC:\(nextVC) viewModel:\(viewModel) navigationController:\(navigationController)") -// navigationController?.pushViewController(nextVC, animated: true) - - rootViewController.present(nextVC, animated: true) - } - public func presentDirectory( for file: File, driveFileManager: DriveFileManager, From 0b48b91fb835c2a5afd63b4fe9ff40c0fe22a0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 16 Sep 2024 17:44:26 +0200 Subject: [PATCH 012/129] fix: Capabilities for Public Share --- kDrive/AppRouter.swift | 7 ++++--- kDriveCore/Data/Api/PublicShareApiFetcher.swift | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index d5ddfe1d5..66a8b000f 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -603,9 +603,10 @@ public struct AppRouter: AppNavigable { } // TODO: Fix access right - // guard !rootFolder.isDisabled else { - // return - // } + guard !frozenRootFolder.isDisabled else { + fatalError("isDisabled") + return + } rootViewController.dismiss(animated: false) { rootViewController.selectedIndex = MainTabBarIndex.files.rawValue diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 5804f362b..789de315e 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -36,7 +36,10 @@ public class PublicShareApiFetcher: ApiFetcher { public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url - let request = Session.default.request(shareLinkFileUrl) + let requestParameters: [String: String] = [ + APIUploadParameter.with.rawValue: FileWith.capabilities.rawValue + ] + let request = Session.default.request(shareLinkFileUrl, parameters: requestParameters) let shareLinkFile: File = try await perform(request: request) return shareLinkFile } From 272968e6e19cca68bbf70f966cb1094f8bcdbabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 17 Sep 2024 17:11:22 +0200 Subject: [PATCH 013/129] feat: Thumbnails on public share --- .../View/Files/FileCollectionViewCell.swift | 82 +++++++++++++++---- .../Files/FileGridCollectionViewCell.swift | 7 +- kDrive/Utils/UniversalLinksHelper.swift | 6 +- kDriveCore/Data/Api/Endpoint+Share.swift | 10 ++- kDriveCore/Data/Cache/AccountManager.swift | 9 +- .../DriveFileManager+Transactionable.swift | 2 +- .../DriveFileManager/DriveFileManager.swift | 44 ++++++++++ kDriveCore/Data/Models/File+Image.swift | 32 ++++++++ 8 files changed, 165 insertions(+), 27 deletions(-) diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 23901692f..e4172dd71 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -48,6 +48,16 @@ protocol FileCellDelegate: AnyObject { var file: File var selectionMode: Bool var isSelected = false + + /// UUID of the public share if file exists within a public share + let publicShareId: String? + + /// Drive ID of the public share if file exists within a public share + let publicDriveId: Int? + + /// Root file ID of the public share if file exists within a public share + let publicRootFileId: Int? + private var downloadProgressObserver: ObservationToken? private var downloadObserver: ObservationToken? var thumbnailDownloadTask: Kingfisher.DownloadTask? @@ -114,6 +124,10 @@ protocol FileCellDelegate: AnyObject { init(driveFileManager: DriveFileManager, file: File, selectionMode: Bool) { self.file = file self.selectionMode = selectionMode + publicShareId = driveFileManager.publicShareId + publicDriveId = driveFileManager.publicDriveId + publicRootFileId = driveFileManager.publicRootFileId + categories = driveFileManager.drive.categories(for: file) } @@ -138,26 +152,53 @@ protocol FileCellDelegate: AnyObject { } func setThumbnail(on imageView: UIImageView) { + // check if public share / use specific endpoint guard !file.isInvalidated, - (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) - else { return } + (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) else { + return + } + // Configure placeholder imageView.image = nil imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = UIConstants.imageCornerRadius imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color - // Fetch thumbnail - thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in - guard let self, - !self.file.isInvalidated, - !self.isSelected else { - return + + if let publicShareId = publicShareId, + let publicDriveId = publicDriveId { + // Fetch public share thumbnail + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareId, + publicDriveId: publicDriveId, + publicFileId: file.id) { [ + requestFileId = file.id, + weak self + ] image, _ in + guard let self, + !self.file.isInvalidated, + !self.isSelected else { + return + } + + if file.id == requestFileId { + imageView.image = image + imageView.backgroundColor = nil + } } - if file.id == requestFileId { - imageView.image = image - imageView.backgroundColor = nil + } else { + // Fetch thumbnail + thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in + guard let self, + !self.file.isInvalidated, + !self.isSelected else { + return + } + + if file.id == requestFileId { + imageView.image = image + imageView.backgroundColor = nil + } } } } @@ -302,7 +343,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { func configure(with viewModel: FileViewModel) { self.viewModel = viewModel - configureLogoImage() + configureLogoImage(viewModel: viewModel) titleLabel.text = viewModel.title detailLabel?.text = viewModel.subtitle favoriteImageView?.isHidden = !viewModel.isFavorite @@ -321,7 +362,12 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { } func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) { - configure(with: FileViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode)) + let fileViewModel = FileViewModel( + driveFileManager: driveFileManager, + file: file, + selectionMode: selectionMode + ) + configure(with: fileViewModel) } /// Update the cell selection mode. @@ -333,18 +379,20 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { } func configureForSelection() { - guard viewModel?.selectionMode == true else { return } + guard let viewModel, + viewModel.selectionMode == true else { + return + } if isSelected { configureCheckmarkImage() configureImport(shouldDisplay: false) } else { - configureLogoImage() + configureLogoImage(viewModel: viewModel) } } - private func configureLogoImage() { - guard let viewModel else { return } + private func configureLogoImage(viewModel: FileViewModel) { logoImage.isAccessibilityElement = true logoImage.accessibilityLabel = viewModel.iconAccessibilityLabel logoImage.image = viewModel.icon diff --git a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift index 4ba0608a7..58b6e7ec2 100644 --- a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift @@ -110,7 +110,12 @@ class FileGridCollectionViewCell: FileCollectionViewCell { } override func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) { - configure(with: FileGridViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode)) + let viewModel = FileGridViewModel( + driveFileManager: driveFileManager, + file: file, + selectionMode: selectionMode + ) + configure(with: viewModel) } override func configureLoading() { diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 3c8ea1dd0..38c72c2cf 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -102,7 +102,11 @@ enum UniversalLinksHelper { } // get file ID from metadata - let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(for: shareLinkUid) + let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( + for: shareLinkUid, + driveId: driveIdInt, + rootFileId: metadata.fileId + ) openPublicShare(driveId: driveIdInt, linkUuid: shareLinkUid, fileId: metadata.fileId, diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 765077eb3..dfb913047 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -23,7 +23,6 @@ import RealmSwift // MARK: - Share Links public extension Endpoint { - /// It is necessary to keep V1 here for backward compatibility of old links static var shareUrlV1: Endpoint { return Endpoint(hostKeypath: \.driveHost, path: "/app") @@ -52,7 +51,12 @@ public extension Endpoint { /// Share link file static func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - Self.shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + /// Some legacy calls like thumbnails require a V2 call + static func shareLinkFileV2(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } /// Share link file children @@ -68,7 +72,7 @@ public extension Endpoint { /// Share link file thumbnail static func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } /// Share mink file preview diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 3f81233e7..047d4d4d9 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -78,8 +78,7 @@ public protocol AccountManageable: AnyObject { func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager /// Create on the fly an "in memory" DriveFileManager for a specific share - func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager - + func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -211,7 +210,7 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } - public func getInMemoryDriveFileManager(for publicShareId: String) -> DriveFileManager { + public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager { if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { return inMemoryDriveFileManager } @@ -233,7 +232,9 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { let forzenPublicShareDrive = publicShareDrive.freeze() let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) - let context = DriveFileManagerContext.publicShare(shareId: publicShareId) + let context = DriveFileManagerContext.publicShare(shareId: publicShareId, + driveId: driveId, + rootFileId: rootFileId) return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index f733d2743..b42faa6da 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -30,7 +30,7 @@ public enum DriveFileManagerContext { case sharedWithMe /// Dedicated in memory dataset for a public share link - case publicShare(shareId: String) + case publicShare(shareId: String, driveId: Int, rootFileId: Int) func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index c022ca1a7..07d161d00 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -99,6 +99,9 @@ public final class DriveFileManager { /// Fetch and write into DB with this object public let database: Transactionable + /// Context this object was initialized with + public let context: DriveFileManagerContext + /// Build a realm configuration for a specific Drive public static func configuration(context: DriveFileManagerContext, driveId: Int, driveUserId: Int) -> Realm.Configuration { let realmURL = context.realmURL(driveId: driveId, driveUserId: driveUserId) @@ -215,9 +218,50 @@ public final class DriveFileManager { ) } + public var isPublicShare: Bool { + switch context { + case .drive: + return false + case .fileProvider: + return false + case .sharedWithMe: + return false + case .publicShare(let shareId): + return true + } + } + + public var publicShareId: String? { + switch context { + case .publicShare(let shareId, _, _): + return shareId + default: + return nil + } + } + + public var publicDriveId: Int? { + switch context { + case .publicShare(_, let driveId, _): + return driveId + default: + return nil + } + } + + public var publicRootFileId: Int? { + switch context { + case .publicShare(_, _, let rootFileId): + return rootFileId + default: + return nil + } + } + init(drive: Drive, apiFetcher: DriveApiFetcher, context: DriveFileManagerContext = .drive) { self.drive = drive self.apiFetcher = apiFetcher + self.context = context realmConfiguration = Self.configuration(context: context, driveId: drive.id, driveUserId: drive.userId) let realmURL = context.realmURL(driveId: drive.id, driveUserId: drive.userId) diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index 87fab6c8e..5fa873adc 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -16,10 +16,42 @@ along with this program. If not, see . */ +import InfomaniakCore import Kingfisher import UIKit public extension File { + /// Get a Thumbnail for a file from a public share + @discardableResult + func getPublicShareThumbnail(publicShareId: String, + publicDriveId: Int, + publicFileId: Int, + completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { + guard supportedBy.contains(.thumbnail), + let currentDriveFileManager = accountManager.currentDriveFileManager else { + completion(icon, false) + return nil + } + + let thumbnailURL = Endpoint.shareLinkFileThumbnail(driveId: publicDriveId, + linkUuid: publicShareId, + fileId: publicFileId).url + + return KingfisherManager.shared.retrieveImage(with: thumbnailURL) { result in + if let image = try? result.get().image { + completion(image, true) + } else { + // The file can become invalidated while retrieving the icon online + completion( + self.isInvalidated ? ConvertedType.unknown.icon : self + .icon, + false + ) + } + } + } + + /// Get a Thumbnail for a file for the current DriveFileManager @discardableResult func getThumbnail(completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { if supportedBy.contains(.thumbnail), let currentDriveFileManager = accountManager.currentDriveFileManager { From 4e484608911c36c64713b7a1bb93fab8f4db58d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 18 Sep 2024 18:21:42 +0200 Subject: [PATCH 014/129] feat: Can navigate hierarchy of public share folders refactor: Simplified code --- .../Files/File List/FileListViewModel.swift | 5 ++- .../UI/Controller/Files/FilePresenter.swift | 6 ++++ .../Files/Preview/PreviewViewController.swift | 4 +-- .../View/Files/FileCollectionViewCell.swift | 22 ++++--------- .../Data/Api/PublicShareApiFetcher.swift | 6 ++-- kDriveCore/Data/Cache/AccountManager.swift | 5 ++- .../DriveFileManager+Transactionable.swift | 2 +- .../DriveFileManager/DriveFileManager.swift | 33 ++++--------------- kDriveCore/Data/Models/File.swift | 6 ++-- 9 files changed, 34 insertions(+), 55 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 6201676ef..f43164e12 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -342,7 +342,10 @@ class FileListViewModel: SelectDelegate { } func didSelectFile(at indexPath: IndexPath) { - guard let file: File = getFile(at: indexPath) else { return } + guard let file: File = getFile(at: indexPath) else { + return + } + if ReachabilityListener.instance.currentStatus == .offline && !file.isDirectory && !file.isAvailableOffline { return } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 0d398e815..be01bcbeb 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -145,6 +145,12 @@ final class FilePresenter { let viewModel: FileListViewModel if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) + } else if let publicShareProxy = driveFileManager.publicShareProxy { + viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: file, + apiFetcher: PublicShareApiFetcher()) } else if file.isTrashed || file.deletedAt != nil { viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else { diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 19c0c6477..ae80c6de3 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -609,8 +609,8 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, previewPageViewController.driveFileManager = driveFileManager previewPageViewController.normalFolderHierarchy = normalFolderHierarchy previewPageViewController.presentationOrigin = presentationOrigin - // currentIndex should be set at the end of the function as the it takes time and the viewDidLoad() is called before the - // function returns + // currentIndex should be set at the end of the function as the it takes time + // and the viewDidLoad() is called before the function returns // this should be fixed in the future with the refactor of the init previewPageViewController.currentIndex = IndexPath(row: index, section: 0) return previewPageViewController diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index e4172dd71..09e0b416b 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -49,14 +49,8 @@ protocol FileCellDelegate: AnyObject { var selectionMode: Bool var isSelected = false - /// UUID of the public share if file exists within a public share - let publicShareId: String? - - /// Drive ID of the public share if file exists within a public share - let publicDriveId: Int? - - /// Root file ID of the public share if file exists within a public share - let publicRootFileId: Int? + /// Public share data if file exists within a public share + let publicShareProxy: PublicShareProxy? private var downloadProgressObserver: ObservationToken? private var downloadObserver: ObservationToken? @@ -124,10 +118,7 @@ protocol FileCellDelegate: AnyObject { init(driveFileManager: DriveFileManager, file: File, selectionMode: Bool) { self.file = file self.selectionMode = selectionMode - publicShareId = driveFileManager.publicShareId - publicDriveId = driveFileManager.publicDriveId - publicRootFileId = driveFileManager.publicRootFileId - + publicShareProxy = driveFileManager.publicShareProxy categories = driveFileManager.drive.categories(for: file) } @@ -165,11 +156,10 @@ protocol FileCellDelegate: AnyObject { imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color - if let publicShareId = publicShareId, - let publicDriveId = publicDriveId { + if let publicShareProxy = publicShareProxy { // Fetch public share thumbnail - thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareId, - publicDriveId: publicDriveId, + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, publicFileId: file.id) { [ requestFileId = file.id, weak self diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 789de315e..341b22741 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -29,6 +29,7 @@ public class PublicShareApiFetcher: ApiFetcher { public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url + // TODO: Use authenticated token if availlable let request = Session.default.request(shareLinkInfoUrl) let metadata: PublicShareMetadata = try await perform(request: request) return metadata @@ -45,13 +46,14 @@ public class PublicShareApiFetcher: ApiFetcher { } /// Query a specific page - public func shareLinkFileChildren(publicShareProxy: PublicShareProxy, + public func shareLinkFileChildren(rootFolderId: Int, + publicShareProxy: PublicShareProxy, sortType: SortType, cursor: String? = nil) async throws -> ValidServerResponse<[File]> { let shareLinkFileChildren = Endpoint.shareLinkFileChildren( driveId: publicShareProxy.driveId, linkUuid: publicShareProxy.shareLinkUid, - fileId: publicShareProxy.fileId, + fileId: rootFolderId, sortType: sortType ) .cursored(cursor) diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 047d4d4d9..53ee93823 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -232,9 +232,8 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { let forzenPublicShareDrive = publicShareDrive.freeze() let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) - let context = DriveFileManagerContext.publicShare(shareId: publicShareId, - driveId: driveId, - rootFileId: rootFileId) + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId) + let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy) return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index b42faa6da..bf1be04df 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -30,7 +30,7 @@ public enum DriveFileManagerContext { case sharedWithMe /// Dedicated in memory dataset for a public share link - case publicShare(shareId: String, driveId: Int, rootFileId: Int) + case publicShare(shareProxy: PublicShareProxy) func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 07d161d00..c924e5704 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -220,39 +220,17 @@ public final class DriveFileManager { public var isPublicShare: Bool { switch context { - case .drive: - return false - case .fileProvider: - return false - case .sharedWithMe: - return false - case .publicShare(let shareId): + case .publicShare: return true - } - } - - public var publicShareId: String? { - switch context { - case .publicShare(let shareId, _, _): - return shareId default: - return nil - } - } - - public var publicDriveId: Int? { - switch context { - case .publicShare(_, let driveId, _): - return driveId - default: - return nil + return false } } - public var publicRootFileId: Int? { + public var publicShareProxy: PublicShareProxy? { switch context { - case .publicShare(_, _, let rootFileId): - return rootFileId + case .publicShare(let shareProxy): + return shareProxy default: return nil } @@ -461,6 +439,7 @@ public final class DriveFileManager { try await files(in: rootProxy, fetchFiles: { let mySharedFiles = try await publicShareApiFetcher.shareLinkFileChildren( + rootFolderId: rootProxy.id, publicShareProxy: publicShareProxy, sortType: sortType ) diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index c3f24eea1..e4625c92d 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -184,9 +184,9 @@ public enum ConvertedType: String, CaseIterable { /// Minimal data needed to query a PublicShare public struct PublicShareProxy { - let driveId: Int - let fileId: Int - let shareLinkUid: String + public let driveId: Int + public let fileId: Int + public let shareLinkUid: String public init(driveId: Int, fileId: Int, shareLinkUid: String) { self.driveId = driveId From 3bdf94e0a2c650b61155ab769caabbf45c0967fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Sep 2024 11:47:16 +0200 Subject: [PATCH 015/129] feat: Preview for public share --- .../Files/Preview/PreviewViewController.swift | 6 ++- ...DownloadingPreviewCollectionViewCell.swift | 20 ++++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 2 +- kDriveCore/Data/Models/File+Image.swift | 49 +++++++++++++------ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index ae80c6de3..ef7b97cff 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -650,7 +650,11 @@ extension PreviewViewController: UICollectionViewDataSource { ) { let file = previewFiles[indexPath.row] if let cell = cell as? DownloadingPreviewCollectionViewCell { - cell.progressiveLoadingForFile(file) + if let publicShareProxy = driveFileManager.publicShareProxy { + cell.progressiveLoadingForPublicShareFile(file, publicShareProxy: publicShareProxy) + } else { + cell.progressiveLoadingForFile(file) + } } } diff --git a/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift b/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift index 18d434818..f26e027c0 100644 --- a/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift +++ b/kDrive/UI/View/Files/Preview/DownloadingPreviewCollectionViewCell.swift @@ -97,6 +97,26 @@ class DownloadingPreviewCollectionViewCell: UICollectionViewCell, UIScrollViewDe return previewImageView } + func progressiveLoadingForPublicShareFile(_ file: File, publicShareProxy: PublicShareProxy) { + self.file = file + file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { thumbnail, _ in + self.previewImageView.image = thumbnail + } + + previewDownloadTask = file.getPublicSharePreview(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { [weak previewImageView] preview in + guard let previewImageView else { + return + } + if let preview { + previewImageView.image = preview + } + } + } + func progressiveLoadingForFile(_ file: File) { self.file = file file.getThumbnail { thumbnail, _ in diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index dfb913047..2ea731f11 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -77,7 +77,7 @@ public extension Endpoint { /// Share mink file preview static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index 5fa873adc..e7a07e627 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -27,8 +27,7 @@ public extension File { publicDriveId: Int, publicFileId: Int, completion: @escaping ((UIImage, Bool) -> Void)) -> Kingfisher.DownloadTask? { - guard supportedBy.contains(.thumbnail), - let currentDriveFileManager = accountManager.currentDriveFileManager else { + guard supportedBy.contains(.thumbnail) else { completion(icon, false) return nil } @@ -72,22 +71,40 @@ public extension File { } @discardableResult - func getPreview(completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { - if let currentDriveFileManager = accountManager.currentDriveFileManager { - return KingfisherManager.shared.retrieveImage(with: imagePreviewUrl, - options: [ - .requestModifier(currentDriveFileManager.apiFetcher - .authenticatedKF), - .preloadAllAnimationData - ]) { result in - if let image = try? result.get().image { - completion(image) - } else { - completion(nil) - } + func getPublicSharePreview(publicShareId: String, + publicDriveId: Int, + publicFileId: Int, + completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { + let previewURL = Endpoint.shareLinkFilePreview(driveId: publicDriveId, + linkUuid: publicShareId, + fileId: publicFileId).url + + return KingfisherManager.shared.retrieveImage(with: previewURL) { result in + if let image = try? result.get().image { + completion(image) + } else { + completion(nil) } - } else { + } + } + + @discardableResult + func getPreview(completion: @escaping ((UIImage?) -> Void)) -> Kingfisher.DownloadTask? { + guard let currentDriveFileManager = accountManager.currentDriveFileManager else { return nil } + + return KingfisherManager.shared.retrieveImage(with: imagePreviewUrl, + options: [ + .requestModifier(currentDriveFileManager.apiFetcher + .authenticatedKF), + .preloadAllAnimationData + ]) { result in + if let image = try? result.get().image { + completion(image) + } else { + completion(nil) + } + } } } From 0b922d3b495e2273071d567413d23c811be3f1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Sep 2024 17:06:00 +0200 Subject: [PATCH 016/129] feat: Download file from public share --- .../MultipleSelectionFileListViewModel.swift | 9 ++- ...sFloatingPanelViewController+Actions.swift | 8 ++- ...leActionsFloatingPanelViewController.swift | 15 ++++- kDriveCore/Data/Api/Endpoint+Files.swift | 12 +++- kDriveCore/Data/Api/Endpoint+Share.swift | 4 +- .../DownloadQueue/DownloadOperation.swift | 60 ++++++++++++++++++- .../Data/DownloadQueue/DownloadQueue.swift | 35 +++++++++++ 7 files changed, 132 insertions(+), 11 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index ea8257905..e8e854b7c 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -107,8 +107,15 @@ class MultipleSelectionFileListViewModel { init(configuration: FileListViewModel.Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { isMultipleSelectionEnabled = false selectedCount = 0 - multipleSelectionActions = [.move, .delete, .more] + self.driveFileManager = driveFileManager + + if driveFileManager.isPublicShare { + multipleSelectionActions = [] + } else { + multipleSelectionActions = [.move, .delete, .more] + } + self.currentDirectory = currentDirectory self.configuration = configuration } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index bbcf13c7c..115c5ecc4 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -58,6 +58,11 @@ extension FileActionsFloatingPanelViewController { } private func setupActions() { + guard !driveFileManager.isPublicShare else { + actions = [] + return + } + actions = (file.isDirectory ? FloatingPanelAction.folderListActions : FloatingPanelAction.listActions).filter { action in switch action { case .openWith: @@ -175,7 +180,8 @@ extension FileActionsFloatingPanelViewController { if file.isMostRecentDownloaded { presentShareSheet(from: indexPath) } else { - downloadFile(action: action, indexPath: indexPath) { [weak self] in + downloadFile(action: action, + indexPath: indexPath) { [weak self] in self?.presentShareSheet(from: indexPath) } } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 4e1acdbb5..5bb72d9f0 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -388,7 +388,9 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { present(activityViewController, animated: true) } - func downloadFile(action: FloatingPanelAction, indexPath: IndexPath, completion: @escaping () -> Void) { + func downloadFile(action: FloatingPanelAction, + indexPath: IndexPath, + completion: @escaping () -> Void) { guard let observerViewController = UIApplication.shared.windows.first?.rootViewController else { return } downloadAction = action setLoading(true, action: action, at: indexPath) @@ -405,7 +407,16 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { } } } - DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + + if let publicShareProxy = driveFileManager.publicShareProxy { + DownloadQueue.instance.addPublicShareToQueue(file: file, + userId: accountManager.currentUserId, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } else { + DownloadQueue.instance.addToQueue(file: file, + userId: accountManager.currentUserId) + } } func copyShareLinkToPasteboard(from indexPath: IndexPath, link: String) { diff --git a/kDriveCore/Data/Api/Endpoint+Files.swift b/kDriveCore/Data/Api/Endpoint+Files.swift index 251ff0792..e7bd35552 100644 --- a/kDriveCore/Data/Api/Endpoint+Files.swift +++ b/kDriveCore/Data/Api/Endpoint+Files.swift @@ -180,14 +180,22 @@ public extension Endpoint { ]) } - static func download(file: AbstractFile, as asType: String? = nil) -> Endpoint { + static func download(file: AbstractFile, + publicShareProxy: PublicShareProxy? = nil, + as asType: String? = nil) -> Endpoint { let queryItems: [URLQueryItem]? if let asType { queryItems = [URLQueryItem(name: "as", value: asType)] } else { queryItems = nil } - return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + if let publicShareProxy { + return .downloadShareLinkFile(driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + fileId: file.id) + } else { + return .fileInfoV2(file).appending(path: "/download", queryItems: queryItems) + } } static func convert(file: AbstractFile) -> Endpoint { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 2ea731f11..2d3d01edd 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -75,14 +75,14 @@ public extension Endpoint { return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") } - /// Share mink file preview + /// Share link file preview static func shareLinkFilePreview(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/preview") } /// Download share link file static func downloadShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { - return shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") } func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift index 47f9a226e..2002c47cc 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift @@ -38,6 +38,7 @@ public class DownloadOperation: Operation, DownloadOperationable { private let fileManager = FileManager.default private let driveFileManager: DriveFileManager private let urlSession: FileDownloadSession + private let publicShareProxy: PublicShareProxy? private let itemIdentifier: NSFileProviderItemIdentifier? private var progressObservation: NSKeyValueObservation? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid @@ -93,19 +94,25 @@ public class DownloadOperation: Operation, DownloadOperationable { file: File, driveFileManager: DriveFileManager, urlSession: FileDownloadSession, + publicShareProxy: PublicShareProxy? = nil, itemIdentifier: NSFileProviderItemIdentifier? = nil ) { self.file = File(value: file) self.driveFileManager = driveFileManager self.urlSession = urlSession + self.publicShareProxy = publicShareProxy self.itemIdentifier = itemIdentifier } - public init(file: File, driveFileManager: DriveFileManager, task: URLSessionDownloadTask, urlSession: FileDownloadSession) { + public init(file: File, + driveFileManager: DriveFileManager, + task: URLSessionDownloadTask, + urlSession: FileDownloadSession) { self.file = file self.driveFileManager = driveFileManager self.urlSession = urlSession self.task = task + publicShareProxy = nil itemIdentifier = nil } @@ -170,6 +177,53 @@ public class DownloadOperation: Operation, DownloadOperationable { } override public func main() { + DDLogInfo("[DownloadOperation] Start for \(file.id) with session \(urlSession.identifier)") + + if let publicShareProxy { + downloadPublicShareFile(publicShareProxy: publicShareProxy) + } else { + downloadFile() + } + } + + private func downloadPublicShareFile(publicShareProxy: PublicShareProxy) { + DDLogInfo("[DownloadOperation] Downloading publicShare \(file.id) with session \(urlSession.identifier)") + + let url = Endpoint.download(file: file, publicShareProxy: publicShareProxy).url + + // Add download task to Realm + let downloadTask = DownloadTask( + fileId: file.id, + isDirectory: file.isDirectory, + driveId: file.driveId, + userId: driveFileManager.drive.userId, + sessionId: urlSession.identifier, + sessionUrl: url.absoluteString + ) + + try? uploadsDatabase.writeTransaction { writableRealm in + writableRealm.add(downloadTask, update: .modified) + } + + let request = URLRequest(url: url) + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: fileId) + } + if let itemIdentifier { + driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in + manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in + // META: keep SonarCloud happy + } + } + } + task?.resume() + } + + private func downloadFile() { DDLogInfo("[DownloadOperation] Downloading \(file.id) with session \(urlSession.identifier)") let url = Endpoint.download(file: file).url @@ -207,7 +261,7 @@ public class DownloadOperation: Operation, DownloadOperationable { } task?.resume() } else { - error = .localError // Other error? + error = .unknownToken // Other error? end(sessionUrl: url) } } @@ -288,7 +342,7 @@ public class DownloadOperation: Operation, DownloadOperationable { return } - assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation") + assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation error:\(error)") try? uploadsDatabase.writeTransaction { writableRealm in guard let task = writableRealm.objects(DownloadTask.self) diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index a0f7450b6..2b5dd97db 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -112,6 +112,41 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { // MARK: - Public methods + public func addPublicShareToQueue(file: File, + userId: Int, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + itemIdentifier: NSFileProviderItemIdentifier? = nil) { + Log.downloadQueue("addPublicShareToQueue file:\(file.id)") + let file = file.freezeIfNeeded() + + dispatchQueue.async { + guard !self.hasOperation(for: file.id) else { + Log.downloadQueue("Already in download queue, skipping \(file.id)", level: .error) + return + } + + OperationQueueHelper.disableIdleTimer(true) + + let operation = DownloadOperation( + file: file, + driveFileManager: driveFileManager, + urlSession: self.bestSession, + publicShareProxy: publicShareProxy, + itemIdentifier: itemIdentifier + ) + operation.completionBlock = { + self.dispatchQueue.async { + self.operationsInQueue.removeValue(forKey: file.id) + self.publishFileDownloaded(fileId: file.id, error: operation.error) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + } + } + self.operationQueue.addOperation(operation) + self.operationsInQueue[file.id] = operation + } + } + public func addToQueue(file: File, userId: Int, itemIdentifier: NSFileProviderItemIdentifier? = nil) { From f9c608b3c471d0c3ca061a5b06046c1d400612b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 20 Sep 2024 11:17:33 +0200 Subject: [PATCH 017/129] feat: Actions match spec on public share --- .../Create File/FloatingPanelLayouts.swift | 141 ++++++++++++++++++ .../Create File/FloatingPanelUtils.swift | 65 -------- .../File List/FileListViewController.swift | 31 +++- ...sFloatingPanelViewController+Actions.swift | 15 +- ...leActionsFloatingPanelViewController.swift | 8 + 5 files changed, 188 insertions(+), 72 deletions(-) create mode 100644 kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift diff --git a/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift new file mode 100644 index 000000000..1e1b5f92a --- /dev/null +++ b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift @@ -0,0 +1,141 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 FloatingPanel +import kDriveCore +import kDriveResources +import UIKit + +/// Layout used for a folder within a public share +class PublicShareFolderFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + let extendedAnchor = FloatingPanelLayoutAnchor( + absoluteInset: 140.0 + safeAreaInset, + edge: .bottom, + referenceGuide: .superview + ) + anchors = [ + .full: extendedAnchor, + .half: extendedAnchor, + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +/// Layout used for a file within a public share +class PublicShareFileFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + let extendedAnchor = FloatingPanelLayoutAnchor( + absoluteInset: 248.0 + safeAreaInset, + edge: .bottom, + referenceGuide: .superview + ) + anchors = [ + .full: extendedAnchor, + .half: extendedAnchor, + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +class FileFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var initialState: FloatingPanelState = .tip + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] + private var backdropAlpha: CGFloat + + init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { + self.initialState = initialState + self.backdropAlpha = backdropAlpha + if hideTip { + anchors = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea) + ] + } else { + anchors = [ + .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), + .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), + .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) + ] + } + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return backdropAlpha + } +} + +class PlusButtonFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + var height: CGFloat = 16 + + init(height: CGFloat) { + self.height = height + } + + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea) + ] + } + + var initialState: FloatingPanelState = .full + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return 0.2 + } +} + +class InformationViewFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + + var initialState: FloatingPanelState = .full + + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea) + ] + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return 0.3 + } +} diff --git a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift index 793edb132..e5ca7bc58 100644 --- a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift +++ b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift @@ -73,68 +73,3 @@ class AdaptiveDriveFloatingPanelController: DriveFloatingPanelController { track(scrollView: scrollView) } } - -class FileFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - var initialState: FloatingPanelState = .tip - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] - private var backdropAlpha: CGFloat - - init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) { - self.initialState = initialState - self.backdropAlpha = backdropAlpha - if hideTip { - anchors = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea) - ] - } else { - anchors = [ - .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea), - .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea), - .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview) - ] - } - } - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return backdropAlpha - } -} - -class PlusButtonFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - var height: CGFloat = 16 - - init(height: CGFloat) { - self.height = height - } - - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea) - ] - } - - var initialState: FloatingPanelState = .full - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return 0.2 - } -} - -class InformationViewFloatingPanelLayout: FloatingPanelLayout { - var position: FloatingPanelPosition = .bottom - - var initialState: FloatingPanelState = .full - - var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { - return [ - .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea) - ] - } - - func backdropAlpha(for state: FloatingPanelState) -> CGFloat { - return 0.3 - } -} diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 5ca4e7167..165d919d0 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -25,6 +25,7 @@ import kDriveCore import kDriveResources import RealmSwift import UIKit +import FloatingPanel extension SwipeCellAction { static let share = SwipeCellAction( @@ -348,6 +349,30 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + private func fileLayout(files: [File]) -> FloatingPanelLayout { + guard driveFileManager.isPublicShare else { + return FileFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } + + if files.first?.isDirectory ?? false { + return PublicShareFolderFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } else { + return PublicShareFileFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } + } + private func showQuickActionsPanel(files: [File], actionType: FileListQuickActionType) { #if !ISEXTENSION var floatingPanelViewController: DriveFloatingPanelController @@ -359,11 +384,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV fileInformationsViewController.presentingParent = self fileInformationsViewController.normalFolderHierarchy = viewModel.configuration.normalFolderHierarchy - floatingPanelViewController.layout = FileFloatingPanelLayout( - initialState: .half, - hideTip: true, - backdropAlpha: 0.2 - ) + floatingPanelViewController.layout = fileLayout(files: files) if let file = files.first { fileInformationsViewController.setFile(file, driveFileManager: driveFileManager) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index 115c5ecc4..4407c521c 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -36,7 +36,14 @@ extension FileActionsFloatingPanelViewController { private func setupQuickActions() { let offline = ReachabilityListener.instance.currentStatus == .offline - quickActions = file.isDirectory ? FloatingPanelAction.folderQuickActions : FloatingPanelAction.quickActions + if driveFileManager.isPublicShare { + quickActions = [] + } else if file.isDirectory { + quickActions = FloatingPanelAction.folderQuickActions + } else { + quickActions = FloatingPanelAction.quickActions + } + for action in quickActions { switch action { case .shareAndRights: @@ -59,7 +66,11 @@ extension FileActionsFloatingPanelViewController { private func setupActions() { guard !driveFileManager.isPublicShare else { - actions = [] + if file.isDirectory { + actions = FloatingPanelAction.publicShareFolderActions + } else { + actions = FloatingPanelAction.publicShareActions + } return } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 5bb72d9f0..b4f2643c0 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -205,6 +205,14 @@ public class FloatingPanelAction: Equatable { return [informations, add, shareAndRights, shareLink].map { $0.reset() } } + static var publicShareActions: [FloatingPanelAction] { + return [openWith, sendCopy, download].map { $0.reset() } + } + + static var publicShareFolderActions: [FloatingPanelAction] { + return [download].map { $0.reset() } + } + static var multipleSelectionActions: [FloatingPanelAction] { return [manageCategories, favorite, offline, download, move, duplicate].map { $0.reset() } } From 827770293e30f79de342e5f2953c6396ef4876b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 23 Sep 2024 08:03:14 +0200 Subject: [PATCH 018/129] fix: Tuist config update to build --- Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift index e980f412f..29445bbf3 100644 --- a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift +++ b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift @@ -68,6 +68,7 @@ public extension Target { "kDrive/UI/Controller/DriveUpdateRequiredViewController.swift", "kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift", "kDrive/UI/Controller/Create File/FloatingPanelUtils.swift", + "kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift", "kDrive/UI/Controller/Files/Categories/**", "kDrive/UI/Controller/Files/Rights and Share/**", "kDrive/UI/Controller/Files/Save File/**", From fda286c4d110f1a05e045890176105a85f3a4fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 23 Sep 2024 15:14:19 +0200 Subject: [PATCH 019/129] feat: Add to my drive button --- kDrive/AppDelegate.swift | 12 --- kDrive/AppRouter.swift | 7 +- kDrive/SceneDelegate.swift | 1 + .../File List/FileListViewController.swift | 78 ++++++++++++++++++- 4 files changed, 82 insertions(+), 16 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index e366d0867..b61288e25 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,18 +103,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() - // swiftlint:disable force_try - Task { - try! await Task.sleep(nanoseconds:5_000_000_000) - print("coucou") - let somePublicShare = URL(string: "") - //await UIApplication.shared.open(somePublicShare!) // opens safari - - let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) - await UniversalLinksHelper.handlePath(components!.path) - } - - return true } diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 66a8b000f..5bef6867c 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -591,7 +591,6 @@ public struct AppRouter: AppNavigable { driveFileManager: DriveFileManager, apiFetcher: PublicShareApiFetcher ) { - // TODO: Present on top of existing views guard let window, let rootViewController = window.rootViewController else { fatalError("TODO: lazy load a rootViewController") @@ -621,9 +620,11 @@ public struct AppRouter: AppNavigable { currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) let viewController = FileListViewController(viewModel: viewModel) - print("viewController:\(viewController) viewModel:\(viewModel) navigationController:\(navigationController)") + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical - navigationController.pushViewController(viewController, animated: true) + navigationController.present(publicShareNavigationController, animated: true, completion: nil) } } diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index 317186adf..25a69a2c9 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -234,6 +234,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { + Log.sceneDelegate("scene continue userActivity - unable", level: .error) return } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 165d919d0..0eb2516c2 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -19,13 +19,13 @@ import CocoaLumberjackSwift import Combine import DifferenceKit +import FloatingPanel import InfomaniakCore import InfomaniakDI import kDriveCore import kDriveResources import RealmSwift import UIKit -import FloatingPanel extension SwipeCellAction { static let share = SwipeCellAction( @@ -142,6 +142,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ) setupViewModel() + setupFooterIfNeeded() } override func viewWillAppear(_ animated: Bool) { @@ -251,6 +252,46 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + func setupFooterIfNeeded() { + guard driveFileManager.isPublicShare else { + return + } + + let addToKDriveButton = IKButton(type: .custom) + addToKDriveButton.setTitle("Add to My Drive", for: .normal) + addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside) + addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) + addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false + addToKDriveButton.cornerRadius = 8.0 + addToKDriveButton.clipsToBounds = true + + view.addSubview(addToKDriveButton) + view.bringSubviewToFront(addToKDriveButton) + + let leadingConstraint = addToKDriveButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, + constant: 16) + leadingConstraint.priority = .defaultHigh + let trailingConstraint = addToKDriveButton.trailingAnchor.constraint( + greaterThanOrEqualTo: view.trailingAnchor, + constant: -16 + ) + trailingConstraint.priority = .defaultHigh + let widthConstraint = addToKDriveButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360) + + NSLayoutConstraint.activate([ + addToKDriveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + leadingConstraint, + trailingConstraint, + addToKDriveButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + addToKDriveButton.heightAnchor.constraint(equalToConstant: 60), + widthConstraint + ]) + } + + @objc func addToMyDriveButtonTapped() { + print("button tapped") + } + func reloadCollectionViewWith(files: [File]) { let changeSet = StagedChangeset(source: displayedFiles, target: files) collectionView.reload(using: changeSet, @@ -917,3 +958,38 @@ extension FileListViewController: UICollectionViewDropDelegate { } } } + +// Move to CoreUIKit or use something else ? +extension UIImage { + convenience init?(color: UIColor) { + let size = CGSize(width: 1, height: 1) + UIGraphicsBeginImageContext(size) + guard let context = UIGraphicsGetCurrentContext() else { + return nil + } + + context.setFillColor(color.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + guard let cgImage = image.cgImage else { + return nil + } + + self.init(cgImage: cgImage) + } +} + +// Move to CoreUIKit or use something else ? +extension IKButton { + func setBackgroundColors(normal normalColor: UIColor, highlighted highlightedColor: UIColor) { + if let normalImage = UIImage(color: normalColor) { + setBackgroundImage(normalImage, for: .normal) + } + + if let highlightedImage = UIImage(color: highlightedColor) { + setBackgroundImage(highlightedImage, for: .highlighted) + } + } +} From 3ba0f11c7f7d0d28d48384bdc093e218ac5dd262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 24 Sep 2024 18:06:56 +0200 Subject: [PATCH 020/129] chore: Sample code to open public share --- kDrive/AppDelegate.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index b61288e25..3b9c5ef2d 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,6 +103,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() + // swiftlint:disable force_try + Task { + try! await Task.sleep(nanoseconds:5_000_000_000) + print("coucou") + let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") + //await UIApplication.shared.open(somePublicShare!) // opens safari + + let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) + await UniversalLinksHelper.handlePath(components!.path) + } + return true } From bc7b593320ad4ebba7c2209ceadf2ddd9c63d843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 25 Sep 2024 14:45:42 +0200 Subject: [PATCH 021/129] chore: Matomo for public share tasks --- ...leActionsFloatingPanelViewController.swift | 12 +++++++- ...SelectionFloatingPanelViewController.swift | 12 +++++++- kDrive/Utils/MatomoUtils+UI.swift | 6 ++-- kDrive/Utils/UniversalLinksHelper.swift | 10 +++++++ kDriveCore/Data/Api/DriveApiFetcher.swift | 4 +++ kDriveCore/Utils/DeeplinkParser.swift | 3 ++ kDriveCore/Utils/MatomoUtils.swift | 29 +++++++++++++++++-- 7 files changed, 68 insertions(+), 8 deletions(-) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index b4f2643c0..117f817b2 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -496,7 +496,17 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { case .actions: action = actions[indexPath.item] } - MatomoUtils.trackFileAction(action: action, file: file, fromPhotoList: presentingParent is PhotoListViewController) + + let eventCategory: MatomoUtils.EventCategory + if presentingParent is PhotoListViewController { + eventCategory = .picturesFileAction + } else if driveFileManager.isPublicShare { + eventCategory = .publicShareAction + } else { + eventCategory = .fileListFileAction + } + + MatomoUtils.trackFileAction(action: action, file: file, category: eventCategory) handleAction(action, at: indexPath) } } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index a22f6035a..a17b0aa76 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -209,6 +209,16 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let action = actions[indexPath.item] handleAction(action, at: indexPath) - MatomoUtils.trackBuklAction(action: action, files: files, fromPhotoList: presentingParent is PhotoListViewController) + + let eventCategory: MatomoUtils.EventCategory + if presentingParent is PhotoListViewController { + eventCategory = .picturesFileAction + } else if driveFileManager.isPublicShare { + eventCategory = .publicShareAction + } else { + eventCategory = .fileListFileAction + } + + MatomoUtils.trackBuklAction(action: action, files: files, category: eventCategory) } } diff --git a/kDrive/Utils/MatomoUtils+UI.swift b/kDrive/Utils/MatomoUtils+UI.swift index e2a828f5c..88082c8ee 100644 --- a/kDrive/Utils/MatomoUtils+UI.swift +++ b/kDrive/Utils/MatomoUtils+UI.swift @@ -45,8 +45,7 @@ extension MatomoUtils { #if !ISEXTENSION - static func trackFileAction(action: FloatingPanelAction, file: File, fromPhotoList: Bool) { - let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction + static func trackFileAction(action: FloatingPanelAction, file: File, category: EventCategory) { switch action { // Quick Actions case .sendCopy: @@ -77,9 +76,8 @@ extension MatomoUtils { } } - static func trackBuklAction(action: FloatingPanelAction, files: [File], fromPhotoList: Bool) { + static func trackBuklAction(action: FloatingPanelAction, files: [File], category: EventCategory) { let numberOfFiles = files.count - let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction switch action { // Quick Actions case .duplicate: diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 38c72c2cf..10718c026 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -101,6 +101,16 @@ enum UniversalLinksHelper { return false } + let trackerName: String + if metadata.isPasswordNeeded { + trackerName = "publicShareWithPassword" + } else if metadata.isExpired { + trackerName = "publicShareExpired" + } else { + trackerName = "publicShare" + } + MatomoUtils.trackDeeplink(name: trackerName) + // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( for: shareLinkUid, diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 108cebef3..ed27a7d51 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -59,6 +59,10 @@ public struct PublicShareMetadata: Decodable { public let validUntil: TimeInterval? public let capabilities: Rights + + // TODO: Test parsing + public let isPasswordNeeded: Bool = false + public let isExpired: Bool = false public let createdBy: TimeInterval public let createdAt: TimeInterval diff --git a/kDriveCore/Utils/DeeplinkParser.swift b/kDriveCore/Utils/DeeplinkParser.swift index 1af20238b..80ab6d180 100644 --- a/kDriveCore/Utils/DeeplinkParser.swift +++ b/kDriveCore/Utils/DeeplinkParser.swift @@ -17,6 +17,7 @@ */ import InfomaniakDI +import MatomoTracker import SwiftUI /// Deeplink entrypoint @@ -50,6 +51,7 @@ public struct DeeplinkParser: DeeplinkParsable { let driveId = params.first(where: { $0.name == "driveId" })?.value, let driveIdInt = Int(driveId), let userIdInt = Int(userId) { await router.navigate(to: .store(driveId: driveIdInt, userId: userIdInt)) + MatomoUtils.trackDeeplink(name: DeeplinkPath.store.rawValue) return true } else if components.host == DeeplinkPath.file.rawValue, @@ -57,6 +59,7 @@ public struct DeeplinkParser: DeeplinkParsable { let fileUrl = URL(fileURLWithPath: filePath) let file = ImportedFile(name: fileUrl.lastPathComponent, path: fileUrl, uti: fileUrl.uti ?? .data) await router.navigate(to: .saveFile(file: file)) + MatomoUtils.trackDeeplink(name: DeeplinkPath.file.rawValue) return true } diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index c63b72b29..444fb4e49 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -44,7 +44,7 @@ public enum MatomoUtils { public enum EventCategory: String { case newElement, fileListFileAction, picturesFileAction, fileInfo, shareAndRights, colorFolder, categories, search, fileList, comment, drive, account, settings, photoSync, home, displayList, inApp, trash, - dropbox, preview, mediaPlayer, shortcuts, appReview + dropbox, preview, mediaPlayer, shortcuts, appReview, deeplink, publicShareAction, publicSharePasswordAction } public enum UserAction: String { @@ -64,7 +64,12 @@ public enum MatomoUtils { shared.track(view: view) } - public static func track(eventWithCategory category: EventCategory, action: UserAction = .click, name: String, value: Float? = nil) { + public static func track( + eventWithCategory category: EventCategory, + action: UserAction = .click, + name: String, + value: Float? = nil + ) { shared.track(eventWithCategory: category.rawValue, action: action.rawValue, name: name, value: value) } @@ -122,4 +127,24 @@ public enum MatomoUtils { public static func trackMediaPlayer(leaveAt percentage: Double?) { track(eventWithCategory: .mediaPlayer, name: "duration", value: Float(percentage ?? 0)) } + + // MARK: - Deeplink + + public static func trackDeeplink(name: String) { + track(eventWithCategory: .deeplink, name: name) + } + + // MARK: - Public Share + + public static func trackAddToMykDrive() { + track(eventWithCategory: .publicShareAction, name: "addToMykDrive") + } + + public static func trackAddBulkToMykDrive() { + track(eventWithCategory: .publicShareAction, name: "bulkaddToMykDrive") + } + + public static func trackPublicSharePasswordAction() { + track(eventWithCategory: .publicSharePasswordAction, name: "openInBrowser") + } } From 6ef3eda92620a3ad34d1b10f290e1458223cb062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 26 Sep 2024 10:22:35 +0200 Subject: [PATCH 022/129] chore: Align Matomo with android on what exists --- kDriveCore/Utils/MatomoUtils.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index 444fb4e49..1d0c46478 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -137,11 +137,11 @@ public enum MatomoUtils { // MARK: - Public Share public static func trackAddToMykDrive() { - track(eventWithCategory: .publicShareAction, name: "addToMykDrive") + track(eventWithCategory: .publicShareAction, name: "saveToKDrive") } public static func trackAddBulkToMykDrive() { - track(eventWithCategory: .publicShareAction, name: "bulkaddToMykDrive") + track(eventWithCategory: .publicShareAction, name: "bulkSaveToKDrive") } public static func trackPublicSharePasswordAction() { From d0257593103b9e79a48c9a916c536a1f50ce6abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 26 Sep 2024 16:32:28 +0200 Subject: [PATCH 023/129] feat: Download all files from current folder as a ZIP within a public share --- .../File List/ConcreteFileListViewModel.swift | 4 +- .../File List/FileListViewController.swift | 6 +- .../Files/File List/FileListViewModel.swift | 3 +- ...leActionsFloatingPanelViewController.swift | 1 - .../Files/Search/SearchFilesViewModel.swift | 19 +++--- .../Menu/PhotoList/PhotoListViewModel.swift | 4 +- .../Menu/Share/PublicShareViewModel.swift | 59 +++++++++++++++++++ .../Menu/Trash/TrashListViewModel.swift | 4 +- kDrive/UI/View/Files/FileListBarButton.swift | 6 +- .../Data/DownloadQueue/DownloadQueue.swift | 1 - kDriveCore/Data/Models/File.swift | 3 +- 11 files changed, 87 insertions(+), 23 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift index 701e596fe..a5bd73668 100644 --- a/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/ConcreteFileListViewModel.swift @@ -59,13 +59,13 @@ class ConcreteFileListViewModel: FileListViewModel { try await loadFiles() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .search { let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager) let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel) onPresentViewController?(.modal, searchViewController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 0eb2516c2..c9c8aca7a 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -258,7 +258,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } let addToKDriveButton = IKButton(type: .custom) - addToKDriveButton.setTitle("Add to My Drive", for: .normal) + addToKDriveButton.setTitle("Add to My kDrive", for: .normal) addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside) addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false @@ -289,7 +289,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } @objc func addToMyDriveButtonTapped() { - print("button tapped") + print("TODO: addToMyDriveButtonTapped") } func reloadCollectionViewWith(files: [File]) { @@ -527,7 +527,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } @objc func barButtonPressed(_ sender: FileListBarButton) { - viewModel.barButtonPressed(type: sender.type) + viewModel.barButtonPressed(sender: sender, type: sender.type) } @objc func forceRefresh() { diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index f43164e12..cf1db00fc 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -34,6 +34,7 @@ enum FileListBarButtonType { case searchFilters case photoSort case addFolder + case downloadAll } enum FileListQuickActionType { @@ -279,7 +280,7 @@ class FileListViewModel: SelectDelegate { }.store(in: &bindStore) } - func barButtonPressed(type: FileListBarButtonType) { + func barButtonPressed(sender: Any? = nil, type: FileListBarButtonType) { if multipleSelectionViewModel?.isMultipleSelectionEnabled == true { multipleSelectionViewModel?.barButtonPressed(type: type) } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 117f817b2..d462c2d3d 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -418,7 +418,6 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { if let publicShareProxy = driveFileManager.publicShareProxy { DownloadQueue.instance.addPublicShareToQueue(file: file, - userId: accountManager.currentUserId, driveFileManager: driveFileManager, publicShareProxy: publicShareProxy) } else { diff --git a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift index 99f975988..32d1acab3 100644 --- a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift +++ b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift @@ -84,7 +84,8 @@ class SearchFilesViewModel: FileListViewModel { filters = Filters() let searchFakeRoot = driveFileManager.getManagedFile(from: DriveFileManager.searchFilesRootFile) super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: searchFakeRoot) - observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children).sorted(by: [sortType.value.sortDescriptor])) + observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children) + .sorted(by: [sortType.value.sortDescriptor])) } override func startObservation() { @@ -145,16 +146,16 @@ class SearchFilesViewModel: FileListViewModel { private func searchOffline() { observedFiles = AnyRealmCollection(driveFileManager.searchOffline(query: currentSearchText, - date: filters.date?.dateInterval, - fileType: filters.fileType, - categories: Array(filters.categories), - fileExtensions: filters.fileExtensions, - belongToAllCategories: filters.belongToAllCategories, - sortType: sortType)) + date: filters.date?.dateInterval, + fileType: filters.fileType, + categories: Array(filters.categories), + fileExtensions: filters.fileExtensions, + belongToAllCategories: filters.belongToAllCategories, + sortType: sortType)) startObservation() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .searchFilters { let navigationController = SearchFiltersViewController .instantiateInNavigationController(driveFileManager: driveFileManager) @@ -163,7 +164,7 @@ class SearchFilesViewModel: FileListViewModel { searchFiltersViewController?.delegate = self onPresentViewController?(.modal, navigationController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift index 72d46e53e..d6378a65e 100644 --- a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift +++ b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift @@ -144,7 +144,7 @@ class PhotoListViewModel: FileListViewModel { self.nextCursor = nextCursor } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .search { let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager, filters: Filters(fileType: .image)) let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel) @@ -156,7 +156,7 @@ class PhotoListViewModel: FileListViewModel { delegate: self) onPresentViewController?(.modal, floatingPanelViewController, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 0898ce34d..69273f79f 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -16,12 +16,16 @@ along with this program. If not, see . */ +import InfomaniakCore +import InfomaniakDI import kDriveCore import RealmSwift import UIKit /// Public share view model, loading content from memory realm final class PublicShareViewModel: InMemoryFileListViewModel { + private var downloadObserver: ObservationToken? + var publicShareProxy: PublicShareProxy? let rootProxy: ProxyFile var publicShareApiFetcher: PublicShareApiFetcher? @@ -36,6 +40,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { rootTitle: "public share", emptyViewType: .emptyFolder, supportsDrop: false, + rightBarButtons: [.downloadAll], matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) rootProxy = currentDirectory.proxify() @@ -79,4 +84,58 @@ final class PublicShareViewModel: InMemoryFileListViewModel { try await loadFiles(cursor: nextCursor) } } + + // TODO: Move away from view model + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { + guard downloadObserver == nil else { + return + } + + guard type == .downloadAll, + let publicShareProxy = publicShareProxy else { + return + } + + // TODO: Abstract sheet presentation + @InjectService var appNavigable: AppNavigable + guard let topMostViewController = appNavigable.topMostViewController else { + return + } + + downloadObserver = DownloadQueue.instance + .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in + Task { @MainActor in + guard let self = self else { + return + } + + defer { + self.downloadObserver?.cancel() + self.downloadObserver = nil + } + + guard let senderItem = sender as? UIBarButtonItem else { + return + } + + guard error == nil else { + UIConstants.showSnackBarIfNeeded(error: DriveError.downloadFailed) + return + } + + // present share sheet + let activityViewController = UIActivityViewController( + activityItems: [self.currentDirectory.localUrl], + applicationActivities: nil + ) + + activityViewController.popoverPresentationController?.barButtonItem = senderItem + topMostViewController.present(activityViewController, animated: true) + } + } + + DownloadQueue.instance.addPublicShareToQueue(file: currentDirectory, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } } diff --git a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift index 67ab3d778..e77094413 100644 --- a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift +++ b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift @@ -92,7 +92,7 @@ class TrashListViewModel: InMemoryFileListViewModel { forceRefresh() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .emptyTrash { let alert = AlertTextViewController(title: KDriveResourcesStrings.Localizable.modalEmptyTrashTitle, message: KDriveResourcesStrings.Localizable.modalEmptyTrashDescription, @@ -103,7 +103,7 @@ class TrashListViewModel: InMemoryFileListViewModel { } onPresentViewController?(.modal, alert, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index c55363306..62984460f 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -17,8 +17,8 @@ */ import Foundation -import UIKit import kDriveResources +import UIKit final class FileListBarButton: UIBarButtonItem { private(set) var type: FileListBarButtonType = .cancel @@ -49,6 +49,10 @@ final class FileListBarButton: UIBarButtonItem { case .addFolder: self.init(image: KDriveResourcesAsset.folderAdd.image, style: .plain, target: target, action: action) accessibilityLabel = KDriveResourcesStrings.Localizable.createFolderTitle + case .downloadAll: + let image = KDriveResourcesAsset.download.image + self.init(image: image, style: .plain, target: target, action: action) + accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload } self.type = type } diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 2b5dd97db..db2eaf452 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -113,7 +113,6 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { // MARK: - Public methods public func addPublicShareToQueue(file: File, - userId: Int, driveFileManager: DriveFileManager, publicShareProxy: PublicShareProxy, itemIdentifier: NSFileProviderItemIdentifier? = nil) { diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index e4625c92d..79c36649d 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -554,7 +554,8 @@ public final class File: Object, Codable { public var isDownloaded: Bool { let localPath = localUrl.path - guard fileManager.fileExists(atPath: localPath) else { + let temporaryPath = temporaryUrl.path + guard fileManager.fileExists(atPath: localPath) || fileManager.fileExists(atPath: temporaryPath) else { DDLogError("[File] no local copy to read from") return false } From 630111fdc76f8c070d55def9835a550ef0ebdf37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 26 Sep 2024 17:20:31 +0200 Subject: [PATCH 024/129] feat: Add to my kDrive button --- .../File List/FileListViewController.swift | 6 +++--- .../Menu/Share/PublicShareViewModel.swift | 20 ++++++++++++++----- kDriveCore/Data/Models/File.swift | 19 ++++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index c9c8aca7a..e1a604cdf 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -259,7 +259,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV let addToKDriveButton = IKButton(type: .custom) addToKDriveButton.setTitle("Add to My kDrive", for: .normal) - addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside) + addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped(_:)), for: .touchUpInside) addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false addToKDriveButton.cornerRadius = 8.0 @@ -288,8 +288,8 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ]) } - @objc func addToMyDriveButtonTapped() { - print("TODO: addToMyDriveButtonTapped") + @objc func addToMyDriveButtonTapped(_ sender: UIView?) { + viewModel.barButtonPressed(sender: sender, type: .downloadAll) } func reloadCollectionViewWith(files: [File]) { diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 69273f79f..1a6818b52 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -96,6 +96,9 @@ final class PublicShareViewModel: InMemoryFileListViewModel { return } + let button = sender as? UIButton + button?.isEnabled = false + // TODO: Abstract sheet presentation @InjectService var appNavigable: AppNavigable guard let topMostViewController = appNavigable.topMostViewController else { @@ -105,6 +108,10 @@ final class PublicShareViewModel: InMemoryFileListViewModel { downloadObserver = DownloadQueue.instance .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in Task { @MainActor in + defer { + button?.isEnabled = true + } + guard let self = self else { return } @@ -114,10 +121,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { self.downloadObserver = nil } - guard let senderItem = sender as? UIBarButtonItem else { - return - } - guard error == nil else { UIConstants.showSnackBarIfNeeded(error: DriveError.downloadFailed) return @@ -129,7 +132,14 @@ final class PublicShareViewModel: InMemoryFileListViewModel { applicationActivities: nil ) - activityViewController.popoverPresentationController?.barButtonItem = senderItem + if let senderItem = sender as? UIBarButtonItem { + activityViewController.popoverPresentationController?.barButtonItem = senderItem + } else if let button = button { + activityViewController.popoverPresentationController?.sourceRect = button.frame + } else { + fatalError("No sender button") + } + topMostViewController.present(activityViewController, animated: true) } } diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index 79c36649d..6a9a3c7f5 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -555,14 +555,29 @@ public final class File: Object, Codable { public var isDownloaded: Bool { let localPath = localUrl.path let temporaryPath = temporaryUrl.path - guard fileManager.fileExists(atPath: localPath) || fileManager.fileExists(atPath: temporaryPath) else { + + let pathToUse: String + if fileManager.fileExists(atPath: localPath) { + pathToUse = localPath + } else if fileManager.fileExists(atPath: temporaryPath) { + pathToUse = temporaryPath + } else { DDLogError("[File] no local copy to read from") return false } + return isDownloaded(atPath: pathToUse) + } + + private func isDownloaded(atPath path: String) -> Bool { + // Skip metadata validation for a zipped folder on local storage + guard !isDirectory else { + return true + } + // Check that size on disk matches, if available do { - let attributes = try fileManager.attributesOfItem(atPath: localPath) + let attributes = try fileManager.attributesOfItem(atPath: path) if let remoteSize = size, let metadataSize = attributes[FileAttributeKey.size] as? NSNumber, metadataSize.intValue != remoteSize { From 5368da7df7247295a18d14e38d66df35d5599eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 27 Sep 2024 15:15:42 +0200 Subject: [PATCH 025/129] feat: Multiselect behaviour --- kDrive/AppRouter.swift | 17 +++++++- .../File List/FileListViewController.swift | 39 ++++++++++-------- .../Files/File List/FileListViewModel.swift | 3 ++ .../MultipleSelectionFileListViewModel.swift | 2 + .../UI/Controller/Files/FilePresenter.swift | 14 ++++++- ...atingPanelSelectOptionViewController.swift | 6 +++ .../Menu/Share/PublicShareViewModel.swift | 41 +++++++++++-------- 7 files changed, 85 insertions(+), 37 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 5bef6867c..1fda140e0 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -614,12 +614,27 @@ public struct AppRouter: AppNavigable { return } + // TODO: i18n + let configuration = FileListViewModel.Configuration(selectAllSupported: true, + rootTitle: "public share", + emptyViewType: .emptyFolder, + supportsDrop: false, + leftBarButtons: [.cancel], + rightBarButtons: [.downloadAll], + matomoViewPath: [ + MatomoUtils.Views.menu.displayName, + "publicShare" + ]) + let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, sortType: .nameAZ, driveFileManager: driveFileManager, currentDirectory: frozenRootFolder, - apiFetcher: apiFetcher) + apiFetcher: apiFetcher, + configuration: configuration) let viewController = FileListViewController(viewModel: viewModel) + viewModel.viewControllerDismissable = viewController + let publicShareNavigationController = UINavigationController(rootViewController: viewController) publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index e1a604cdf..4781d7566 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -49,7 +49,7 @@ extension SortType: Selectable { } class FileListViewController: UICollectionViewController, SwipeActionCollectionViewDelegate, - SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable { + SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable, ViewControllerDismissable { @LazyInjectService var accountManager: AccountManageable // MARK: - Constants @@ -60,6 +60,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV private let leftRightInset = 12.0 private let gridInnerSpacing = 16.0 private let headerViewIdentifier = "FilesHeaderView" + private var addToKDriveButton: IKButton? // MARK: - Properties @@ -257,33 +258,35 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV return } - let addToKDriveButton = IKButton(type: .custom) - addToKDriveButton.setTitle("Add to My kDrive", for: .normal) - addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped(_:)), for: .touchUpInside) - addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) - addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false - addToKDriveButton.cornerRadius = 8.0 - addToKDriveButton.clipsToBounds = true + let addToKDrive = IKButton(type: .custom) + addToKDriveButton = addToKDrive - view.addSubview(addToKDriveButton) - view.bringSubviewToFront(addToKDriveButton) + addToKDrive.setTitle("Add to My kDrive", for: .normal) + addToKDrive.addTarget(self, action: #selector(addToMyDriveButtonTapped(_:)), for: .touchUpInside) + addToKDrive.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) + addToKDrive.translatesAutoresizingMaskIntoConstraints = false + addToKDrive.cornerRadius = 8.0 + addToKDrive.clipsToBounds = true - let leadingConstraint = addToKDriveButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, - constant: 16) + view.addSubview(addToKDrive) + view.bringSubviewToFront(addToKDrive) + + let leadingConstraint = addToKDrive.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, + constant: 16) leadingConstraint.priority = .defaultHigh - let trailingConstraint = addToKDriveButton.trailingAnchor.constraint( + let trailingConstraint = addToKDrive.trailingAnchor.constraint( greaterThanOrEqualTo: view.trailingAnchor, constant: -16 ) trailingConstraint.priority = .defaultHigh - let widthConstraint = addToKDriveButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360) + let widthConstraint = addToKDrive.widthAnchor.constraint(lessThanOrEqualToConstant: 360) NSLayoutConstraint.activate([ - addToKDriveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + addToKDrive.centerXAnchor.constraint(equalTo: view.centerXAnchor), leadingConstraint, trailingConstraint, - addToKDriveButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), - addToKDriveButton.heightAnchor.constraint(equalToConstant: 60), + addToKDrive.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + addToKDrive.heightAnchor.constraint(equalToConstant: 60), widthConstraint ]) } @@ -614,6 +617,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV func toggleMultipleSelection(_ on: Bool) { if on { + addToKDriveButton?.isHidden = true navigationItem.title = nil headerView?.selectView.isHidden = false headerView?.selectView.setActions(viewModel.multipleSelectionViewModel?.multipleSelectionActions ?? []) @@ -623,6 +627,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV generator.prepare() generator.impactOccurred() } else { + addToKDriveButton?.isHidden = false headerView?.selectView.isHidden = true collectionView.allowsMultipleSelection = false navigationController?.navigationBar.prefersLargeTitles = true diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index cf1db00fc..20220be63 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -93,6 +93,9 @@ class FileListViewModel: SelectDelegate { var matomoViewPath = ["FileList"] } + /// Tracking a way to dismiss the current stack + weak var viewControllerDismissable: ViewControllerDismissable? + var realmObservationToken: NotificationToken? var currentDirectoryObservationToken: NotificationToken? diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index e8e854b7c..65ee438b3 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -67,6 +67,8 @@ class MultipleSelectionFileListViewModel { leftBarButtons = [.cancel] if configuration.selectAllSupported { rightBarButtons = [.selectAll] + } else { + rightBarButtons = [] } } else { leftBarButtons = nil diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index be01bcbeb..820812773 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -146,11 +146,23 @@ final class FilePresenter { if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else if let publicShareProxy = driveFileManager.publicShareProxy { + // TODO: i18n + let configuration = FileListViewModel.Configuration(selectAllSupported: true, + rootTitle: "public share", + emptyViewType: .emptyFolder, + supportsDrop: false, + rightBarButtons: [.downloadAll], + matomoViewPath: [ + MatomoUtils.Views.menu.displayName, + "publicShare" + ]) + viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, sortType: .nameAZ, driveFileManager: driveFileManager, currentDirectory: file, - apiFetcher: PublicShareApiFetcher()) + apiFetcher: PublicShareApiFetcher(), + configuration: configuration) } else if file.isTrashed || file.deletedAt != nil { viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else { diff --git a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift index 88bcb3c7b..1097e3a76 100644 --- a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift +++ b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift @@ -37,6 +37,12 @@ protocol SelectDelegate: AnyObject { func didSelect(option: Selectable) } +/// Something that can dismiss the current VC if presented +@MainActor +public protocol ViewControllerDismissable: AnyObject { + func dismiss(animated flag: Bool, completion: (() -> Void)?) +} + class FloatingPanelSelectOptionViewController: UITableViewController { var headerTitle = "" var selectedOption: T? diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 1a6818b52..4d50ce094 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -30,32 +30,26 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let rootProxy: ProxyFile var publicShareApiFetcher: PublicShareApiFetcher? - required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { - guard let currentDirectory else { - fatalError("PublicShareViewModel requires a currentDirectory to work") - } - - // TODO: i18n - let configuration = Configuration(selectAllSupported: false, - rootTitle: "public share", - emptyViewType: .emptyFolder, - supportsDrop: false, - rightBarButtons: [.downloadAll], - matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) - + override init(configuration: Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { rootProxy = currentDirectory.proxify() super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) observedFiles = AnyRealmCollection(currentDirectory.children) } + required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) { + fatalError("unsupported initializer") + } + convenience init( publicShareProxy: PublicShareProxy, sortType: SortType, driveFileManager: DriveFileManager, currentDirectory: File, - apiFetcher: PublicShareApiFetcher + apiFetcher: PublicShareApiFetcher, + configuration: Configuration ) { - self.init(driveFileManager: driveFileManager, currentDirectory: currentDirectory) + self.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) + self.publicShareProxy = publicShareProxy self.sortType = sortType publicShareApiFetcher = apiFetcher @@ -85,14 +79,25 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } } - // TODO: Move away from view model override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { + guard type == .downloadAll else { + // We try to close the "Public Share screen" + if type == .cancel, + !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true), + let viewControllerDismissable = viewControllerDismissable { + viewControllerDismissable.dismiss(animated: true, completion: nil) + return + } + + super.barButtonPressed(sender: sender, type: type) + return + } + guard downloadObserver == nil else { return } - guard type == .downloadAll, - let publicShareProxy = publicShareProxy else { + guard let publicShareProxy = publicShareProxy else { return } From 245e8bf934c0d52e8877db98959dd4a4ba71aac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 07:39:50 +0200 Subject: [PATCH 026/129] chore: PR Feedback --- kDrive/AppRouter.swift | 2 -- kDrive/UI/Controller/Files/File List/FileListViewModel.swift | 4 +--- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 3 +-- kDrive/UI/View/Files/FileCollectionViewCell.swift | 2 +- kDriveCore/Data/Api/Endpoint+Share.swift | 1 - kDriveCore/Data/Cache/AccountManager.swift | 4 ++-- 6 files changed, 5 insertions(+), 11 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 55a7c5e63..acfa150fc 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -621,8 +621,6 @@ public struct AppRouter: AppNavigable { currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) let viewController = FileListViewController(viewModel: viewModel) - print("viewController:\(viewController) viewModel:\(viewModel) navigationController:\(navigationController)") - navigationController.pushViewController(viewController, animated: true) } } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index f43164e12..c8f9031a4 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -342,9 +342,7 @@ class FileListViewModel: SelectDelegate { } func didSelectFile(at indexPath: IndexPath) { - guard let file: File = getFile(at: indexPath) else { - return - } + guard let file: File = getFile(at: indexPath) else { return } if ReachabilityListener.instance.currentStatus == .offline && !file.isDirectory && !file.isAvailableOffline { return diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 0898ce34d..19997a86f 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -31,9 +31,8 @@ final class PublicShareViewModel: InMemoryFileListViewModel { fatalError("PublicShareViewModel requires a currentDirectory to work") } - // TODO: i18n let configuration = Configuration(selectAllSupported: false, - rootTitle: "public share", + rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, emptyViewType: .emptyFolder, supportsDrop: false, matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"]) diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 09e0b416b..5b74cdc94 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -156,7 +156,7 @@ protocol FileCellDelegate: AnyObject { imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color - if let publicShareProxy = publicShareProxy { + if let publicShareProxy { // Fetch public share thumbnail thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, publicDriveId: publicShareProxy.driveId, diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 2ea731f11..511d4b15e 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -54,7 +54,6 @@ public extension Endpoint { shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } - /// Some legacy calls like thumbnails require a V2 call static func shareLinkFileV2(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") } diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 251d676a3..9dee5130e 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -220,13 +220,13 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } catch { fatalError("unable to update public share drive in base, \(error)") } - let forzenPublicShareDrive = publicShareDrive.freeze() + let frozenPublicShareDrive = publicShareDrive.freeze() let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId) let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy) - return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context) + return DriveFileManager(drive: frozenPublicShareDrive, apiFetcher: apiFetcher, context: context) } public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { From fbf4989f0bcf5c8798cfa218082dedc4b5390c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 08:00:54 +0200 Subject: [PATCH 027/129] chore: PR Feedback --- .../Files/File List/FileListViewModel.swift | 1 - .../Menu/Share/PublicShareViewModel.swift | 2 +- .../View/Files/FileCollectionViewCell.swift | 34 ++++++++----------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index c8f9031a4..6201676ef 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -343,7 +343,6 @@ class FileListViewModel: SelectDelegate { func didSelectFile(at indexPath: IndexPath) { guard let file: File = getFile(at: indexPath) else { return } - if ReachabilityListener.instance.currentStatus == .offline && !file.isDirectory && !file.isAvailableOffline { return } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 19997a86f..3db8d8c5e 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -30,7 +30,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { guard let currentDirectory else { fatalError("PublicShareViewModel requires a currentDirectory to work") } - + let configuration = Configuration(selectAllSupported: false, rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, emptyViewType: .emptyFolder, diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 5b74cdc94..20294df74 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -164,35 +164,29 @@ protocol FileCellDelegate: AnyObject { requestFileId = file.id, weak self ] image, _ in - guard let self, - !self.file.isInvalidated, - !self.isSelected else { - return - } - - if file.id == requestFileId { - imageView.image = image - imageView.backgroundColor = nil - } + self?.setImage(image, on: imageView, requestFileId: requestFileId) } } else { // Fetch thumbnail thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in - guard let self, - !self.file.isInvalidated, - !self.isSelected else { - return - } - - if file.id == requestFileId { - imageView.image = image - imageView.backgroundColor = nil - } + self?.setImage(image, on: imageView, requestFileId: requestFileId) } } } + private func setImage(_ image: UIImage, on imageView: UIImageView, requestFileId: Int) { + guard !file.isInvalidated, + !isSelected else { + return + } + + if file.id == requestFileId { + imageView.image = image + imageView.backgroundColor = nil + } + } + deinit { downloadProgressObserver?.cancel() downloadObserver?.cancel() From 6c99ba06a490a24daa1eb4f4fc824d04daff813c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 08:22:05 +0200 Subject: [PATCH 028/129] chore: PR Feedback --- kDrive/SceneDelegate.swift | 2 +- .../File List/FileListViewController.swift | 57 ++++--------------- 2 files changed, 12 insertions(+), 47 deletions(-) diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index ae546e711..ae3a6cae3 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -234,7 +234,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { - Log.sceneDelegate("scene continue userActivity - unable", level: .error) + Log.sceneDelegate("scene continue userActivity - invalid activity", level: .error) return } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index e1a604cdf..a50c944da 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -92,6 +92,14 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV viewModel.driveFileManager } + lazy var addToKDriveButton: IKLargeButton = { + let button = IKLargeButton(frame: .zero) + button.setTitle(KDriveCoreStrings.Localizable.buttonAddToKDrive, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(addToMyDriveButtonTapped(_:)), for: .touchUpInside) + return button + }() + // MARK: - View controller lifecycle deinit { @@ -257,14 +265,6 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV return } - let addToKDriveButton = IKButton(type: .custom) - addToKDriveButton.setTitle("Add to My kDrive", for: .normal) - addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped(_:)), for: .touchUpInside) - addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray) - addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false - addToKDriveButton.cornerRadius = 8.0 - addToKDriveButton.clipsToBounds = true - view.addSubview(addToKDriveButton) view.bringSubviewToFront(addToKDriveButton) @@ -390,7 +390,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } - private func fileLayout(files: [File]) -> FloatingPanelLayout { + private func fileFloatingPanelLayout(files: [File]) -> FloatingPanelLayout { guard driveFileManager.isPublicShare else { return FileFloatingPanelLayout( initialState: .half, @@ -399,7 +399,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ) } - if files.first?.isDirectory ?? false { + if files.first?.isDirectory == true { return PublicShareFolderFloatingPanelLayout( initialState: .half, hideTip: true, @@ -425,7 +425,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV fileInformationsViewController.presentingParent = self fileInformationsViewController.normalFolderHierarchy = viewModel.configuration.normalFolderHierarchy - floatingPanelViewController.layout = fileLayout(files: files) + floatingPanelViewController.layout = fileFloatingPanelLayout(files: files) if let file = files.first { fileInformationsViewController.setFile(file, driveFileManager: driveFileManager) @@ -958,38 +958,3 @@ extension FileListViewController: UICollectionViewDropDelegate { } } } - -// Move to CoreUIKit or use something else ? -extension UIImage { - convenience init?(color: UIColor) { - let size = CGSize(width: 1, height: 1) - UIGraphicsBeginImageContext(size) - guard let context = UIGraphicsGetCurrentContext() else { - return nil - } - - context.setFillColor(color.cgColor) - context.fill(CGRect(origin: .zero, size: size)) - - let image = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - guard let cgImage = image.cgImage else { - return nil - } - - self.init(cgImage: cgImage) - } -} - -// Move to CoreUIKit or use something else ? -extension IKButton { - func setBackgroundColors(normal normalColor: UIColor, highlighted highlightedColor: UIColor) { - if let normalImage = UIImage(color: normalColor) { - setBackgroundImage(normalImage, for: .normal) - } - - if let highlightedImage = UIImage(color: highlightedColor) { - setBackgroundImage(highlightedImage, for: .highlighted) - } - } -} From 2f33f7b3b61fcd11357714bc10e006650203506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 09:31:56 +0200 Subject: [PATCH 029/129] feat: LockedFolderViewController --- .../LockedFolderViewController.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift diff --git a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift new file mode 100644 index 000000000..ef49b8977 --- /dev/null +++ b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift @@ -0,0 +1,19 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 . + */ + +class LockedFolderViewController: UIView {} From 7d8748fa40557e80558f01422944500c0b96691a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 09:49:38 +0200 Subject: [PATCH 030/129] feat: UnavaillableFolderViewController --- .../LockedFolderViewController.swift | 9 ++++++- .../UnavaillableFolderViewController.swift | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift diff --git a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift index ef49b8977..48c97819d 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift @@ -16,4 +16,11 @@ along with this program. If not, see . */ -class LockedFolderViewController: UIView {} +class LockedFolderViewController: UIViewController { + viewDidLoad() { + super.viewDidLoad() + title = "Locked Folder" + } +} + + diff --git a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift new file mode 100644 index 000000000..3ba696189 --- /dev/null +++ b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift @@ -0,0 +1,24 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 . + */ + +class UnavaillableFolderViewController: UIViewController { + viewDidLoad() { + super.viewDidLoad() + title = "Content Unavailable" + } +} From fb8ba1ccbd4d425ba6156a764b8c1385c6029578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 23 Oct 2024 14:24:44 +0200 Subject: [PATCH 031/129] feat: Error on fetch public share metadata brings the locked landing --- kDrive/AppDelegate.swift | 4 +- kDrive/AppRouter.swift | 54 +++++++++++++++++ .../LockedFolderViewController.swift | 6 +- .../UnavaillableFolderViewController.swift | 4 +- kDrive/Utils/UniversalLinksHelper.swift | 59 +++++++++++++------ kDriveCore/Data/Api/DriveApiFetcher.swift | 4 -- kDriveCore/Utils/AppNavigable.swift | 6 ++ kDriveCore/Utils/Logging.swift | 8 +-- 8 files changed, 111 insertions(+), 34 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 68d1240ff..5b5d01ad2 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -107,8 +107,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { Task { try! await Task.sleep(nanoseconds:5_000_000_000) print("coucou") - let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") - //await UIApplication.shared.open(somePublicShare!) // opens safari + // a public share password protected + let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) await UniversalLinksHelper.handlePath(components!.path) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 3bf380276..39d1e3035 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -585,6 +585,60 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable + @MainActor public func presentPublicShareLocked() { + guard let window, + let rootViewController = window.rootViewController else { + fatalError("TODO: lazy load a rootViewController") + } + + guard let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("Root is not a MainTabViewController") + return + } + + rootViewController.dismiss(animated: false) { + let viewController = LockedFolderViewController() + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical + + rootViewController.selectedIndex = MainTabBarIndex.files.rawValue + + guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { + return + } + + navigationController.present(publicShareNavigationController, animated: true, completion: nil) + } + } + + @MainActor public func presentPublicShareExpired() { + guard let window, + let rootViewController = window.rootViewController else { + fatalError("TODO: lazy load a rootViewController") + } + + guard let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("Root is not a MainTabViewController") + return + } + + rootViewController.dismiss(animated: false) { + let viewController = UnavaillableFolderViewController() + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical + + rootViewController.selectedIndex = MainTabBarIndex.files.rawValue + + guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { + return + } + + navigationController.present(publicShareNavigationController, animated: true, completion: nil) + } + } + @MainActor public func presentPublicShare( frozenRootFolder: File, publicShareProxy: PublicShareProxy, diff --git a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift index 48c97819d..161b46a43 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift @@ -16,11 +16,11 @@ along with this program. If not, see . */ +import UIKit + class LockedFolderViewController: UIViewController { - viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() title = "Locked Folder" } } - - diff --git a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift index 3ba696189..e16d7182f 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift @@ -16,8 +16,10 @@ along with this program. If not, see . */ +import UIKit + class UnavaillableFolderViewController: UIViewController { - viewDidLoad() { + override func viewDidLoad() { super.viewDidLoad() title = "Content Unavailable" } diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 10718c026..522070329 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -85,8 +85,6 @@ enum UniversalLinksHelper { } private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool { - @InjectService var accountManager: AccountManageable - guard let firstMatch = matches.first, let driveId = firstMatch[safe: 1], let driveIdInt = Int(driveId), @@ -96,32 +94,59 @@ enum UniversalLinksHelper { // request metadata let apiFetcher = PublicShareApiFetcher() - guard let metadata = try? await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) - else { - return false + do { + let metadata = try await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + return await processPublicShareMetadata( + metadata, + driveId: driveIdInt, + shareLinkUid: shareLinkUid, + apiFetcher: apiFetcher + ) + } catch { + return await processPublicShareMetadataError(error) } + } - let trackerName: String - if metadata.isPasswordNeeded { - trackerName = "publicShareWithPassword" - } else if metadata.isExpired { - trackerName = "publicShareExpired" - } else { - trackerName = "publicShare" - } - MatomoUtils.trackDeeplink(name: trackerName) + private static func processPublicShareMetadataError(_ error: Error) async -> Bool { + @InjectService var accountManager: AccountManageable + + print("•• woops \(error)") + // TODO: Switch on error +// if isPasswordNeeded { + MatomoUtils.trackDeeplink(name: "publicShareWithPassword") + @InjectService var appNavigable: AppNavigable + await appNavigable.presentPublicShareExpired() +// } + +// if .isExpired { +// MatomoUtils.trackDeeplink(name: "publicShareExpired") +// @InjectService var appNavigable: AppNavigable +// await appNavigable.presentPublicShareLocked() +// } + + return true + } + + private static func processPublicShareMetadata(_ metadata: PublicShareMetadata, + driveId: Int, + shareLinkUid: String, + apiFetcher: PublicShareApiFetcher) async -> Bool { + @InjectService var accountManager: AccountManageable + + MatomoUtils.trackDeeplink(name: "publicShare") - // get file ID from metadata let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( for: shareLinkUid, - driveId: driveIdInt, + driveId: driveId, rootFileId: metadata.fileId ) - openPublicShare(driveId: driveIdInt, + + openPublicShare(driveId: driveId, linkUuid: shareLinkUid, fileId: metadata.fileId, driveFileManager: publicShareDriveFileManager, apiFetcher: apiFetcher) + return true } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 8591eefd9..a3cd50e5e 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -59,10 +59,6 @@ public struct PublicShareMetadata: Decodable { public let validUntil: TimeInterval? public let capabilities: Rights - - // TODO: Test parsing - public let isPasswordNeeded: Bool = false - public let isExpired: Bool = false public let createdBy: TimeInterval public let createdAt: TimeInterval diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index bcd52b59e..814a28061 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -64,6 +64,12 @@ public protocol RouterFileNavigable { /// - office: Open in only office @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) + /// Present the public share locked screen + @MainActor func presentPublicShareLocked() + + /// Present the public share expired screen + @MainActor func presentPublicShareExpired() + /// Present a file list for a public share, regardless of authenticated state @MainActor func presentPublicShare( frozenRootFolder: File, diff --git a/kDriveCore/Utils/Logging.swift b/kDriveCore/Utils/Logging.swift index 3a1b63316..29ce73b67 100644 --- a/kDriveCore/Utils/Logging.swift +++ b/kDriveCore/Utils/Logging.swift @@ -81,13 +81,7 @@ public enum Logging { } private static func initNetworkLogging() { - #if DEBUG - @InjectService var appContextService: AppContextServiceable - if !appContextService.isExtension, - appContextService.context != .appTests { - Atlantis.start(hostName: ProcessInfo.processInfo.environment["hostname"]) - } - #endif + Atlantis.start(hostName: ProcessInfo.processInfo.environment["hostname"]) } private static func initSentry() { From 4a92608b3137de9ac3db6f81446f39dd65b442d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 24 Oct 2024 10:47:21 +0200 Subject: [PATCH 032/129] feat: Public Share limitations handling --- kDrive/Utils/UniversalLinksHelper.swift | 38 +++++++++------- .../Data/Api/PublicShareApiFetcher.swift | 45 +++++++++++++++---- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 522070329..a61916f9f 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -18,6 +18,7 @@ import CocoaLumberjackSwift import Foundation +import InfomaniakCore import InfomaniakDI import kDriveCore import kDriveResources @@ -103,26 +104,29 @@ enum UniversalLinksHelper { apiFetcher: apiFetcher ) } catch { - return await processPublicShareMetadataError(error) + guard let apiError = error as? ApiError else { + return false + } + + guard let limitation = PublicShareLimitation(rawValue: apiError.code) else { + return false + } + + return await processPublicShareMetadataLimitation(limitation) } } - private static func processPublicShareMetadataError(_ error: Error) async -> Bool { - @InjectService var accountManager: AccountManageable - - print("•• woops \(error)") - // TODO: Switch on error -// if isPasswordNeeded { - MatomoUtils.trackDeeplink(name: "publicShareWithPassword") - @InjectService var appNavigable: AppNavigable - await appNavigable.presentPublicShareExpired() -// } - -// if .isExpired { -// MatomoUtils.trackDeeplink(name: "publicShareExpired") -// @InjectService var appNavigable: AppNavigable -// await appNavigable.presentPublicShareLocked() -// } + private static func processPublicShareMetadataLimitation(_ limitation: PublicShareLimitation) async -> Bool { + switch limitation { + case .passwordProtected: + MatomoUtils.trackDeeplink(name: "publicShareWithPassword") + @InjectService var appNavigable: AppNavigable + await appNavigable.presentPublicShareExpired() + case .expired: + MatomoUtils.trackDeeplink(name: "publicShareExpired") + @InjectService var appNavigable: AppNavigable + await appNavigable.presentPublicShareLocked() + } return true } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 341b22741..f7b28e312 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -17,25 +17,54 @@ */ import Alamofire +import Foundation import InfomaniakCore import InfomaniakDI import InfomaniakLogin import Kingfisher +/// Server can notify us of publicShare limitations. +public enum PublicShareLimitation: String { + case passwordProtected = "password_not_valid" + case expired // TODO: +} + public class PublicShareApiFetcher: ApiFetcher { override public init() { super.init() } - public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { + /// All status including 401 are handled by our code. A locked public share will 401, therefore we need to support it. + private static var handledHttpStatus: Set = { + var allStatus = Set(200 ... 500) + return allStatus + }() + + override public func perform(request: DataRequest, + decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ValidServerResponse { + let validatedRequest = request.validate(statusCode: PublicShareApiFetcher.handledHttpStatus) + let dataResponse = await validatedRequest.serializingDecodable(ApiResponse.self, + automaticallyCancelling: true, + decoder: decoder).response + return try handleApiResponse(dataResponse) + } +} + +public extension PublicShareApiFetcher { + func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata { let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url // TODO: Use authenticated token if availlable let request = Session.default.request(shareLinkInfoUrl) - let metadata: PublicShareMetadata = try await perform(request: request) - return metadata + + do { + let metadata: PublicShareMetadata = try await perform(request: request) + return metadata + } catch InfomaniakError.apiError(let apiError) { + throw apiError + } } - public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { + func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File { let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url let requestParameters: [String: String] = [ APIUploadParameter.with.rawValue: FileWith.capabilities.rawValue @@ -46,10 +75,10 @@ public class PublicShareApiFetcher: ApiFetcher { } /// Query a specific page - public func shareLinkFileChildren(rootFolderId: Int, - publicShareProxy: PublicShareProxy, - sortType: SortType, - cursor: String? = nil) async throws -> ValidServerResponse<[File]> { + func shareLinkFileChildren(rootFolderId: Int, + publicShareProxy: PublicShareProxy, + sortType: SortType, + cursor: String? = nil) async throws -> ValidServerResponse<[File]> { let shareLinkFileChildren = Endpoint.shareLinkFileChildren( driveId: publicShareProxy.driveId, linkUuid: publicShareProxy.shareLinkUid, From 70c38ac10bab4213d55bb7f2113bef056e162961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 24 Oct 2024 19:06:25 +0200 Subject: [PATCH 033/129] feat: UFO asset --- .../UFO.imageset/Contents.json | 24 ++++++++++++++++++ .../Assets.xcassets/UFO.imageset/saucer.png | Bin 0 -> 68475 bytes .../BaseInfoViewController.swift | 19 ++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json create mode 100644 kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png create mode 100644 kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift diff --git a/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json new file mode 100644 index 000000000..881b1eefb --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "saucer.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png b/kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png new file mode 100644 index 0000000000000000000000000000000000000000..6d7b27fd524c153713de3f23cce0e2e28a241651 GIT binary patch literal 68475 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 literal 0 HcmV?d00001 diff --git a/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift new file mode 100644 index 000000000..3d5406d43 --- /dev/null +++ b/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift @@ -0,0 +1,19 @@ +// +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 . + */ + From 9889f331eaba960bb4342f34817c27f6244d0cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 24 Oct 2024 19:07:10 +0200 Subject: [PATCH 034/129] feat: Public share locked UI match desing --- .../BaseInfoViewController.swift | 78 ++++++++++++++++++- .../LockedFolderViewController.swift | 48 +++++++++++- .../UnavaillableFolderViewController.swift | 10 ++- kDrive/Utils/UniversalLinksHelper.swift | 7 +- 4 files changed, 134 insertions(+), 9 deletions(-) diff --git a/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift index 3d5406d43..33151479d 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift @@ -1,4 +1,3 @@ -// /* Infomaniak kDrive - iOS App Copyright (C) 2024 Infomaniak Network SA @@ -17,3 +16,80 @@ along with this program. If not, see . */ +import kDriveCore +import kDriveResources +import UIKit + +class BaseInfoViewController: UIViewController { + let titleLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .headline) + label.textColor = KDriveResourcesAsset.primaryTextColor.color + label.numberOfLines = 1 + label.textAlignment = .center + return label + }() + + let descriptionLabel: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .body) + label.textColor = KDriveResourcesAsset.secondaryTextColor.color + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + let centerImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + let containerView = UIView() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + + containerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(containerView) + NSLayoutConstraint.activate([ + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + + centerImageView.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(centerImageView) + containerView.addSubview(titleLabel) + containerView.addSubview(descriptionLabel) + + let views = ["titleLabel": titleLabel, + "descriptionLabel": descriptionLabel, + "centerImageView": centerImageView] + + let verticalConstraints = NSLayoutConstraint + .constraints(withVisualFormat: "V:|[centerImageView]-[titleLabel]-[descriptionLabel]|", + metrics: nil, + views: views) + + let horizontalConstraints = [ + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + titleLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1, constant: -20), + descriptionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + descriptionLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1, constant: -20), + centerImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + centerImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1) + ] + + NSLayoutConstraint.activate(verticalConstraints) + NSLayoutConstraint.activate(horizontalConstraints) + } + + @objc open func closeButtonPressed() { + dismiss(animated: true) + } +} diff --git a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift index 161b46a43..294150729 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift @@ -16,11 +16,55 @@ along with this program. If not, see . */ +import kDriveCore +import kDriveResources import UIKit -class LockedFolderViewController: UIViewController { +class LockedFolderViewController: BaseInfoViewController { + let openWebButton: IKLargeButton = { + let button = IKLargeButton(frame: .zero) + // TODO: i18n + button.setTitle("Open in Browser", for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(openWebBrowser), for: .touchUpInside) + return button + }() + override func viewDidLoad() { super.viewDidLoad() - title = "Locked Folder" + + centerImageView.image = KDriveResourcesAsset.lockInfomaniak.image + titleLabel.text = "Protected content" + descriptionLabel.text = "Password-protected links are not yet available on the mobile app." + + setupOpenWebButton() + } + + private func setupOpenWebButton() { + view.addSubview(openWebButton) + view.bringSubviewToFront(openWebButton) + + let leadingConstraint = openWebButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, + constant: 16) + leadingConstraint.priority = UILayoutPriority.defaultHigh + let trailingConstraint = openWebButton.trailingAnchor.constraint( + greaterThanOrEqualTo: view.trailingAnchor, + constant: -16 + ) + trailingConstraint.priority = UILayoutPriority.defaultHigh + let widthConstraint = openWebButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360) + + NSLayoutConstraint.activate([ + openWebButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + leadingConstraint, + trailingConstraint, + openWebButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + openWebButton.heightAnchor.constraint(equalToConstant: 60), + widthConstraint + ]) + } + + @objc public func openWebBrowser() { + // dismiss(animated: true) } } diff --git a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift index e16d7182f..f1d9b79fb 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift @@ -16,11 +16,17 @@ along with this program. If not, see . */ +import kDriveResources import UIKit -class UnavaillableFolderViewController: UIViewController { +class UnavaillableFolderViewController: BaseInfoViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Content Unavailable" + + centerImageView.image = KDriveResourcesAsset.ufo.image + titleLabel.text = "Content Unavailable" + descriptionLabel + .text = + "The link has been deactivated or has expired. To access the files, send a message to the user who shared the link with you to reactivate it." } } diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index a61916f9f..f85a8385b 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -117,15 +117,14 @@ enum UniversalLinksHelper { } private static func processPublicShareMetadataLimitation(_ limitation: PublicShareLimitation) async -> Bool { + @InjectService var appNavigable: AppNavigable switch limitation { case .passwordProtected: MatomoUtils.trackDeeplink(name: "publicShareWithPassword") - @InjectService var appNavigable: AppNavigable - await appNavigable.presentPublicShareExpired() + await appNavigable.presentPublicShareLocked() case .expired: MatomoUtils.trackDeeplink(name: "publicShareExpired") - @InjectService var appNavigable: AppNavigable - await appNavigable.presentPublicShareLocked() + await appNavigable.presentPublicShareExpired() } return true From 424e218ab11f655c0074d9cc797ff7599c2fbd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 25 Oct 2024 11:15:33 +0200 Subject: [PATCH 035/129] feat: Latest UI --- kDrive/AppDelegate.swift | 3 +- kDrive/AppRouter.swift | 3 +- .../UFO.imageset/Contents.json | 2 +- .../UFO.imageset/abducted_files.svg | 39 ++++++++++++++++++ .../Assets.xcassets/UFO.imageset/saucer.png | Bin 68475 -> 0 bytes .../lock_external.imageset/Contents.json | 25 +++++++++++ .../lock_external.imageset/lock-clear.svg | 4 ++ .../lock_external.imageset/lock-dark.svg | 4 ++ kDrive/SceneDelegate.swift | 5 +-- .../BaseInfoViewController.swift | 23 ++++++++++- .../LockedFolderViewController.swift | 26 +++++++----- .../UnavaillableFolderViewController.swift | 0 kDrive/Utils/UniversalLinksHelper.swift | 22 +++++++--- kDriveCore/Utils/AppNavigable.swift | 2 +- 14 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 kDrive/Resources/Assets.xcassets/UFO.imageset/abducted_files.svg delete mode 100644 kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png create mode 100644 kDrive/Resources/Assets.xcassets/lock_external.imageset/Contents.json create mode 100644 kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-clear.svg create mode 100644 kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-dark.svg rename kDrive/UI/Controller/Files/{Rights and Share => External}/BaseInfoViewController.swift (81%) rename kDrive/UI/Controller/Files/{Rights and Share => External}/LockedFolderViewController.swift (81%) rename kDrive/UI/Controller/Files/{Rights and Share => External}/UnavaillableFolderViewController.swift (100%) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 5b5d01ad2..4942350e0 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -110,8 +110,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // a public share password protected let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") - let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true) - await UniversalLinksHelper.handlePath(components!.path) + await UniversalLinksHelper.handleURL(somePublicShare!) } return true diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 39d1e3035..df8176374 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -585,7 +585,7 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable - @MainActor public func presentPublicShareLocked() { + @MainActor public func presentPublicShareLocked(_ destinationURL: URL) { guard let window, let rootViewController = window.rootViewController else { fatalError("TODO: lazy load a rootViewController") @@ -598,6 +598,7 @@ public struct AppRouter: AppNavigable { rootViewController.dismiss(animated: false) { let viewController = LockedFolderViewController() + viewController.destinationURL = destinationURL let publicShareNavigationController = UINavigationController(rootViewController: viewController) publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical diff --git a/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json index 881b1eefb..96eb5f71b 100644 --- a/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json +++ b/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "saucer.png", + "filename" : "abducted_files.svg", "idiom" : "universal" }, { diff --git a/kDrive/Resources/Assets.xcassets/UFO.imageset/abducted_files.svg b/kDrive/Resources/Assets.xcassets/UFO.imageset/abducted_files.svg new file mode 100644 index 000000000..a08d17917 --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/UFO.imageset/abducted_files.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png b/kDrive/Resources/Assets.xcassets/UFO.imageset/saucer.png deleted file mode 100644 index 6d7b27fd524c153713de3f23cce0e2e28a241651..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68475 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 diff --git a/kDrive/Resources/Assets.xcassets/lock_external.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/lock_external.imageset/Contents.json new file mode 100644 index 000000000..a7224c493 --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/lock_external.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "lock-clear.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "lock-dark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-clear.svg b/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-clear.svg new file mode 100644 index 000000000..5c32cbb94 --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-clear.svg @@ -0,0 +1,4 @@ + + + + diff --git a/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-dark.svg b/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-dark.svg new file mode 100644 index 000000000..667d8ce4e --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/lock_external.imageset/lock-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index ae3a6cae3..5ddc84048 100644 --- a/kDrive/SceneDelegate.swift +++ b/kDrive/SceneDelegate.swift @@ -232,13 +232,12 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel Log.sceneDelegate("scene continue userActivity") Task { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let incomingURL = userActivity.webpageURL, - let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { + let incomingURL = userActivity.webpageURL else { Log.sceneDelegate("scene continue userActivity - invalid activity", level: .error) return } - await UniversalLinksHelper.handlePath(components.path) + await UniversalLinksHelper.handleURL(incomingURL) } } diff --git a/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift similarity index 81% rename from kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift rename to kDrive/UI/Controller/Files/External/BaseInfoViewController.swift index 33151479d..5f8b68624 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/BaseInfoViewController.swift +++ b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift @@ -45,13 +45,34 @@ class BaseInfoViewController: UIViewController { return imageView }() + let closeButton: IKButton = { + let button = IKButton() + button.setImage(UIImage(named: "close"), for: .normal) + return button + }() + let containerView = UIView() override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + view.backgroundColor = KDriveResourcesAsset.backgroundColor.color + setupCloseButton() + setupBody() + } + + private func setupCloseButton() { + closeButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(closeButton) + + closeButton.addTarget(self, action: #selector(closeButtonPressed), for: .touchUpInside) + NSLayoutConstraint.activate([ + closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0), + closeButton.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 25) + ]) + } + private func setupBody() { containerView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(containerView) NSLayoutConstraint.activate([ diff --git a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift similarity index 81% rename from kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift rename to kDrive/UI/Controller/Files/External/LockedFolderViewController.swift index 294150729..e8be9707d 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift @@ -21,19 +21,14 @@ import kDriveResources import UIKit class LockedFolderViewController: BaseInfoViewController { - let openWebButton: IKLargeButton = { - let button = IKLargeButton(frame: .zero) - // TODO: i18n - button.setTitle("Open in Browser", for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(openWebBrowser), for: .touchUpInside) - return button - }() + var destinationURL: URL? + + let openWebButton = IKLargeButton(frame: .zero) override func viewDidLoad() { super.viewDidLoad() - centerImageView.image = KDriveResourcesAsset.lockInfomaniak.image + centerImageView.image = KDriveResourcesAsset.lockExternal.image titleLabel.text = "Protected content" descriptionLabel.text = "Password-protected links are not yet available on the mobile app." @@ -41,6 +36,11 @@ class LockedFolderViewController: BaseInfoViewController { } private func setupOpenWebButton() { + // TODO: i18n + openWebButton.setTitle("Open in Browser", for: .normal) + openWebButton.translatesAutoresizingMaskIntoConstraints = false + openWebButton.addTarget(self, action: #selector(openWebBrowser), for: .touchUpInside) + view.addSubview(openWebButton) view.bringSubviewToFront(openWebButton) @@ -63,8 +63,12 @@ class LockedFolderViewController: BaseInfoViewController { widthConstraint ]) } - + @objc public func openWebBrowser() { - // dismiss(animated: true) + guard let destinationURL else { + return + } + + UIApplication.shared.open(destinationURL) } } diff --git a/kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift similarity index 100% rename from kDrive/UI/Controller/Files/Rights and Share/UnavaillableFolderViewController.swift rename to kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index f85a8385b..0afcd7e9c 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -63,13 +63,19 @@ enum UniversalLinksHelper { } @discardableResult - static func handlePath(_ path: String) async -> Bool { + static func handleURL(_ url: URL) async -> Bool { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + DDLogError("[UniversalLinksHelper] Failed to process url:\(url)") + return false + } + + let path = components.path DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") // Public share link regex let shareLink = Link.publicShareLink let matches = shareLink.regex.matches(in: path) - if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode) { + if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode, publicShareURL: url) { return true } @@ -85,7 +91,7 @@ enum UniversalLinksHelper { return false } - private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool { + private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode, publicShareURL: URL) async -> Bool { guard let firstMatch = matches.first, let driveId = firstMatch[safe: 1], let driveIdInt = Int(driveId), @@ -112,16 +118,20 @@ enum UniversalLinksHelper { return false } - return await processPublicShareMetadataLimitation(limitation) + return await processPublicShareMetadataLimitation(limitation, publicShareURL: publicShareURL) } } - private static func processPublicShareMetadataLimitation(_ limitation: PublicShareLimitation) async -> Bool { + private static func processPublicShareMetadataLimitation(_ limitation: PublicShareLimitation, + publicShareURL: URL?) async -> Bool { @InjectService var appNavigable: AppNavigable switch limitation { case .passwordProtected: + guard let publicShareURL else { + return false + } MatomoUtils.trackDeeplink(name: "publicShareWithPassword") - await appNavigable.presentPublicShareLocked() + await appNavigable.presentPublicShareLocked(publicShareURL) case .expired: MatomoUtils.trackDeeplink(name: "publicShareExpired") await appNavigable.presentPublicShareExpired() diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index 814a28061..f1ee481e9 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -65,7 +65,7 @@ public protocol RouterFileNavigable { @MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool) /// Present the public share locked screen - @MainActor func presentPublicShareLocked() + @MainActor func presentPublicShareLocked(_ destinationURL: URL) /// Present the public share expired screen @MainActor func presentPublicShareExpired() From 097e966e4024642295495778653ff28429000213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 28 Oct 2024 13:06:21 +0100 Subject: [PATCH 036/129] chore: New strings for public share --- kDrive/Resources/de.lproj/Localizable.strings | 61 +++++++++++++++++- kDrive/Resources/en.lproj/Localizable.strings | 63 ++++++++++++++++++- kDrive/Resources/es.lproj/Localizable.strings | 52 ++++++++++++++- kDrive/Resources/fr.lproj/Localizable.strings | 61 +++++++++++++++++- kDrive/Resources/it.lproj/Localizable.strings | 61 +++++++++++++++++- 5 files changed, 287 insertions(+), 11 deletions(-) diff --git a/kDrive/Resources/de.lproj/Localizable.strings b/kDrive/Resources/de.lproj/Localizable.strings index a1aa2fc8e..bc92b5a46 100644 --- a/kDrive/Resources/de.lproj/Localizable.strings +++ b/kDrive/Resources/de.lproj/Localizable.strings @@ -3,8 +3,8 @@ * Project: kDrive * Locale: de, German * Tagged: ios - * Exported by: Matthieu Déglon - * Exported at: Thu, 03 Oct 2024 08:22:20 +0200 + * Exported by: Adrien Coye + * Exported at: Mon, 28 Oct 2024 09:46:15 +0100 */ /* loco:610a8791fa12ab20713c09e4 */ @@ -376,6 +376,9 @@ /* loco:6075c0bb65160c29997c5e32 */ "buttonOpenDocument" = "Dokument öffnen"; +/* loco:66dfe78b7da115a16c0733e2 */ +"buttonOpenInBrowser" = "Im Browser öffnen"; + /* loco:607948379bda7f7df0121872 */ "buttonOpenReadOnly" = "Im Lese-Modus öffnen"; @@ -709,6 +712,15 @@ /* loco:6049df4d5c2c3a04bc397992 */ "dropBoxTitle" = "Briefkasten"; +/* loco:6708c590f8f4d36ec100ef42 */ +"dropboxPublicShareOutdatedDescription" = "Der Link wurde deaktiviert oder ist abgelaufen.\nUm Ihre Dateien einzureichen, senden Sie eine Nachricht an den Nutzer, der die Dropbox für Sie freigegeben hat, damit er sie wieder aktiviert."; + +/* loco:6708c5155b4785a798019472 */ +"dropboxPublicShareOutdatedTitle" = "Diese Dropbox ist nicht mehr verfügbar"; + +/* loco:6707c8d16e460cb6a304b692 */ +"dropboxPublicShareTitleUploadButton" = "%@ lädt Sie ein, Ihre Dateien auf sein kDrive zu importieren"; + /* loco:618b870b92fff1241d67e713 */ "dropboxSharedLinkDescription" = "Sie können keinen Freigabelink für einen Briefkasten erstellen."; @@ -1498,6 +1510,27 @@ /* loco:6049dfb2105eca5bbd0801b4 */ "notificationUploadServiceChannelName" = "Dienst für den Dateiimport"; +/* loco:66f658cbb0e522d4f50cc2f2 */ +"obtainkDriveAdAlreadyGotAccount" = "Ich habe bereits ein Konto"; + +/* loco:66f64e66297dda6eb402f022 */ +"obtainkDriveAdDescription" = "Speichern Sie Ihre Fotos, Dokumente und E-Mails in der Schweiz bei einem unabhängigen Unternehmen, das die Privatsphäre respektiert."; + +/* loco:66f657511dc417a794017b92 */ +"obtainkDriveAdFreeTrialButton" = "Kostenlos ausprobieren"; + +/* loco:66f64fa849bd6bdd0f0ae622 */ +"obtainkDriveAdListing1" = "15 GB kostenlos, dann 2 TB bis zu 106 TB"; + +/* loco:66f65063f4b00a1c660ce462 */ +"obtainkDriveAdListing2" = "Online-Erstellung und Zusammenarbeit für Word, Excel und PowerPoint-Dokumenten"; + +/* loco:66f651361a641d2f5f0207d2 */ +"obtainkDriveAdListing3" = "Automatischer Import Ihrer Dateien aus Google Drive, Dropbox, One Drive, NextCloud, Hubic und WebDav"; + +/* loco:66f64797fdc443552a0a5e92 */ +"obtainkDriveAdTitle" = "Erhalten Sie kDrive kostenlos"; + /* loco:6049df4d5c2c3a04bc397a19 */ "offlineFileNoFile" = "Keine offline Dateien"; @@ -1564,6 +1597,30 @@ /* loco:6049df4d5c2c3a04bc397a28 */ "previewVideoSourceError" = "Videodatei wird vom Videoplayer nicht unterstützt"; +/* loco:6707af761017cac6e10d85b4 */ +"publicShareBadLinkError" = "Keine diesem Link zugeordneten Freigaben"; + +/* loco:66ffbbcd294f6022e60948f3 */ +"publicShareImportationInProgress" = "Laufender Download im ausgewählten Ordner"; + +/* loco:6707ca3c9b41ff114e052962 */ +"publicShareLinkValidityDescription" = "Dieser Link ist gültig bis zum %@"; + +/* loco:66e0295f01da3c1ab90c2d72 */ +"publicShareOutdatedLinkDescription" = "Der Link wurde deaktiviert oder ist abgelaufen.\nUm auf die Dateien zuzugreifen, senden Sie eine Nachricht an den Nutzer, der den Link für Sie freigegeben hat, damit er ihn wieder aktiviert."; + +/* loco:66e028cb3cd41df03c0a7003 */ +"publicShareOutdatedLinkTitle" = "Die Dateien sind nicht mehr verfügbar"; + +/* loco:66d05b05fe43da3cb8009652 */ +"publicSharePasswordNeededDescription" = "Bitte geben Sie das Passwort ein, das Sie erhalten haben, um auf den Inhalt zuzugreifen."; + +/* loco:66d05b5928ec407b32087892 */ +"publicSharePasswordNeededTitle" = "Sicherer Inhalt"; + +/* loco:66dfe73543cef47d3e073e62 */ +"publicSharePasswordNotSupportedDescription" = "Passwortgeschützte Links sind in der mobilen Anwendung noch nicht verfügbar."; + /* loco:617a50f1744434292a2f07a2 */ "publicSharedLinkTitle" = "Öffentlicher Freigabelink"; diff --git a/kDrive/Resources/en.lproj/Localizable.strings b/kDrive/Resources/en.lproj/Localizable.strings index adc6269a0..da436ea1e 100644 --- a/kDrive/Resources/en.lproj/Localizable.strings +++ b/kDrive/Resources/en.lproj/Localizable.strings @@ -1,10 +1,10 @@ /* - * Loco ios export: iOS Localizable.strings + * Loco ios export: Xcode Strings (legacy) * Project: kDrive * Locale: en, English * Tagged: ios - * Exported by: Valentin Perignon - * Exported at: Fri, 26 Jul 2024 13:09:12 +0200 + * Exported by: Adrien Coye + * Exported at: Mon, 28 Oct 2024 09:46:15 +0100 */ /* loco:610a8791fa12ab20713c09e4 */ @@ -376,6 +376,9 @@ /* loco:6075c0bb65160c29997c5e32 */ "buttonOpenDocument" = "Open document"; +/* loco:66dfe78b7da115a16c0733e2 */ +"buttonOpenInBrowser" = "Open in browser"; + /* loco:607948379bda7f7df0121872 */ "buttonOpenReadOnly" = "Open in read-only mode"; @@ -709,6 +712,15 @@ /* loco:6049df4d5c2c3a04bc397992 */ "dropBoxTitle" = "Drop box"; +/* loco:6708c590f8f4d36ec100ef42 */ +"dropboxPublicShareOutdatedDescription" = "The link has been deactivated or has expired.\nTo upload your files, send a message to the user who shared the Dropbox with you so that they can reactivate it."; + +/* loco:6708c5155b4785a798019472 */ +"dropboxPublicShareOutdatedTitle" = "This dropbox is no longer available"; + +/* loco:6707c8d16e460cb6a304b692 */ +"dropboxPublicShareTitleUploadButton" = "%@ invites you to import your files onto their kDrive"; + /* loco:618b870b92fff1241d67e713 */ "dropboxSharedLinkDescription" = "You cannot create a share link on a drop box."; @@ -1498,6 +1510,27 @@ /* loco:6049dfb2105eca5bbd0801b4 */ "notificationUploadServiceChannelName" = "File upload services"; +/* loco:66f658cbb0e522d4f50cc2f2 */ +"obtainkDriveAdAlreadyGotAccount" = "I already have an account"; + +/* loco:66f64e66297dda6eb402f022 */ +"obtainkDriveAdDescription" = "Store your photos, documents and emails in Switzerland with an independent company that respects your privacy."; + +/* loco:66f657511dc417a794017b92 */ +"obtainkDriveAdFreeTrialButton" = "Free trial"; + +/* loco:66f64fa849bd6bdd0f0ae622 */ +"obtainkDriveAdListing1" = "15 GB free, then 2 TB up to 106 TB"; + +/* loco:66f65063f4b00a1c660ce462 */ +"obtainkDriveAdListing2" = "Online creation and collaboration for Word, Excel and PowerPoint documents"; + +/* loco:66f651361a641d2f5f0207d2 */ +"obtainkDriveAdListing3" = "Automatic file import from Google Drive, Dropbox, One Drive, NextCloud, Hubic and WebDav"; + +/* loco:66f64797fdc443552a0a5e92 */ +"obtainkDriveAdTitle" = "Get kDrive for free"; + /* loco:6049df4d5c2c3a04bc397a19 */ "offlineFileNoFile" = "No files offline"; @@ -1564,6 +1597,30 @@ /* loco:6049df4d5c2c3a04bc397a28 */ "previewVideoSourceError" = "Video file not supported by the video player"; +/* loco:6707af761017cac6e10d85b4 */ +"publicShareBadLinkError" = "No share associated with this link"; + +/* loco:66ffbbcd294f6022e60948f3 */ +"publicShareImportationInProgress" = "Download in progress in selected folder"; + +/* loco:6707ca3c9b41ff114e052962 */ +"publicShareLinkValidityDescription" = "This link is valid until %@"; + +/* loco:66e0295f01da3c1ab90c2d72 */ +"publicShareOutdatedLinkDescription" = "The link has been deactivated or has expired.\nTo access its files, send a message to the user who shared the link with you so that they can reactivate it."; + +/* loco:66e028cb3cd41df03c0a7003 */ +"publicShareOutdatedLinkTitle" = "The files are no longer available"; + +/* loco:66d05b05fe43da3cb8009652 */ +"publicSharePasswordNeededDescription" = "Please enter the password provided to access the content."; + +/* loco:66d05b5928ec407b32087892 */ +"publicSharePasswordNeededTitle" = "Protected content"; + +/* loco:66dfe73543cef47d3e073e62 */ +"publicSharePasswordNotSupportedDescription" = "Password-protected links are not yet available on the mobile application."; + /* loco:617a50f1744434292a2f07a2 */ "publicSharedLinkTitle" = "Public sharing link"; diff --git a/kDrive/Resources/es.lproj/Localizable.strings b/kDrive/Resources/es.lproj/Localizable.strings index 5f47ad193..800986ddb 100644 --- a/kDrive/Resources/es.lproj/Localizable.strings +++ b/kDrive/Resources/es.lproj/Localizable.strings @@ -3,8 +3,8 @@ * Project: kDrive * Locale: es, Spanish * Tagged: ios - * Exported by: Matthieu Déglon - * Exported at: Thu, 03 Oct 2024 08:22:20 +0200 + * Exported by: Adrien Coye + * Exported at: Mon, 28 Oct 2024 09:46:15 +0100 */ /* loco:610a8791fa12ab20713c09e4 */ @@ -376,6 +376,9 @@ /* loco:6075c0bb65160c29997c5e32 */ "buttonOpenDocument" = "Abrir el documento"; +/* loco:66dfe78b7da115a16c0733e2 */ +"buttonOpenInBrowser" = "Abrir en el navegador"; + /* loco:607948379bda7f7df0121872 */ "buttonOpenReadOnly" = "Abrir en modo de solo lectura"; @@ -1498,6 +1501,27 @@ /* loco:6049dfb2105eca5bbd0801b4 */ "notificationUploadServiceChannelName" = "Servicio de importación de archivos"; +/* loco:66f658cbb0e522d4f50cc2f2 */ +"obtainkDriveAdAlreadyGotAccount" = "Ya tengo una cuenta"; + +/* loco:66f64e66297dda6eb402f022 */ +"obtainkDriveAdDescription" = "Almacene sus fotos, documentos y correos electrónicos en Suiza con una empresa independiente que respeta su privacidad."; + +/* loco:66f657511dc417a794017b92 */ +"obtainkDriveAdFreeTrialButton" = "Prueba gratuita"; + +/* loco:66f64fa849bd6bdd0f0ae622 */ +"obtainkDriveAdListing1" = "15 GB gratuitos, luego 2 TB hasta 106 TB"; + +/* loco:66f65063f4b00a1c660ce462 */ +"obtainkDriveAdListing2" = "Creación y colaboración en línea de documentos Word, Excel y PowerPoint"; + +/* loco:66f651361a641d2f5f0207d2 */ +"obtainkDriveAdListing3" = "Importación automática de tus archivos desde Google Drive, Dropbox, One Drive, NextCloud, Hubic y WebDav"; + +/* loco:66f64797fdc443552a0a5e92 */ +"obtainkDriveAdTitle" = "Consigue kDrive gratis"; + /* loco:6049df4d5c2c3a04bc397a19 */ "offlineFileNoFile" = "No hay archivos sin conexión"; @@ -1564,6 +1588,30 @@ /* loco:6049df4d5c2c3a04bc397a28 */ "previewVideoSourceError" = "El archivo de vídeo no es compatible con el lector de vídeo"; +/* loco:6707af761017cac6e10d85b4 */ +"publicShareBadLinkError" = "No hay compartir asociadas a este enlace"; + +/* loco:66ffbbcd294f6022e60948f3 */ +"publicShareImportationInProgress" = "Descarga en curso en la carpeta seleccionada"; + +/* loco:6707ca3c9b41ff114e052962 */ +"publicShareLinkValidityDescription" = "Este enlace es válido hasta el %@"; + +/* loco:66e0295f01da3c1ab90c2d72 */ +"publicShareOutdatedLinkDescription" = "El enlace ha sido desactivado o ha caducado.\nPara acceder a los archivos, envía un mensaje al usuario que compartió el enlace contigo para que pueda reactivarlo."; + +/* loco:66e028cb3cd41df03c0a7003 */ +"publicShareOutdatedLinkTitle" = "Los archivos ya no están disponibles"; + +/* loco:66d05b05fe43da3cb8009652 */ +"publicSharePasswordNeededDescription" = "Introduzca la contraseña que se le ha facilitado para acceder al contenido."; + +/* loco:66d05b5928ec407b32087892 */ +"publicSharePasswordNeededTitle" = "Contenido seguro"; + +/* loco:66dfe73543cef47d3e073e62 */ +"publicSharePasswordNotSupportedDescription" = "Los enlaces protegidos por contraseña aún no están disponibles en la aplicación móvil."; + /* loco:617a50f1744434292a2f07a2 */ "publicSharedLinkTitle" = "Enlace de uso compartido público"; diff --git a/kDrive/Resources/fr.lproj/Localizable.strings b/kDrive/Resources/fr.lproj/Localizable.strings index a1bc7174d..f8259d3f4 100644 --- a/kDrive/Resources/fr.lproj/Localizable.strings +++ b/kDrive/Resources/fr.lproj/Localizable.strings @@ -3,8 +3,8 @@ * Project: kDrive * Locale: fr, French * Tagged: ios - * Exported by: Matthieu Déglon - * Exported at: Thu, 03 Oct 2024 08:22:20 +0200 + * Exported by: Adrien Coye + * Exported at: Mon, 28 Oct 2024 09:46:15 +0100 */ /* loco:610a8791fa12ab20713c09e4 */ @@ -376,6 +376,9 @@ /* loco:6075c0bb65160c29997c5e32 */ "buttonOpenDocument" = "Ouvrir le document"; +/* loco:66dfe78b7da115a16c0733e2 */ +"buttonOpenInBrowser" = "Ouvrir dans le navigateur"; + /* loco:607948379bda7f7df0121872 */ "buttonOpenReadOnly" = "Ouvrir en lecture seule"; @@ -709,6 +712,15 @@ /* loco:6049df4d5c2c3a04bc397992 */ "dropBoxTitle" = "Boîte de dépôt"; +/* loco:6708c590f8f4d36ec100ef42 */ +"dropboxPublicShareOutdatedDescription" = "Le lien a été désactivé ou a expiré.\nPour déposer vos fichiers, envoyez un message à l’utilisateur qui vous a partagé la boîte de dépôt pour qu’il la réactive"; + +/* loco:6708c5155b4785a798019472 */ +"dropboxPublicShareOutdatedTitle" = "Cette boîte de dépôt n’est plus disponible"; + +/* loco:6707c8d16e460cb6a304b692 */ +"dropboxPublicShareTitleUploadButton" = "%@ vous invite à importer vos fichiers sur son kDrive"; + /* loco:618b870b92fff1241d67e713 */ "dropboxSharedLinkDescription" = "Vous ne pouvez pas créer un lien de partage sur une boite de dépôt."; @@ -1498,6 +1510,27 @@ /* loco:6049dfb2105eca5bbd0801b4 */ "notificationUploadServiceChannelName" = "Service d’importation de fichier"; +/* loco:66f658cbb0e522d4f50cc2f2 */ +"obtainkDriveAdAlreadyGotAccount" = "J’ai déjà un compte"; + +/* loco:66f64e66297dda6eb402f022 */ +"obtainkDriveAdDescription" = "Stockez vos photos, documents et e-mails en Suisse auprès d’une entreprise indépendante qui respecte la vie privée."; + +/* loco:66f657511dc417a794017b92 */ +"obtainkDriveAdFreeTrialButton" = "Tester gratuitement"; + +/* loco:66f64fa849bd6bdd0f0ae622 */ +"obtainkDriveAdListing1" = "15 Go gratuit, puis de 2 To jusqu’à 106 To"; + +/* loco:66f65063f4b00a1c660ce462 */ +"obtainkDriveAdListing2" = "Création et collaboration en ligne de documents Word, Excel et PowerPoint"; + +/* loco:66f651361a641d2f5f0207d2 */ +"obtainkDriveAdListing3" = "Import automatique de vos fichiers depuis Google Drive, Dropbox, One Drive, NextCloud, Hubic et WebDav"; + +/* loco:66f64797fdc443552a0a5e92 */ +"obtainkDriveAdTitle" = "Obtenez kDrive gratuitement"; + /* loco:6049df4d5c2c3a04bc397a19 */ "offlineFileNoFile" = "Aucun fichier hors ligne"; @@ -1564,6 +1597,30 @@ /* loco:6049df4d5c2c3a04bc397a28 */ "previewVideoSourceError" = "Fichier vidéo non pris en charge par le lecteur vidéo"; +/* loco:6707af761017cac6e10d85b4 */ +"publicShareBadLinkError" = "Aucun partage associé à ce lien"; + +/* loco:66ffbbcd294f6022e60948f3 */ +"publicShareImportationInProgress" = "Téléchargement en cours dans le dossier sélectionné"; + +/* loco:6707ca3c9b41ff114e052962 */ +"publicShareLinkValidityDescription" = "Ce lien est valable jusqu’au %@"; + +/* loco:66e0295f01da3c1ab90c2d72 */ +"publicShareOutdatedLinkDescription" = "Le lien a été désactivé ou a expiré.\nPour accéder aux fichiers, envoyez un message à l’utilisateur qui vous a partagé le lien pour qu’il le réactive."; + +/* loco:66e028cb3cd41df03c0a7003 */ +"publicShareOutdatedLinkTitle" = "Les fichiers ne sont plus disponibles"; + +/* loco:66d05b05fe43da3cb8009652 */ +"publicSharePasswordNeededDescription" = "Veuillez saisir le mot de passe qui vous a été fourni pour accéder au contenu."; + +/* loco:66d05b5928ec407b32087892 */ +"publicSharePasswordNeededTitle" = "Contenu sécurisé"; + +/* loco:66dfe73543cef47d3e073e62 */ +"publicSharePasswordNotSupportedDescription" = "Les liens protégés par mot de passe ne sont pas encore disponibles sur l’application mobile."; + /* loco:617a50f1744434292a2f07a2 */ "publicSharedLinkTitle" = "Lien de partage public"; diff --git a/kDrive/Resources/it.lproj/Localizable.strings b/kDrive/Resources/it.lproj/Localizable.strings index d46fcfc9f..77321d1b3 100644 --- a/kDrive/Resources/it.lproj/Localizable.strings +++ b/kDrive/Resources/it.lproj/Localizable.strings @@ -3,8 +3,8 @@ * Project: kDrive * Locale: it, Italian * Tagged: ios - * Exported by: Matthieu Déglon - * Exported at: Thu, 03 Oct 2024 08:22:20 +0200 + * Exported by: Adrien Coye + * Exported at: Mon, 28 Oct 2024 09:46:15 +0100 */ /* loco:610a8791fa12ab20713c09e4 */ @@ -376,6 +376,9 @@ /* loco:6075c0bb65160c29997c5e32 */ "buttonOpenDocument" = "Aprire documento"; +/* loco:66dfe78b7da115a16c0733e2 */ +"buttonOpenInBrowser" = "Apri nel browser"; + /* loco:607948379bda7f7df0121872 */ "buttonOpenReadOnly" = "Aprire in modalità di sola lettura"; @@ -709,6 +712,15 @@ /* loco:6049df4d5c2c3a04bc397992 */ "dropBoxTitle" = "Deposito file"; +/* loco:6708c590f8f4d36ec100ef42 */ +"dropboxPublicShareOutdatedDescription" = "Il link è stato disattivato o è scaduto.\nPer caricare i vostri file, inviate un messaggio all’utente che ha condiviso il Dropbox con te in modo che possa riattivarlo."; + +/* loco:6708c5155b4785a798019472 */ +"dropboxPublicShareOutdatedTitle" = "Questo dropbox non è più disponibile"; + +/* loco:6707c8d16e460cb6a304b692 */ +"dropboxPublicShareTitleUploadButton" = "%@ ti invita a importare i tuoi file sul loro kDrive"; + /* loco:618b870b92fff1241d67e713 */ "dropboxSharedLinkDescription" = "Non è possibile creare un link di condivisione su un deposito file."; @@ -1498,6 +1510,27 @@ /* loco:6049dfb2105eca5bbd0801b4 */ "notificationUploadServiceChannelName" = "Servizi per l’importazione di file"; +/* loco:66f658cbb0e522d4f50cc2f2 */ +"obtainkDriveAdAlreadyGotAccount" = "Ho già un account"; + +/* loco:66f64e66297dda6eb402f022 */ +"obtainkDriveAdDescription" = "Archiviate le tue foto, i tuoi documenti e le tue e-mail in Svizzera con un’azienda indipendente che rispetta la tua privacy."; + +/* loco:66f657511dc417a794017b92 */ +"obtainkDriveAdFreeTrialButton" = "Prova gratuita"; + +/* loco:66f64fa849bd6bdd0f0ae622 */ +"obtainkDriveAdListing1" = "15 GB gratuiti, poi 2 TB fino a 106 TB"; + +/* loco:66f65063f4b00a1c660ce462 */ +"obtainkDriveAdListing2" = "Creazione e collaborazione online di documenti Word, Excel e PowerPoint"; + +/* loco:66f651361a641d2f5f0207d2 */ +"obtainkDriveAdListing3" = "Importazione automatica dei file da Google Drive, Dropbox, One Drive, NextCloud, Hubic e WebDav"; + +/* loco:66f64797fdc443552a0a5e92 */ +"obtainkDriveAdTitle" = "Ottieni kDrive gratuitamente"; + /* loco:6049df4d5c2c3a04bc397a19 */ "offlineFileNoFile" = "Nessun file offline"; @@ -1564,6 +1597,30 @@ /* loco:6049df4d5c2c3a04bc397a28 */ "previewVideoSourceError" = "File video non supportato dal lettore"; +/* loco:6707af761017cac6e10d85b4 */ +"publicShareBadLinkError" = "Nessuna condivisione associata a questo link"; + +/* loco:66ffbbcd294f6022e60948f3 */ +"publicShareImportationInProgress" = "Download in corso nella cartella selezionata"; + +/* loco:6707ca3c9b41ff114e052962 */ +"publicShareLinkValidityDescription" = "Questo link è valido fino al %@"; + +/* loco:66e0295f01da3c1ab90c2d72 */ +"publicShareOutdatedLinkDescription" = "Il link è stato disattivato o è scaduto.\nPer accedere ai file, invia un messaggio all’utente che ha condiviso il link in modo che possa riattivarlo."; + +/* loco:66e028cb3cd41df03c0a7003 */ +"publicShareOutdatedLinkTitle" = "I file non sono più disponibili"; + +/* loco:66d05b05fe43da3cb8009652 */ +"publicSharePasswordNeededDescription" = "Inserisci la password che ti è stata fornita per accedere al contenuto."; + +/* loco:66d05b5928ec407b32087892 */ +"publicSharePasswordNeededTitle" = "Contenuto sicuro"; + +/* loco:66dfe73543cef47d3e073e62 */ +"publicSharePasswordNotSupportedDescription" = "I link protetti da password non sono ancora disponibili nell’applicazione mobile."; + /* loco:617a50f1744434292a2f07a2 */ "publicSharedLinkTitle" = "Link di condivisione pubblica"; From 5b8f4c05ddf575d79b74cbbdfca4af8c79dd7622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 29 Oct 2024 14:38:56 +0100 Subject: [PATCH 037/129] chore: Bump DB version as it was bumped on master --- .../Data/Cache/DriveFileManager/DriveFileManagerConstants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift index b65c3f6d2..bfe181e39 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift @@ -28,7 +28,7 @@ public enum RealmSchemaVersion { static let upload: UInt64 = 21 /// Current version of the Drive Realm - static let drive: UInt64 = 12 + static let drive: UInt64 = 13 } public class DriveFileManagerConstants { From a5aa0a17de30bdf3bb3222781e06ad9457cfacfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 29 Oct 2024 15:14:24 +0100 Subject: [PATCH 038/129] chore: Making use of i18n --- .../Files/External/LockedFolderViewController.swift | 7 +++---- .../Files/External/UnavaillableFolderViewController.swift | 6 ++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift index e8be9707d..9bc2b487e 100644 --- a/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift @@ -29,15 +29,14 @@ class LockedFolderViewController: BaseInfoViewController { super.viewDidLoad() centerImageView.image = KDriveResourcesAsset.lockExternal.image - titleLabel.text = "Protected content" - descriptionLabel.text = "Password-protected links are not yet available on the mobile app." + titleLabel.text = KDriveCoreStrings.Localizable.publicSharePasswordNeededTitle + descriptionLabel.text = KDriveCoreStrings.Localizable.publicSharePasswordNotSupportedDescription setupOpenWebButton() } private func setupOpenWebButton() { - // TODO: i18n - openWebButton.setTitle("Open in Browser", for: .normal) + openWebButton.setTitle(KDriveCoreStrings.Localizable.buttonOpenInBrowser, for: .normal) openWebButton.translatesAutoresizingMaskIntoConstraints = false openWebButton.addTarget(self, action: #selector(openWebBrowser), for: .touchUpInside) diff --git a/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift index f1d9b79fb..752966a33 100644 --- a/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift +++ b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift @@ -24,9 +24,7 @@ class UnavaillableFolderViewController: BaseInfoViewController { super.viewDidLoad() centerImageView.image = KDriveResourcesAsset.ufo.image - titleLabel.text = "Content Unavailable" - descriptionLabel - .text = - "The link has been deactivated or has expired. To access the files, send a message to the user who shared the link with you to reactivate it." + titleLabel.text = KDriveStrings.Localizable.dropboxPublicShareOutdatedTitle + descriptionLabel.text = KDriveStrings.Localizable.dropboxPublicShareOutdatedDescription } } From 3cca05b10d5c8dd5c1bfae2687726f775788df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 29 Oct 2024 20:10:29 +0100 Subject: [PATCH 039/129] feat: Added last error type to trigger the correct screen --- kDrive/AppDelegate.swift | 4 +++- kDriveCore/Data/Api/PublicShareApiFetcher.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 4942350e0..54195f04a 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -107,8 +107,10 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { Task { try! await Task.sleep(nanoseconds:5_000_000_000) print("coucou") + // a public share expired + let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") // a public share password protected - let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") +// let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") await UniversalLinksHelper.handleURL(somePublicShare!) } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index f7b28e312..4db1d43d0 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -26,7 +26,7 @@ import Kingfisher /// Server can notify us of publicShare limitations. public enum PublicShareLimitation: String { case passwordProtected = "password_not_valid" - case expired // TODO: + case expired = "link_is_not_valid" } public class PublicShareApiFetcher: ApiFetcher { From 4b0f126cd57eddec37cd8027f607d9852049be65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 30 Oct 2024 08:20:17 +0100 Subject: [PATCH 040/129] chore: Sonar feedback --- kDrive/AppRouter.swift | 24 +++++-------------- kDrive/Utils/UniversalLinksHelper.swift | 4 ++-- .../Data/Api/PublicShareApiFetcher.swift | 5 +--- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index df8176374..3125a405d 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -587,12 +587,8 @@ public struct AppRouter: AppNavigable { @MainActor public func presentPublicShareLocked(_ destinationURL: URL) { guard let window, - let rootViewController = window.rootViewController else { - fatalError("TODO: lazy load a rootViewController") - } - - guard let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("Root is not a MainTabViewController") + let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("TODO: fix offline routing - presentPublicShareLocked") return } @@ -615,12 +611,8 @@ public struct AppRouter: AppNavigable { @MainActor public func presentPublicShareExpired() { guard let window, - let rootViewController = window.rootViewController else { - fatalError("TODO: lazy load a rootViewController") - } - - guard let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("Root is not a MainTabViewController") + let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("TODO: fix offline routing - presentPublicShareExpired") return } @@ -647,12 +639,8 @@ public struct AppRouter: AppNavigable { apiFetcher: PublicShareApiFetcher ) { guard let window, - let rootViewController = window.rootViewController else { - fatalError("TODO: lazy load a rootViewController") - } - - guard let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("Root is not a MainTabViewController") + let rootViewController = window.rootViewController as? MainTabViewController else { + fatalError("TODO: fix offline routing - presentPublicShare") return } diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index 0afcd7e9c..c0ffd9a2f 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -75,7 +75,7 @@ enum UniversalLinksHelper { // Public share link regex let shareLink = Link.publicShareLink let matches = shareLink.regex.matches(in: path) - if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode, publicShareURL: url) { + if await processPublicShareLink(matches: matches, publicShareURL: url) { return true } @@ -91,7 +91,7 @@ enum UniversalLinksHelper { return false } - private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode, publicShareURL: URL) async -> Bool { + private static func processPublicShareLink(matches: [[String]], publicShareURL: URL) async -> Bool { guard let firstMatch = matches.first, let driveId = firstMatch[safe: 1], let driveIdInt = Int(driveId), diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 4db1d43d0..8005dc1f2 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -35,10 +35,7 @@ public class PublicShareApiFetcher: ApiFetcher { } /// All status including 401 are handled by our code. A locked public share will 401, therefore we need to support it. - private static var handledHttpStatus: Set = { - var allStatus = Set(200 ... 500) - return allStatus - }() + private static var handledHttpStatus = Set(200 ... 500) override public func perform(request: DataRequest, decoder: JSONDecoder = ApiFetcher.decoder) async throws -> ValidServerResponse { From ba63bfd351573fd7fa4211afd8d9133a26c3e44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 30 Oct 2024 08:55:26 +0100 Subject: [PATCH 041/129] fix: Fix i18n keys --- .../Files/External/UnavaillableFolderViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift index 752966a33..b39a118af 100644 --- a/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift +++ b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift @@ -24,7 +24,7 @@ class UnavaillableFolderViewController: BaseInfoViewController { super.viewDidLoad() centerImageView.image = KDriveResourcesAsset.ufo.image - titleLabel.text = KDriveStrings.Localizable.dropboxPublicShareOutdatedTitle - descriptionLabel.text = KDriveStrings.Localizable.dropboxPublicShareOutdatedDescription + titleLabel.text = KDriveStrings.Localizable.publicShareOutdatedLinkTitle + descriptionLabel.text = KDriveStrings.Localizable.publicShareOutdatedLinkDescription } } From 1861f13eb95ffa5120151c87e603e4b057fbc9bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 30 Oct 2024 13:16:56 +0100 Subject: [PATCH 042/129] feat: Public Share multi selection actions are set to download only --- .../File List/FileListViewController.swift | 6 ++- .../Files/File List/FileListViewModel.swift | 2 +- .../MultipleSelectionFileListViewModel.swift | 29 ++++++++--- ...nFloatingPanelViewController+Actions.swift | 44 ++++++++++++---- ...SelectionFloatingPanelViewController.swift | 51 ++++++++++++++++++- .../DownloadArchiveOperation.swift | 25 ++++++++- .../Data/DownloadQueue/DownloadQueue.swift | 26 ++++++++++ 7 files changed, 160 insertions(+), 23 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 2f06f81ec..f3352d37d 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -441,7 +441,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV floatingPanelViewController.set(contentViewController: trashFloatingPanelTableViewController) (floatingPanelViewController as? AdaptiveDriveFloatingPanelController)? .trackAndObserve(scrollView: trashFloatingPanelTableViewController.tableView) - case .multipleSelection: + case .multipleSelection(let downloadOnly): let allItemsSelected: Bool let exceptFileIds: [Int]? let selectedFiles: [File] @@ -467,6 +467,10 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV presentingParent: self ) + if downloadOnly { + selectViewController.actions = [.download] + } + floatingPanelViewController = AdaptiveDriveFloatingPanelController() floatingPanelViewController.set(contentViewController: selectViewController) (floatingPanelViewController as? AdaptiveDriveFloatingPanelController)? diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 10b8c0333..d329a4f2f 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -40,7 +40,7 @@ enum FileListBarButtonType { enum FileListQuickActionType { case file case trash - case multipleSelection + case multipleSelection(onlyDownload: Bool) } enum ControllerPresentationType { diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index 65ee438b3..c54d0bef5 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -23,35 +23,48 @@ import kDriveCore import kDriveResources struct MultipleSelectionAction: Equatable { - let id: Int + private let id: MultipleSelectionActionId let name: String let icon: KDriveResourcesImages var enabled = true + private enum MultipleSelectionActionId: Equatable { + case move + case delete + case more + case deletePermanently + case download + } + static func == (lhs: MultipleSelectionAction, rhs: MultipleSelectionAction) -> Bool { return lhs.id == rhs.id } static let move = MultipleSelectionAction( - id: 0, + id: MultipleSelectionActionId.move, name: KDriveResourcesStrings.Localizable.buttonMove, icon: KDriveResourcesAsset.folderSelect ) static let delete = MultipleSelectionAction( - id: 1, + id: MultipleSelectionActionId.delete, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) static let more = MultipleSelectionAction( - id: 2, + id: MultipleSelectionActionId.more, name: KDriveResourcesStrings.Localizable.buttonMenu, icon: KDriveResourcesAsset.menu ) static let deletePermanently = MultipleSelectionAction( - id: 3, + id: MultipleSelectionActionId.deletePermanently, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) + static let download = MultipleSelectionAction( + id: MultipleSelectionActionId.download, + name: KDriveResourcesStrings.Localizable.buttonDownload, + icon: KDriveResourcesAsset.menu + ) } @MainActor @@ -113,7 +126,7 @@ class MultipleSelectionFileListViewModel { self.driveFileManager = driveFileManager if driveFileManager.isPublicShare { - multipleSelectionActions = [] + multipleSelectionActions = [.download] } else { multipleSelectionActions = [.move, .delete, .more] } @@ -170,7 +183,9 @@ class MultipleSelectionFileListViewModel { } onPresentViewController?(.modal, alert, true) case .more: - onPresentQuickActionPanel?(Array(selectedItems), .multipleSelection) + onPresentQuickActionPanel?(Array(selectedItems), .multipleSelection(onlyDownload: false)) + case .download: + onPresentQuickActionPanel?(Array(selectedItems), .multipleSelection(onlyDownload: true)) default: break } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift index 951c30288..4747d4b48 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift @@ -122,7 +122,14 @@ extension MultipleSelectionFloatingPanelViewController { } group.leave() } - DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + + if let publicShareProxy = driveFileManager.publicShareProxy { + DownloadQueue.instance.addPublicShareToQueue(file: file, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } else { + DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + } } } } else { @@ -147,16 +154,33 @@ extension MultipleSelectionFloatingPanelViewController { downloadInProgress = true collectionView.reloadItems(at: [indexPath]) group.enter() - downloadArchivedFiles(downloadCellPath: indexPath) { result in - switch result { - case .success(let archiveUrl): - self.downloadedArchiveUrl = archiveUrl - self.success = true - case .failure(let error): - self.downloadError = error - self.success = false + + if let publicShareProxy = driveFileManager.publicShareProxy { + downloadPublicShareArchivedFiles(downloadCellPath: indexPath, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) { result in + switch result { + case .success(let archiveUrl): + self.downloadedArchiveUrl = archiveUrl + self.success = true + case .failure(let error): + self.downloadError = error + self.success = false + } + group.leave() + } + } else { + downloadArchivedFiles(downloadCellPath: indexPath) { result in + switch result { + case .success(let archiveUrl): + self.downloadedArchiveUrl = archiveUrl + self.success = true + case .failure(let error): + self.downloadError = error + self.success = false + } + group.leave() } - group.leave() } } } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 631b203c8..8c85850b6 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -48,7 +48,7 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro return currentDirectory.visibility == .isInSharedSpace || currentDirectory.visibility == .isSharedSpace } - var actions = FloatingPanelAction.listActions + var actions: [FloatingPanelAction] = [] init( driveFileManager: DriveFileManager, @@ -83,6 +83,8 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } func setupContent() { + guard actions.isEmpty else { return } + if sharedWithMe { actions = FloatingPanelAction.multipleSelectionSharedWithMeActions } else if allItemsSelected { @@ -139,7 +141,10 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } - func downloadArchivedFiles(downloadCellPath: IndexPath, completion: @escaping (Result) -> Void) { + func downloadPublicShareArchivedFiles(downloadCellPath: IndexPath, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + completion: @escaping (Result) -> Void) { Task { [proxyFiles = files.map { $0.proxify() }, currentProxyDirectory = currentDirectory.proxify()] in do { let archiveBody: ArchiveBody @@ -172,6 +177,48 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } + func downloadArchivedFiles(downloadCellPath: IndexPath, + completion: @escaping (Result) -> Void) { + Task { [proxyFiles = files.map { $0.proxify() }, currentProxyDirectory = currentDirectory.proxify()] in + do { + let archiveBody: ArchiveBody + if allItemsSelected { + archiveBody = .init(parentId: currentProxyDirectory.id, exceptFileIds: exceptFileIds) + } else { + archiveBody = .init(files: proxyFiles) + } + let response = try await self.driveFileManager.apiFetcher.buildArchive( + drive: driveFileManager.drive, + body: archiveBody + ) + currentArchiveId = response.uuid + guard let rootViewController = view.window?.rootViewController else { return } + DownloadQueue.instance + .observeArchiveDownloaded(rootViewController, archiveId: response.uuid) { _, archiveUrl, error in + if let archiveUrl { + completion(.success(archiveUrl)) + } else { + completion(.failure(error ?? .unknownError)) + } + } + + if let publicShareProxy = self.driveFileManager.publicShareProxy { + DownloadQueue.instance.addPublicShareArchiveToQueue(archiveId: response.uuid, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } else { + DownloadQueue.instance.addToQueue(archiveId: response.uuid, + driveId: self.driveFileManager.drive.id, + userId: accountManager.currentUserId) + } + + self.collectionView.reloadItems(at: [downloadCellPath]) + } catch { + completion(.failure(error as? DriveError ?? .unknownError)) + } + } + } + private static func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { _, _ in let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(53)) diff --git a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift index a49585b40..b2b2769e9 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift @@ -31,6 +31,7 @@ public class DownloadArchiveOperation: Operation { private let archiveId: String private let driveFileManager: DriveFileManager private let urlSession: FileDownloadSession + private let publicShareProxy: PublicShareProxy? private var progressObservation: NSKeyValueObservation? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid @@ -68,10 +69,14 @@ public class DownloadArchiveOperation: Operation { return true } - public init(archiveId: String, driveFileManager: DriveFileManager, urlSession: FileDownloadSession) { + public init(archiveId: String, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession, + publicShareProxy: PublicShareProxy? = nil) { self.archiveId = archiveId self.driveFileManager = driveFileManager self.urlSession = urlSession + self.publicShareProxy = publicShareProxy } // MARK: - Public methods @@ -103,7 +108,17 @@ public class DownloadArchiveOperation: Operation { } override public func main() { - DDLogInfo("[DownloadOperation] Downloading Archive of files \(archiveId) with session \(urlSession.identifier)") + if publicShareProxy == nil { + authenticatedDownload() + } else { + publicShareDownload() + } + } + + func publicShareDownload() { + DDLogInfo( + "[DownloadOperation] Downloading Archive of public share files \(archiveId) with session \(urlSession.identifier)" + ) let url = Endpoint.getArchive(drive: driveFileManager.drive, uuid: archiveId).url @@ -131,6 +146,12 @@ public class DownloadArchiveOperation: Operation { } } + func authenticatedDownload() { + DDLogInfo("[DownloadOperation] Downloading Archive of files \(archiveId) with session \(urlSession.identifier)") + + // TODO: missing imp + } + func downloadCompletion(url: URL?, response: URLResponse?, error: Error?) { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 0fd65e28b..11e711834 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -188,6 +188,32 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { } } + public func addPublicShareArchiveToQueue(archiveId: String, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy) { + Log.downloadQueue("addPublicShareArchiveToQueue archiveId:\(archiveId)") + dispatchQueue.async { + OperationQueueHelper.disableIdleTimer(true) + + let operation = DownloadArchiveOperation( + archiveId: archiveId, + driveFileManager: driveFileManager, + urlSession: self.bestSession, + publicShareProxy: publicShareProxy + ) + + operation.completionBlock = { + self.dispatchQueue.async { + self.archiveOperationsInQueue.removeValue(forKey: archiveId) + self.publishArchiveDownloaded(archiveId: archiveId, archiveUrl: operation.archiveUrl, error: operation.error) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + } + } + self.operationQueue.addOperation(operation) + self.archiveOperationsInQueue[archiveId] = operation + } + } + public func addToQueue(archiveId: String, driveId: Int, userId: Int) { Log.downloadQueue("addToQueue archiveId:\(archiveId)") dispatchQueue.async { From a79706e7c9a1fe8a1c2f7029c18e60ec887b89cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 11 Nov 2024 12:43:00 +0100 Subject: [PATCH 043/129] chore: PR Feedback --- kDriveCore/Utils/Logging.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kDriveCore/Utils/Logging.swift b/kDriveCore/Utils/Logging.swift index 29ce73b67..3a1b63316 100644 --- a/kDriveCore/Utils/Logging.swift +++ b/kDriveCore/Utils/Logging.swift @@ -81,7 +81,13 @@ public enum Logging { } private static func initNetworkLogging() { - Atlantis.start(hostName: ProcessInfo.processInfo.environment["hostname"]) + #if DEBUG + @InjectService var appContextService: AppContextServiceable + if !appContextService.isExtension, + appContextService.context != .appTests { + Atlantis.start(hostName: ProcessInfo.processInfo.environment["hostname"]) + } + #endif } private static func initSentry() { From 5ad6e99ccda3d561084bc04223ae1aaa4e1e5890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 11 Nov 2024 15:16:02 +0100 Subject: [PATCH 044/129] chore: PR Feedback --- .../External/BaseInfoViewController.swift | 35 +++++++------------ .../External/LockedFolderViewController.swift | 6 ++-- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift index 5f8b68624..41d270324 100644 --- a/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift +++ b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift @@ -16,24 +16,24 @@ along with this program. If not, see . */ +import InfomaniakCoreCommonUI +import InfomaniakCoreUIKit import kDriveCore import kDriveResources import UIKit class BaseInfoViewController: UIViewController { - let titleLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .headline) - label.textColor = KDriveResourcesAsset.primaryTextColor.color - label.numberOfLines = 1 + let titleLabel: IKLabel = { + let label = IKLabel() + label.style = .header1 + label.numberOfLines = 0 label.textAlignment = .center return label }() - let descriptionLabel: UILabel = { - let label = UILabel() - label.font = .preferredFont(forTextStyle: .body) - label.textColor = KDriveResourcesAsset.secondaryTextColor.color + let descriptionLabel: IKLabel = { + let label = IKLabel() + label.style = .body2 label.numberOfLines = 0 label.textAlignment = .center return label @@ -45,12 +45,6 @@ class BaseInfoViewController: UIViewController { return imageView }() - let closeButton: IKButton = { - let button = IKButton() - button.setImage(UIImage(named: "close"), for: .normal) - return button - }() - let containerView = UIView() override func viewDidLoad() { @@ -62,14 +56,9 @@ class BaseInfoViewController: UIViewController { } private func setupCloseButton() { - closeButton.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(closeButton) - - closeButton.addTarget(self, action: #selector(closeButtonPressed), for: .touchUpInside) - NSLayoutConstraint.activate([ - closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0), - closeButton.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 25) - ]) + let closeButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(closeButtonPressed)) + closeButton.accessibilityLabel = KDriveResourcesStrings.Localizable.buttonClose + navigationItem.leftBarButtonItem = closeButton } private func setupBody() { diff --git a/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift index 9bc2b487e..614f5cf7e 100644 --- a/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift +++ b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift @@ -44,11 +44,11 @@ class LockedFolderViewController: BaseInfoViewController { view.bringSubviewToFront(openWebButton) let leadingConstraint = openWebButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, - constant: 16) + constant: 25) leadingConstraint.priority = UILayoutPriority.defaultHigh let trailingConstraint = openWebButton.trailingAnchor.constraint( greaterThanOrEqualTo: view.trailingAnchor, - constant: -16 + constant: -25 ) trailingConstraint.priority = UILayoutPriority.defaultHigh let widthConstraint = openWebButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360) @@ -57,7 +57,7 @@ class LockedFolderViewController: BaseInfoViewController { openWebButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), leadingConstraint, trailingConstraint, - openWebButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + openWebButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -30), openWebButton.heightAnchor.constraint(equalToConstant: 60), widthConstraint ]) From 176ce723ee2c7d0c65be95f0ea0fb298d95d4378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 14 Nov 2024 18:52:22 +0100 Subject: [PATCH 045/129] chore: Remove VisualFormat --- .../Files/External/BaseInfoViewController.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift index 41d270324..440f2597f 100644 --- a/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift +++ b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift @@ -77,14 +77,12 @@ class BaseInfoViewController: UIViewController { containerView.addSubview(titleLabel) containerView.addSubview(descriptionLabel) - let views = ["titleLabel": titleLabel, - "descriptionLabel": descriptionLabel, - "centerImageView": centerImageView] - - let verticalConstraints = NSLayoutConstraint - .constraints(withVisualFormat: "V:|[centerImageView]-[titleLabel]-[descriptionLabel]|", - metrics: nil, - views: views) + let verticalConstraints = [ + centerImageView.topAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.topAnchor), + titleLabel.topAnchor.constraint(equalTo: centerImageView.bottomAnchor, constant: 8), + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + descriptionLabel.bottomAnchor.constraint(equalTo: containerView.safeAreaLayoutGuide.bottomAnchor) + ] let horizontalConstraints = [ titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), From ced6c1949cc7f4f4a1ecccbffa5b74a425e25d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Nov 2024 07:35:25 +0100 Subject: [PATCH 046/129] chore: Removed unnecessary title --- kDrive/AppRouter.swift | 3 +-- kDrive/UI/Controller/Files/FilePresenter.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 7b2b4ad38..38685cb79 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -657,9 +657,8 @@ public struct AppRouter: AppNavigable { return } - // TODO: i18n let configuration = FileListViewModel.Configuration(selectAllSupported: true, - rootTitle: "public share", + rootTitle: nil, emptyViewType: .emptyFolder, supportsDrop: false, leftBarButtons: [.cancel], diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index 820812773..ec133a8d4 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -146,9 +146,8 @@ final class FilePresenter { if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else if let publicShareProxy = driveFileManager.publicShareProxy { - // TODO: i18n let configuration = FileListViewModel.Configuration(selectAllSupported: true, - rootTitle: "public share", + rootTitle: nil, emptyViewType: .emptyFolder, supportsDrop: false, rightBarButtons: [.downloadAll], From 0513dfbbfc7c2a120c1738822007327737dd1f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Nov 2024 08:21:19 +0100 Subject: [PATCH 047/129] refactor: Split download action in manageable bits --- ...nFloatingPanelViewController+Actions.swift | 154 ++++++++++-------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift index 4747d4b48..f4772a1b3 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift @@ -100,87 +100,99 @@ extension MultipleSelectionFloatingPanelViewController { } private func downloadAction(group: DispatchGroup, at indexPath: IndexPath) { - if !allItemsSelected && - (files.allSatisfy { $0.convertedType == .image || $0.convertedType == .video } || files.count <= 1) { - for file in files { - if file.isDownloaded { - FileActionsHelper.save(file: file, from: self, showSuccessSnackBar: false) - } else { - guard let observerViewController = view.window?.rootViewController else { return } - downloadInProgress = true - collectionView.reloadItems(at: [indexPath]) - group.enter() - DownloadQueue.instance - .observeFileDownloaded(observerViewController, fileId: file.id) { [weak self] _, error in - guard let self else { return } - if error == nil { - Task { @MainActor in - FileActionsHelper.save(file: file, from: self, showSuccessSnackBar: false) - } - } else { - success = false - } - group.leave() - } + if !allItemsSelected, + files.allSatisfy { $0.convertedType == .image || $0.convertedType == .video } || files.count <= 1 { + downloadActionMediaOrSingleFile(group: group, at: indexPath) + } else { + downloadActionArchive(group: group, at: indexPath) + } + } + + private func downloadActionMediaOrSingleFile(group: DispatchGroup, at indexPath: IndexPath) { + for file in files { + guard !file.isDownloaded else { + FileActionsHelper.save(file: file, from: self, showSuccessSnackBar: false) + return + } - if let publicShareProxy = driveFileManager.publicShareProxy { - DownloadQueue.instance.addPublicShareToQueue(file: file, - driveFileManager: driveFileManager, - publicShareProxy: publicShareProxy) + guard let observerViewController = view.window?.rootViewController else { + return + } + + downloadInProgress = true + collectionView.reloadItems(at: [indexPath]) + group.enter() + DownloadQueue.instance + .observeFileDownloaded(observerViewController, fileId: file.id) { [weak self] _, error in + guard let self else { return } + if error == nil { + Task { @MainActor in + FileActionsHelper.save(file: file, from: self, showSuccessSnackBar: false) + } } else { - DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + success = false } + group.leave() } + + if let publicShareProxy = driveFileManager.publicShareProxy { + DownloadQueue.instance.addPublicShareToQueue(file: file, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } else { + DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) } + } + } + + private func downloadActionArchive(group: DispatchGroup, at indexPath: IndexPath) { + if downloadInProgress, + let currentArchiveId, + let operation = DownloadQueue.instance.archiveOperationsInQueue[currentArchiveId] { + group.enter() + let alert = AlertTextViewController( + title: KDriveResourcesStrings.Localizable.cancelDownloadTitle, + message: KDriveResourcesStrings.Localizable.cancelDownloadDescription, + action: KDriveResourcesStrings.Localizable.buttonYes, + destructive: true + ) { + operation.cancel() + self.downloadError = .taskCancelled + self.success = false + group.leave() + } + present(alert, animated: true) } else { - if downloadInProgress, - let currentArchiveId, - let operation = DownloadQueue.instance.archiveOperationsInQueue[currentArchiveId] { - group.enter() - let alert = AlertTextViewController( - title: KDriveResourcesStrings.Localizable.cancelDownloadTitle, - message: KDriveResourcesStrings.Localizable.cancelDownloadDescription, - action: KDriveResourcesStrings.Localizable.buttonYes, - destructive: true - ) { - operation.cancel() - self.downloadError = .taskCancelled - self.success = false + downloadedArchiveUrl = nil + downloadInProgress = true + collectionView.reloadItems(at: [indexPath]) + group.enter() + + if let publicShareProxy = driveFileManager.publicShareProxy { + downloadPublicShareArchivedFiles(downloadCellPath: indexPath, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) { result in + switch result { + case .success(let archiveUrl): + self.downloadedArchiveUrl = archiveUrl + self.success = true + case .failure(let error): + self.downloadError = error + self.success = false + } group.leave() } - present(alert, animated: true) } else { - downloadedArchiveUrl = nil - downloadInProgress = true - collectionView.reloadItems(at: [indexPath]) - group.enter() - - if let publicShareProxy = driveFileManager.publicShareProxy { - downloadPublicShareArchivedFiles(downloadCellPath: indexPath, - driveFileManager: driveFileManager, - publicShareProxy: publicShareProxy) { result in - switch result { - case .success(let archiveUrl): - self.downloadedArchiveUrl = archiveUrl - self.success = true - case .failure(let error): - self.downloadError = error - self.success = false - } - group.leave() - } - } else { - downloadArchivedFiles(downloadCellPath: indexPath) { result in - switch result { - case .success(let archiveUrl): - self.downloadedArchiveUrl = archiveUrl - self.success = true - case .failure(let error): - self.downloadError = error - self.success = false - } - group.leave() + downloadArchivedFiles(downloadCellPath: indexPath) { result in + switch result { + case .success(let archiveUrl): + self.downloadedArchiveUrl = archiveUrl + self.success = true + case .failure(let error): + self.downloadError = error + self.success = false } + group.leave() } } } From 7c48ef29fcc593c038ae612ed5f606f1d718dd97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 27 Nov 2024 09:05:27 +0100 Subject: [PATCH 048/129] chore: PR notes --- kDrive/AppDelegate.swift | 10 ++++++---- .../MultipleSelectionFloatingPanelViewController.swift | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 54195f04a..d82f00173 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -105,13 +105,15 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // swiftlint:disable force_try Task { - try! await Task.sleep(nanoseconds:5_000_000_000) + try! await Task.sleep(nanoseconds: 5_000_000_000) print("coucou") // a public share expired - let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") +// let somePublicShare = URL(string:"https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") // a public share password protected -// let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") - +// let somePublicShare = URL(string:"https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") + // a valid public share + let somePublicShare = + URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") await UniversalLinksHelper.handleURL(somePublicShare!) } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 8c85850b6..3284d1369 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -141,6 +141,7 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } + // TODO: make it work func downloadPublicShareArchivedFiles(downloadCellPath: IndexPath, driveFileManager: DriveFileManager, publicShareProxy: PublicShareProxy, From de883e07e3c012f322484986b2e10039900a154a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 28 Nov 2024 14:10:16 +0100 Subject: [PATCH 049/129] fix: Fix broken merge --- kDriveCore/Data/Models/Drive/Drive.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/kDriveCore/Data/Models/Drive/Drive.swift b/kDriveCore/Data/Models/Drive/Drive.swift index d7069182e..00031e3e8 100644 --- a/kDriveCore/Data/Models/Drive/Drive.swift +++ b/kDriveCore/Data/Models/Drive/Drive.swift @@ -187,9 +187,6 @@ public final class Drive: Object, Codable { // Also the Realm sort can crash if managed by realm let fileCategoriesIds = file.categories.sorted { $0.addedAt.compare($1.addedAt) == .orderedAscending }.map(\.categoryId) let filteredCategories = categories.filter("id IN %@", fileCategoriesIds) - - // Sort the categories - let filteredCategories = categories.filter("id IN %@", fileCategoriesIds) return fileCategoriesIds.compactMap { id in filteredCategories.first { $0.id == id } } } From 424a60c5849583a94c6f62a88eb079b23d308be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 28 Nov 2024 15:55:40 +0100 Subject: [PATCH 050/129] feat: Public share archive request working --- ...tionFloatingPanelViewController+Actions.swift | 1 - ...pleSelectionFloatingPanelViewController.swift | 16 +++++++++------- kDriveCore/Data/Api/Endpoint+Share.swift | 5 +++++ kDriveCore/Data/Api/PublicShareApiFetcher.swift | 12 ++++++++++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift index f4772a1b3..c79d1bf20 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift @@ -170,7 +170,6 @@ extension MultipleSelectionFloatingPanelViewController { if let publicShareProxy = driveFileManager.publicShareProxy { downloadPublicShareArchivedFiles(downloadCellPath: indexPath, - driveFileManager: driveFileManager, publicShareProxy: publicShareProxy) { result in switch result { case .success(let archiveUrl): diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 3284d1369..7f5ebec9b 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -141,9 +141,8 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } - // TODO: make it work + // TODO:  make it work func downloadPublicShareArchivedFiles(downloadCellPath: IndexPath, - driveFileManager: DriveFileManager, publicShareProxy: PublicShareProxy, completion: @escaping (Result) -> Void) { Task { [proxyFiles = files.map { $0.proxify() }, currentProxyDirectory = currentDirectory.proxify()] in @@ -154,8 +153,10 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } else { archiveBody = .init(files: proxyFiles) } - let response = try await driveFileManager.apiFetcher.buildArchive( - drive: driveFileManager.drive, + + let response = try await PublicShareApiFetcher().buildPublicShareArchive( + driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, body: archiveBody ) currentArchiveId = response.uuid @@ -168,9 +169,10 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro completion(.failure(error ?? .unknownError)) } } - DownloadQueue.instance.addToQueue(archiveId: response.uuid, - driveId: self.driveFileManager.drive.id, - userId: accountManager.currentUserId) + DownloadQueue.instance.addPublicShareArchiveToQueue(archiveId: response.uuid, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + self.collectionView.reloadItems(at: [downloadCellPath]) } catch { completion(.failure(error as? DriveError ?? .unknownError)) diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index ee80590b4..ef06c45ba 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -84,6 +84,11 @@ public extension Endpoint { return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/download") } + /// Archive files from a share link + static func publicShareArchive(driveId: Int, linkUuid: String) -> Endpoint { + return shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/archive") + } + func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 8005dc1f2..7fb3fd4d9 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -90,4 +90,16 @@ public extension PublicShareApiFetcher { let shareLinkFiles: ValidServerResponse<[File]> = try await perform(request: request) return shareLinkFiles } + + func buildPublicShareArchive(driveId: Int, + linkUuid: String, + body: ArchiveBody) async throws -> DownloadArchiveResponse { + let shareLinkArchiveUrl = Endpoint.publicShareArchive(driveId: driveId, linkUuid: linkUuid).url + let request = Session.default.request(shareLinkArchiveUrl, + method: .post, + parameters: body, + encoder: JSONParameterEncoder.convertToSnakeCase) + let archiveResponse: ValidServerResponse = try await perform(request: request) + return archiveResponse.validApiResponse.data + } } From 89e00af976d657f4a1f54af47f65e860f0608404 Mon Sep 17 00:00:00 2001 From: adrien-coye Date: Fri, 29 Nov 2024 15:41:17 +0100 Subject: [PATCH 051/129] feat: External links upsale sheet (#1309) --- kDrive/AppDelegate.swift | 26 +- .../Contents.json | 16 + .../img-kDrive.svg | 355 ++++++++++++++++++ .../upsale-header.imageset/Contents.json | 16 + .../upsale-header.imageset/drive-rocket.svg | 30 ++ .../Create File/FloatingPanelUtils.swift | 2 +- ...lusButtonFloatingPanelViewController.swift | 2 +- .../DriveUpdateRequiredViewController.swift | 4 +- .../DropBox/ManageDropBoxViewController.swift | 2 +- .../File List/FileListViewController.swift | 4 +- ...leActionsFloatingPanelViewController.swift | 2 +- .../RightsSelectionViewController.swift | 2 +- .../ShareAndRightsViewController.swift | 2 +- .../Files/RootMenuViewController.swift | 2 +- .../Save File/SaveFileViewController.swift | 2 +- .../SelectFolderViewController.swift | 2 +- .../Controller/Home/HomeViewController.swift | 2 +- .../Controller/Menu/MenuViewController.swift | 2 +- .../Controller/Menu/StoreViewController.swift | 2 +- .../View/Files/FileCollectionViewCell.swift | 2 +- .../SearchFilterCollectionViewCell.swift | 2 +- .../Files/Upload/UploadTableViewCell.swift | 2 +- .../PhotoList/PhotoCollectionViewCell.swift | 2 +- .../Upsale/NoDriveUpsaleViewController.swift | 43 +++ .../UpsaleFloatingPanelController.swift | 62 +++ .../UI/View/Upsale/UpsaleViewController.swift | 228 +++++++++++ kDriveCore/UI/Alert/AlertViewController.swift | 2 +- kDriveCore/UI/IKLargeButton.swift | 2 +- kDriveCore/UI/UIConstants.swift | 61 ++- 29 files changed, 833 insertions(+), 48 deletions(-) create mode 100644 kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/Contents.json create mode 100644 kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/img-kDrive.svg create mode 100644 kDrive/Resources/Assets.xcassets/upsale-header.imageset/Contents.json create mode 100644 kDrive/Resources/Assets.xcassets/upsale-header.imageset/drive-rocket.svg create mode 100644 kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift create mode 100644 kDrive/UI/View/Upsale/UpsaleFloatingPanelController.swift create mode 100644 kDrive/UI/View/Upsale/UpsaleViewController.swift diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 54195f04a..2247a0e86 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -105,14 +105,24 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // swiftlint:disable force_try Task { - try! await Task.sleep(nanoseconds:5_000_000_000) - print("coucou") - // a public share expired - let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") - // a public share password protected -// let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") - - await UniversalLinksHelper.handleURL(somePublicShare!) + try! await Task.sleep(nanoseconds: 5_000_000_000) + + @InjectService var router: AppNavigable + //let upsaleViewController = UpsaleViewController() + let noDriveViewController = NoDriveUpsaleViewController() + let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) + router.topMostViewController?.present(floatingPanel, animated: true, completion: nil) + + /* Temp code to test out the feature. TODO: Remove later + // a public share expired + let somePublicShare = + URL(string: "https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") + // a public share password protected + // let somePublicShare = URL(string: + // "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") + + await UniversalLinksHelper.handleURL(somePublicShare!) + */ } return true diff --git a/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/Contents.json new file mode 100644 index 000000000..8506f4766 --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "img-kDrive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/img-kDrive.svg b/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/img-kDrive.svg new file mode 100644 index 000000000..8d930158f --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/upsale-header-noDrive.imageset/img-kDrive.svg @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDrive/Resources/Assets.xcassets/upsale-header.imageset/Contents.json b/kDrive/Resources/Assets.xcassets/upsale-header.imageset/Contents.json new file mode 100644 index 000000000..51dcff292 --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/upsale-header.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "drive-rocket.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/kDrive/Resources/Assets.xcassets/upsale-header.imageset/drive-rocket.svg b/kDrive/Resources/Assets.xcassets/upsale-header.imageset/drive-rocket.svg new file mode 100644 index 000000000..63f2027df --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/upsale-header.imageset/drive-rocket.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift index e5ca7bc58..8a59d11fd 100644 --- a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift +++ b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift @@ -25,7 +25,7 @@ class DriveFloatingPanelController: FloatingPanelController { init() { super.init(delegate: nil) let appearance = SurfaceAppearance() - appearance.cornerRadius = UIConstants.floatingPanelCornerRadius + appearance.cornerRadius = UIConstants.FloatingPanel.cornerRadius appearance.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color surfaceView.appearance = appearance surfaceView.grabberHandlePadding = 16 diff --git a/kDrive/UI/Controller/Create File/PlusButtonFloatingPanelViewController.swift b/kDrive/UI/Controller/Create File/PlusButtonFloatingPanelViewController.swift index 1f0c49b12..086a02610 100644 --- a/kDrive/UI/Controller/Create File/PlusButtonFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Create File/PlusButtonFloatingPanelViewController.swift @@ -147,7 +147,7 @@ class PlusButtonFloatingPanelViewController: UITableViewController, FloatingPane override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if indexPath.row == 0 && indexPath.section == 0 { - return UIConstants.floatingPanelHeaderHeight + return UIConstants.FloatingPanel.headerHeight } else { return UITableView.automaticDimension } diff --git a/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift b/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift index 04e006511..fb09543d7 100644 --- a/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift +++ b/kDrive/UI/Controller/DriveUpdateRequiredViewController.swift @@ -35,8 +35,8 @@ class DriveUpdateRequiredViewController: UIViewController { buttonStyle: .init( background: Color(largeButtonStyle.backgroundColor), textStyle: .init(font: Font(largeButtonStyle.titleFont), color: Color(largeButtonStyle.titleColor)), - height: 60, - radius: UIConstants.buttonCornerRadius + height: UIConstants.Button.largeHeight, + radius: UIConstants.Button.cornerRadius ) ) }() diff --git a/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift b/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift index 058caed16..7878c0c41 100644 --- a/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift +++ b/kDrive/UI/Controller/Files/DropBox/ManageDropBoxViewController.swift @@ -67,7 +67,7 @@ class ManageDropBoxViewController: UIViewController, UITableViewDelegate, UITabl tableView.register(cellView: DropBoxDisableTableViewCell.self) tableView.register(cellView: DropBoxLinkTableViewCell.self) tableView.register(cellView: NewFolderSettingsTableViewCell.self) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) tableView.sectionHeaderHeight = 0 tableView.sectionFooterHeight = 16 diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 98f5e6374..1b818956b 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -130,7 +130,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerViewIdentifier ) - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color (collectionView as? SwipableCollectionView)?.swipeDataSource = self (collectionView as? SwipableCollectionView)?.swipeDelegate = self @@ -814,7 +814,7 @@ extension FileListViewController: UICollectionViewDelegateFlowLayout { switch viewModel.listStyle { case .list: // Important: subtract safe area insets - return CGSize(width: effectiveContentWidth, height: UIConstants.fileListCellHeight) + return CGSize(width: effectiveContentWidth, height: UIConstants.FileList.cellHeight) case .grid: // Adjust cell size based on screen size let cellWidth = floor((effectiveContentWidth - gridInnerSpacing * CGFloat(gridColumns - 1)) / CGFloat(gridColumns)) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index e60b39b37..33a74e12c 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -355,7 +355,7 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { case .header: let itemSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .absolute(UIConstants.fileListCellHeight) + heightDimension: .absolute(UIConstants.FileList.cellHeight) ) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) diff --git a/kDrive/UI/Controller/Files/Rights and Share/RightsSelectionViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/RightsSelectionViewController.swift index e7f164ae1..bfb6deae8 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/RightsSelectionViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/RightsSelectionViewController.swift @@ -100,7 +100,7 @@ class RightsSelectionViewController: UIViewController { super.viewDidLoad() tableView.register(cellView: RightsSelectionTableViewCell.self) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) navigationController?.setInfomaniakAppearanceNavigationBar() navigationItem.leftBarButtonItem = UIBarButtonItem( diff --git a/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift b/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift index ef19b0877..e8fcdf7c7 100644 --- a/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift +++ b/kDrive/UI/Controller/Files/Rights and Share/ShareAndRightsViewController.swift @@ -60,7 +60,7 @@ class ShareAndRightsViewController: UIViewController { tableView.register(cellView: InviteUserTableViewCell.self) tableView.register(cellView: UsersAccessTableViewCell.self) tableView.register(cellView: ShareLinkTableViewCell.self) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) updateShareList() hideKeyboardWhenTappedAround() diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift index 31f0ae1cf..99e7b00a0 100644 --- a/kDrive/UI/Controller/Files/RootMenuViewController.swift +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -110,7 +110,7 @@ class RootMenuViewController: CustomLargeTitleCollectionViewController, SelectSw navigationItem.rightBarButtonItem = FileListBarButton(type: .search, target: self, action: #selector(presentSearch)) collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) collectionView.refreshControl = refreshControl collectionView.register(RootMenuCell.self, forCellWithReuseIdentifier: RootMenuCell.identifier) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 5a099f78c..acdc1a034 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -139,7 +139,7 @@ class SaveFileViewController: UIViewController { tableView.register(cellView: ImportingTableViewCell.self) tableView.register(cellView: LocationTableViewCell.self) tableView.register(cellView: PhotoFormatTableViewCell.self) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listFloatingButtonPaddingBottom, right: 0) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.floatingButtonPaddingBottom, right: 0) tableView.sectionHeaderHeight = UITableView.automaticDimension tableView.estimatedSectionHeaderHeight = 50 hideKeyboardWhenTappedAround() diff --git a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift index cec483a03..28f276d54 100644 --- a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift @@ -73,7 +73,7 @@ class SelectFolderViewController: FileListViewController { override func viewDidLoad() { super.viewDidLoad() - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listFloatingButtonPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.floatingButtonPaddingBottom, right: 0) view.addSubview(selectFolderButton) diff --git a/kDrive/UI/Controller/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift index c8cd24c9a..d61de09fb 100644 --- a/kDrive/UI/Controller/Home/HomeViewController.swift +++ b/kDrive/UI/Controller/Home/HomeViewController.swift @@ -153,7 +153,7 @@ class HomeViewController: CustomLargeTitleCollectionViewController, UpdateAccoun collectionView.collectionViewLayout = createLayout() collectionView.dataSource = self collectionView.delegate = self - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) collectionView.refreshControl = refreshControl navigationItem.hideBackButtonText() diff --git a/kDrive/UI/Controller/Menu/MenuViewController.swift b/kDrive/UI/Controller/Menu/MenuViewController.swift index ae38221e6..1d7b50166 100644 --- a/kDrive/UI/Controller/Menu/MenuViewController.swift +++ b/kDrive/UI/Controller/Menu/MenuViewController.swift @@ -92,7 +92,7 @@ final class MenuViewController: UITableViewController, SelectSwitchDriveDelegate tableView.register(cellView: MenuTableViewCell.self) tableView.register(cellView: MenuTopTableViewCell.self) tableView.register(cellView: UploadsInProgressTableViewCell.self) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) updateTableContent() diff --git a/kDrive/UI/Controller/Menu/StoreViewController.swift b/kDrive/UI/Controller/Menu/StoreViewController.swift index e22017e5b..c164a5285 100644 --- a/kDrive/UI/Controller/Menu/StoreViewController.swift +++ b/kDrive/UI/Controller/Menu/StoreViewController.swift @@ -86,7 +86,7 @@ final class StoreViewController: UICollectionViewController, SceneStateRestorabl collectionView.register(supplementaryView: StoreHelpFooter.self, forSupplementaryViewOfKind: .footer) collectionView.collectionViewLayout = createLayout() collectionView.allowsSelection = false - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.listPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) // Set up delegates StoreManager.shared.delegate = self diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 20294df74..5f55ada8e 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -152,7 +152,7 @@ protocol FileCellDelegate: AnyObject { // Configure placeholder imageView.image = nil imageView.contentMode = .scaleAspectFill - imageView.layer.cornerRadius = UIConstants.imageCornerRadius + imageView.layer.cornerRadius = UIConstants.Image.cornerRadius imageView.layer.masksToBounds = true imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color diff --git a/kDrive/UI/View/Files/Search/SearchFilterCollectionViewCell.swift b/kDrive/UI/View/Files/Search/SearchFilterCollectionViewCell.swift index 9b99caa88..576cff401 100644 --- a/kDrive/UI/View/Files/Search/SearchFilterCollectionViewCell.swift +++ b/kDrive/UI/View/Files/Search/SearchFilterCollectionViewCell.swift @@ -37,7 +37,7 @@ class SearchFilterCollectionViewCell: UICollectionViewCell { override func awakeFromNib() { super.awakeFromNib() - contentView.layer.cornerRadius = UIConstants.buttonCornerRadius + contentView.layer.cornerRadius = UIConstants.Button.cornerRadius contentView.clipsToBounds = true removeButton.accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDelete } diff --git a/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift b/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift index c751dbb69..9e8c20aa3 100644 --- a/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift +++ b/kDrive/UI/View/Files/Upload/UploadTableViewCell.swift @@ -96,7 +96,7 @@ final class UploadTableViewCell: InsetTableViewCell { private func addThumbnail(image: UIImage) { Task { @MainActor in - self.cardContentView.iconView.layer.cornerRadius = UIConstants.imageCornerRadius + self.cardContentView.iconView.layer.cornerRadius = UIConstants.Image.cornerRadius self.cardContentView.iconView.contentMode = .scaleAspectFill self.cardContentView.iconView.layer.masksToBounds = true self.cardContentView.iconViewHeightConstraint.constant = 38 diff --git a/kDrive/UI/View/Menu/PhotoList/PhotoCollectionViewCell.swift b/kDrive/UI/View/Menu/PhotoList/PhotoCollectionViewCell.swift index bbfebf691..26478cbf4 100644 --- a/kDrive/UI/View/Menu/PhotoList/PhotoCollectionViewCell.swift +++ b/kDrive/UI/View/Menu/PhotoList/PhotoCollectionViewCell.swift @@ -28,6 +28,6 @@ class PhotoCollectionViewCell: UICollectionViewCell { override func awakeFromNib() { super.awakeFromNib() - image.layer.cornerRadius = UIConstants.imageCornerRadius + image.layer.cornerRadius = UIConstants.Image.cornerRadius } } diff --git a/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift new file mode 100644 index 000000000..ad7e0844c --- /dev/null +++ b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift @@ -0,0 +1,43 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +public class NoDriveUpsaleViewController: UpsaleViewController { + override func configureButtons() { + dismissButton.style = .primaryButton + freeTrialButton.setTitle(KDriveStrings.Localizable.obtainkDriveAdFreeTrialButton, for: .normal) + freeTrialButton.addTarget(self, action: #selector(freeTrial), for: .touchUpInside) + + dismissButton.style = .secondaryButton + dismissButton.setTitle(KDriveStrings.Localizable.buttonLater, for: .normal) + dismissButton.addTarget(self, action: #selector(dismissViewController), for: .touchUpInside) + } + + override func configureHeader() { + titleImageView.contentMode = .scaleAspectFit + titleImageView.image = KDriveResourcesAsset.upsaleHeaderNoDrive.image + } + + @objc public func dismissViewController() { + dismiss(animated: true, completion: nil) + } +} diff --git a/kDrive/UI/View/Upsale/UpsaleFloatingPanelController.swift b/kDrive/UI/View/Upsale/UpsaleFloatingPanelController.swift new file mode 100644 index 000000000..9fcc70fac --- /dev/null +++ b/kDrive/UI/View/Upsale/UpsaleFloatingPanelController.swift @@ -0,0 +1,62 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 FloatingPanel +import InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +class UpsaleFloatingPanelController: AdaptiveDriveFloatingPanelController { + private let upsaleViewController: UpsaleViewController + + init(upsaleViewController: UpsaleViewController) { + self.upsaleViewController = upsaleViewController + + super.init() + + set(contentViewController: upsaleViewController) + trackAndObserve(scrollView: upsaleViewController.scrollView) + + surfaceView.grabberHandle.isHidden = true + surfaceView.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + } +} + +/// A dedicated layout that maintains a custom static height +class UpsaleFloatingPanelLayout: FloatingPanelLayout { + var position: FloatingPanelPosition = .bottom + + var initialState: FloatingPanelState = .full + + var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] { + return [ + .full: FloatingPanelLayoutAnchor(absoluteInset: height + 60, edge: .bottom, referenceGuide: .superview) + ] + } + + var height: CGFloat = 0 + + init(height: CGFloat) { + self.height = height + } + + func backdropAlpha(for state: FloatingPanelState) -> CGFloat { + return 0.0 + } +} diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift new file mode 100644 index 000000000..bc1c28f40 --- /dev/null +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -0,0 +1,228 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +public class UpsaleViewController: UIViewController { + let titleImageView = UIImageView() + + let titleLabel: UILabel = { + let label = IKLabel() + label.style = .header2 + label.numberOfLines = 0 + label.textAlignment = .center + label.text = KDriveStrings.Localizable.obtainkDriveAdTitle + return label + }() + + let descriptionLabel: UILabel = { + let label = IKLabel() + label.style = .subtitle1 + label.textColor = KDriveResourcesAsset.primaryTextColor.color + label.numberOfLines = 0 + label.textAlignment = .center + label.text = KDriveStrings.Localizable.obtainkDriveAdDescription + return label + }() + + let freeTrialButton = IKLargeButton(frame: .zero) + + let dismissButton = IKLargeButton(frame: .zero) + + let scrollView = UIScrollView() + + let containerView = UIView() + + let bulletPointsView = UIView() + + override public func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color + configureButtons() + configureHeader() + setupBody() + layoutStackView() + } + + func configureHeader() { + titleImageView.contentMode = .scaleAspectFit + titleImageView.image = KDriveResourcesAsset.upsaleHeader.image + } + + func configureButtons() { + freeTrialButton.style = .primaryButton + freeTrialButton.setTitle(KDriveStrings.Localizable.obtainkDriveAdFreeTrialButton, for: .normal) + freeTrialButton.addTarget(self, action: #selector(freeTrial), for: .touchUpInside) + + dismissButton.style = .secondaryButton + dismissButton.setTitle(KDriveStrings.Localizable.obtainkDriveAdAlreadyGotAccount, for: .normal) + dismissButton.addTarget(self, action: #selector(login), for: .touchUpInside) + } + + /// Layout all the vertical elements of this view from code. + private func setupBody() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + scrollView.addSubview(containerView) + + containerView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: scrollView.topAnchor), + containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: UIConstants.Padding.standard), + containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -UIConstants.Padding.standard), + containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + containerView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -(2 * UIConstants.Padding.standard)) + ]) + + titleImageView.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + bulletPointsView.translatesAutoresizingMaskIntoConstraints = false + freeTrialButton.translatesAutoresizingMaskIntoConstraints = false + dismissButton.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(titleLabel) + containerView.addSubview(descriptionLabel) + containerView.addSubview(bulletPointsView) + containerView.addSubview(titleImageView) + containerView.addSubview(freeTrialButton) + containerView.addSubview(dismissButton) + + let verticalConstraints = [ + titleImageView.topAnchor.constraint(equalTo: containerView.topAnchor), + titleLabel.topAnchor.constraint(equalTo: titleImageView.bottomAnchor, constant: UIConstants.Padding.standard), + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: UIConstants.Padding.standard), + bulletPointsView.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: UIConstants.Padding.standard), + freeTrialButton.topAnchor.constraint(equalTo: bulletPointsView.bottomAnchor, constant: UIConstants.Padding.standard), + freeTrialButton.heightAnchor.constraint(equalToConstant: UIConstants.Button.largeHeight), + dismissButton.topAnchor.constraint(equalTo: freeTrialButton.bottomAnchor, constant: UIConstants.Padding.medium), + dismissButton.bottomAnchor.constraint( + equalTo: containerView.safeAreaLayoutGuide.bottomAnchor, + constant: -UIConstants.Padding.small + ), + dismissButton.heightAnchor.constraint(equalToConstant: UIConstants.Button.largeHeight) + ] + + let dismissButtonConstraintHigh = dismissButton.widthAnchor.constraint( + equalTo: containerView.widthAnchor, + multiplier: 1 + ) + dismissButtonConstraintHigh.priority = .defaultHigh + + let dismissButtonConstraintRequired = dismissButton.widthAnchor.constraint(lessThanOrEqualToConstant: 370) + + let freeTrialButtonConstraintHigh = freeTrialButton.widthAnchor.constraint( + equalTo: containerView.widthAnchor, + multiplier: 1 + ) + freeTrialButtonConstraintHigh.priority = .defaultHigh + + let freeTrialButtonConstraintRequired = freeTrialButton.widthAnchor.constraint(lessThanOrEqualToConstant: 370) + + let horizontalConstraints = [ + titleLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + titleLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 1, constant: -20), + descriptionLabel.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + descriptionLabel.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 1, constant: -20), + titleImageView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + titleImageView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 1), + bulletPointsView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + bulletPointsView.widthAnchor.constraint(equalTo: containerView.widthAnchor, multiplier: 1), + freeTrialButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + freeTrialButtonConstraintHigh, + freeTrialButtonConstraintRequired, + dismissButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), + dismissButtonConstraintHigh, + dismissButtonConstraintRequired + ] + + NSLayoutConstraint.activate(verticalConstraints) + NSLayoutConstraint.activate(horizontalConstraints) + } + + private func layoutStackView() { + let mainStackView = UIStackView() + mainStackView.axis = .vertical + mainStackView.spacing = UIConstants.Padding.medium + mainStackView.alignment = .leading + mainStackView.translatesAutoresizingMaskIntoConstraints = false + + mainStackView.addArrangedSubview(createRow( + text: KDriveStrings.Localizable.obtainkDriveAdListing1 + )) + mainStackView.addArrangedSubview(createRow( + text: KDriveStrings.Localizable.obtainkDriveAdListing2 + )) + mainStackView.addArrangedSubview(createRow( + text: KDriveStrings.Localizable.obtainkDriveAdListing3 + )) + + bulletPointsView.addSubview(mainStackView) + + NSLayoutConstraint.activate([ + mainStackView.heightAnchor.constraint(equalTo: bulletPointsView.heightAnchor), + mainStackView.widthAnchor.constraint(equalTo: bulletPointsView.widthAnchor) + ]) + } + + private func createRow(text: String) -> UIStackView { + let imageView = UIImageView(image: KDriveResourcesAsset.select.image) + imageView.contentMode = .scaleAspectFit + + NSLayoutConstraint.activate([ + imageView.heightAnchor.constraint(equalToConstant: 20), + imageView.widthAnchor.constraint(equalToConstant: 20) + ]) + + let label = IKLabel() + label.style = .subtitle1 + label.text = text + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.textAlignment = .left + + let rowStackView = UIStackView(arrangedSubviews: [imageView, label]) + rowStackView.axis = .horizontal + rowStackView.spacing = UIConstants.Padding.medium + rowStackView.alignment = .top + + return rowStackView + } + + @objc public func freeTrial() { + // TODO: Hook free trial + dismiss(animated: true, completion: nil) + } + + @objc public func login() { + // TODO: Hook login + dismiss(animated: true, completion: nil) + } +} diff --git a/kDriveCore/UI/Alert/AlertViewController.swift b/kDriveCore/UI/Alert/AlertViewController.swift index 4c9db10eb..098d9166f 100644 --- a/kDriveCore/UI/Alert/AlertViewController.swift +++ b/kDriveCore/UI/Alert/AlertViewController.swift @@ -74,7 +74,7 @@ open class AlertViewController: UIViewController { // Alert view alertView = UIView() - alertView.cornerRadius = UIConstants.alertCornerRadius + alertView.cornerRadius = UIConstants.Alert.cornerRadius alertView.backgroundColor = KDriveResourcesAsset.backgroundCardViewColor.color alertView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(alertView) diff --git a/kDriveCore/UI/IKLargeButton.swift b/kDriveCore/UI/IKLargeButton.swift index 8db664a1a..08d0acc2c 100644 --- a/kDriveCore/UI/IKLargeButton.swift +++ b/kDriveCore/UI/IKLargeButton.swift @@ -112,7 +112,7 @@ import UIKit } func setUpButton() { - layer.cornerRadius = UIConstants.buttonCornerRadius + layer.cornerRadius = UIConstants.Button.cornerRadius // Set text font & color titleLabel?.font = style.titleFont diff --git a/kDriveCore/UI/UIConstants.swift b/kDriveCore/UI/UIConstants.swift index 61c29d25e..d53e37ac6 100644 --- a/kDriveCore/UI/UIConstants.swift +++ b/kDriveCore/UI/UIConstants.swift @@ -25,6 +25,39 @@ import SnackBar import UIKit public enum UIConstants { + public enum Padding { + public static let small: CGFloat = 8.0 + public static let medium: CGFloat = 16.0 + public static let standard: CGFloat = 24.0 + } + + public enum Button { + public static let largeHeight: CGFloat = 60.0 + public static let cornerRadius = 10.0 + } + + public enum List { + public static let paddingBottom = 50.0 + public static let floatingButtonPaddingBottom = 75.0 + } + + public enum FloatingPanel { + public static let cornerRadius = 20.0 + public static let headerHeight = 70.0 + } + + public enum Image { + public static let cornerRadius = 3.0 + } + + public enum Alert { + public static let cornerRadius = 8.0 + } + + public enum FileList { + public static let cellHeight = 60.0 + } + private static let style: SnackBarStyle = { var style = SnackBarStyle.infomaniakStyle style.anchor = 20.0 @@ -32,26 +65,18 @@ public enum UIConstants { return style }() - public static let inputCornerRadius = 2.0 - public static let imageCornerRadius = 3.0 public static let cornerRadius = 6.0 - public static let alertCornerRadius = 8.0 - public static let buttonCornerRadius = 10.0 - public static let floatingPanelCornerRadius = 20.0 - public static let listPaddingBottom = 50.0 - public static let listFloatingButtonPaddingBottom = 75.0 - public static let homeListPaddingTop = 16.0 - public static let floatingPanelHeaderHeight = 70.0 - public static let fileListCellHeight = 60.0 public static let largeTitleHeight = 96.0 public static let insufficientStorageMinimumPercentage = 90.0 public static let dropDelay = -1.0 +} +public extension UIConstants { @discardableResult @MainActor - public static func showSnackBar(message: String, - duration: SnackBar.Duration = .lengthLong, - action: IKSnackBar.Action? = nil) -> IKSnackBar? { + static func showSnackBar(message: String, + duration: SnackBar.Duration = .lengthLong, + action: IKSnackBar.Action? = nil) -> IKSnackBar? { let snackbar = IKSnackBar.make(message: message, duration: duration, style: style) @@ -65,7 +90,7 @@ public enum UIConstants { @discardableResult @MainActor - public static func showCancelableSnackBar( + static func showCancelableSnackBar( message: String, cancelSuccessMessage: String, duration: SnackBar.Duration = .lengthLong, @@ -94,7 +119,7 @@ public enum UIConstants { } @MainActor - public static func showSnackBarIfNeeded(error: Error) { + static func showSnackBarIfNeeded(error: Error) { if (ReachabilityListener.instance.currentStatus == .offline || ReachabilityListener.instance.currentStatus == .undefined) && (error.asAFError?.isRequestAdaptationError == true || error.asAFError?.isSessionTaskError == true) { // No network and refresh token failed @@ -107,13 +132,13 @@ public enum UIConstants { } } - public static func openUrl(_ string: String, from viewController: UIViewController) { + static func openUrl(_ string: String, from viewController: UIViewController) { if let url = URL(string: string) { openUrl(url, from: viewController) } } - public static func openUrl(_ url: URL, from viewController: UIViewController) { + static func openUrl(_ url: URL, from viewController: UIViewController) { #if ISEXTENSION viewController.extensionContext?.open(url) #else @@ -121,7 +146,7 @@ public enum UIConstants { #endif } - public static func presentLinkPreviewForFile( + static func presentLinkPreviewForFile( _ file: File, link: String, from viewController: UIViewController, From 6c88f1562fd2f9c79e7f91ee59359968dfcd97c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 29 Nov 2024 17:15:14 +0100 Subject: [PATCH 052/129] feat: Download public share --- .../Files/File List/FileListViewModel.swift | 1 - ...SelectionFloatingPanelViewController.swift | 1 - ...atingPanelSelectOptionViewController.swift | 1 - .../DownloadArchiveOperation.swift | 26 ++++++++++++++----- .../Data/DownloadQueue/DownloadQueue.swift | 3 +++ kDriveCore/Data/Models/File.swift | 4 +++ 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index d329a4f2f..9c15a79fe 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -93,7 +93,6 @@ class FileListViewModel: SelectDelegate { var matomoViewPath = ["FileList"] } - /// Tracking a way to dismiss the current stack weak var viewControllerDismissable: ViewControllerDismissable? var realmObservationToken: NotificationToken? diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 7f5ebec9b..345be1e1e 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -141,7 +141,6 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } - // TODO:  make it work func downloadPublicShareArchivedFiles(downloadCellPath: IndexPath, publicShareProxy: PublicShareProxy, completion: @escaping (Result) -> Void) { diff --git a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift index 1097e3a76..9a34c195a 100644 --- a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift +++ b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift @@ -37,7 +37,6 @@ protocol SelectDelegate: AnyObject { func didSelect(option: Selectable) } -/// Something that can dismiss the current VC if presented @MainActor public protocol ViewControllerDismissable: AnyObject { func dismiss(animated flag: Bool, completion: (() -> Void)?) diff --git a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift index b2b2769e9..7df1e755d 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import Alamofire import CocoaLumberjackSwift import FileProvider import Foundation @@ -29,6 +30,7 @@ public class DownloadArchiveOperation: Operation { @LazyInjectService var appContextService: AppContextServiceable private let archiveId: String + private let shareDrive: AbstractDrive private let driveFileManager: DriveFileManager private let urlSession: FileDownloadSession private let publicShareProxy: PublicShareProxy? @@ -70,10 +72,12 @@ public class DownloadArchiveOperation: Operation { } public init(archiveId: String, + shareDrive: AbstractDrive, driveFileManager: DriveFileManager, urlSession: FileDownloadSession, publicShareProxy: PublicShareProxy? = nil) { self.archiveId = archiveId + self.shareDrive = shareDrive self.driveFileManager = driveFileManager self.urlSession = urlSession self.publicShareProxy = publicShareProxy @@ -120,6 +124,22 @@ public class DownloadArchiveOperation: Operation { "[DownloadOperation] Downloading Archive of public share files \(archiveId) with session \(urlSession.identifier)" ) + let url = Endpoint.getArchive(drive: shareDrive, uuid: archiveId).url + let request = URLRequest(url: url) + + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: self.archiveId) + } + task?.resume() + } + + func authenticatedDownload() { + DDLogInfo("[DownloadOperation] Downloading Archive of files \(archiveId) with session \(urlSession.identifier)") + let url = Endpoint.getArchive(drive: driveFileManager.drive, uuid: archiveId).url if let userToken = accountManager.getTokenForUserId(driveFileManager.drive.userId) { @@ -146,12 +166,6 @@ public class DownloadArchiveOperation: Operation { } } - func authenticatedDownload() { - DDLogInfo("[DownloadOperation] Downloading Archive of files \(archiveId) with session \(urlSession.identifier)") - - // TODO: missing imp - } - func downloadCompletion(url: URL?, response: URLResponse?, error: Error?) { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 11e711834..f6e41dee5 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -197,6 +197,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { let operation = DownloadArchiveOperation( archiveId: archiveId, + shareDrive: publicShareProxy.proxyDrive, driveFileManager: driveFileManager, urlSession: self.bestSession, publicShareProxy: publicShareProxy @@ -209,6 +210,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) } } + self.operationQueue.addOperation(operation) self.archiveOperationsInQueue[archiveId] = operation } @@ -226,6 +228,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { let operation = DownloadArchiveOperation( archiveId: archiveId, + shareDrive: drive, driveFileManager: driveFileManager, urlSession: self.bestSession ) diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index db5690028..c973d6848 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -193,6 +193,10 @@ public struct PublicShareProxy { self.fileId = fileId self.shareLinkUid = shareLinkUid } + + public var proxyDrive: ProxyDrive { + ProxyDrive(id: driveId) + } } public enum SortType: String { From 744fc93b5a9b69f7a70502f873936a883bc9a723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Dec 2024 11:34:25 +0100 Subject: [PATCH 053/129] feat: Dedicated public share archive download endpoint --- kDriveCore/Data/Api/Endpoint+Share.swift | 5 +++++ .../DownloadQueue/DownloadArchiveOperation.swift | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index ef06c45ba..2581f9d57 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -89,6 +89,11 @@ public extension Endpoint { return shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/archive") } + /// Downloads a public share archive + static func downloadPublicShareArchive(drive: AbstractDrive, linkUuid: String, archiveUuid: String) -> Endpoint { + return publicShareArchive(driveId: drive.id, linkUuid: linkUuid).appending(path: "/\(archiveUuid)/download") + } + func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } diff --git a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift index 7df1e755d..1877cad09 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift @@ -112,19 +112,24 @@ public class DownloadArchiveOperation: Operation { } override public func main() { - if publicShareProxy == nil { + guard let publicShareProxy else { authenticatedDownload() - } else { - publicShareDownload() + return } + + publicShareDownload(proxy: publicShareProxy) } - func publicShareDownload() { + func publicShareDownload(proxy: PublicShareProxy) { DDLogInfo( "[DownloadOperation] Downloading Archive of public share files \(archiveId) with session \(urlSession.identifier)" ) - let url = Endpoint.getArchive(drive: shareDrive, uuid: archiveId).url + let url = Endpoint.downloadPublicShareArchive( + drive: shareDrive, + linkUuid: proxy.shareLinkUid, + archiveUuid: archiveId + ).url let request = URLRequest(url: url) task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) From c53c4c128cc91b0d4d99f7f117338ebd4ae94a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Dec 2024 11:53:57 +0100 Subject: [PATCH 054/129] chore: Self assessment --- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 9c6ce10a3..7f0939534 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -84,7 +84,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { // We try to close the "Public Share screen" if type == .cancel, !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true), - let viewControllerDismissable = viewControllerDismissable { + let viewControllerDismissable { viewControllerDismissable.dismiss(animated: true, completion: nil) return } @@ -97,7 +97,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { return } - guard let publicShareProxy = publicShareProxy else { + guard let publicShareProxy else { return } From c5644a6b908f9914cbaa3bedb198b3ca599248c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Dec 2024 12:16:43 +0100 Subject: [PATCH 055/129] refactor: Removed action from ViewController and moved to the view in order to not risk breaking the inheritance stack. --- .../File List/MultipleSelectionFileListViewModel.swift | 8 +------- .../Files/FileActionsFloatingPanelViewController.swift | 4 ++++ .../MultipleSelectionFloatingPanelViewController.swift | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index c54d0bef5..26396e5de 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -122,15 +122,9 @@ class MultipleSelectionFileListViewModel { init(configuration: FileListViewModel.Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { isMultipleSelectionEnabled = false selectedCount = 0 + multipleSelectionActions = [.move, .delete, .more] self.driveFileManager = driveFileManager - - if driveFileManager.isPublicShare { - multipleSelectionActions = [.download] - } else { - multipleSelectionActions = [.move, .delete, .more] - } - self.currentDirectory = currentDirectory self.configuration = configuration } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 33a74e12c..8b3eefa56 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -228,6 +228,10 @@ public class FloatingPanelAction: Equatable { return [manageCategories, favorite, upsaleColor, folderColor, offline, download, move, duplicate].map { $0.reset() } } + static var multipleSelectionPublicShareActions: [FloatingPanelAction] { + return [download].map { $0.reset() } + } + static var multipleSelectionSharedWithMeActions: [FloatingPanelAction] { return [download].map { $0.reset() } } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift index 345be1e1e..92d346a7d 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -48,7 +48,7 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro return currentDirectory.visibility == .isInSharedSpace || currentDirectory.visibility == .isSharedSpace } - var actions: [FloatingPanelAction] = [] + var actions = FloatingPanelAction.listActions init( driveFileManager: DriveFileManager, @@ -83,9 +83,9 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } func setupContent() { - guard actions.isEmpty else { return } - - if sharedWithMe { + if driveFileManager.isPublicShare { + actions = FloatingPanelAction.multipleSelectionPublicShareActions + } else if sharedWithMe { actions = FloatingPanelAction.multipleSelectionSharedWithMeActions } else if allItemsSelected { actions = FloatingPanelAction.selectAllActions From 8918aa6d5ae183bce855ed15ad1a3bd52749249a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 3 Dec 2024 20:15:57 +0100 Subject: [PATCH 056/129] feat: Public share file count for select all --- .../File List/MultipleSelectionFileListViewModel.swift | 10 +++++++++- kDriveCore/Data/Api/Endpoint+Share.swift | 5 +++++ kDriveCore/Data/Api/PublicShareApiFetcher.swift | 7 +++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index 26396e5de..a2b8565a1 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -219,7 +219,15 @@ class MultipleSelectionFileListViewModel { onSelectAll?() Task { [proxyCurrentDirectory = currentDirectory.proxify()] in do { - let directoryCount = try await driveFileManager.apiFetcher.count(of: proxyCurrentDirectory) + let directoryCount: FileCount + if let publicShareProxy = driveFileManager.publicShareProxy { + directoryCount = try await PublicShareApiFetcher() + .countPublicShare(drive: publicShareProxy.proxyDrive, + linkUuid: publicShareProxy.shareLinkUid) + } else { + directoryCount = try await driveFileManager.apiFetcher.count(of: proxyCurrentDirectory) + } + selectedCount = directoryCount.count rightBarButtons = [.deselectAll] } catch { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 2581f9d57..8fc76c170 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -94,6 +94,11 @@ public extension Endpoint { return publicShareArchive(driveId: drive.id, linkUuid: linkUuid).appending(path: "/\(archiveUuid)/download") } + /// Count files of a public share folder + static func countPublicShare(drive: AbstractDrive, linkUuid: String) -> Endpoint { + return publicShareArchive(driveId: drive.id, linkUuid: linkUuid).appending(path: "/count") + } + func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 7fb3fd4d9..e59c86686 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -102,4 +102,11 @@ public extension PublicShareApiFetcher { let archiveResponse: ValidServerResponse = try await perform(request: request) return archiveResponse.validApiResponse.data } + + func countPublicShare(drive: AbstractDrive, linkUuid: String) async throws -> FileCount { + let countUrl = Endpoint.countPublicShare(drive: drive, linkUuid: linkUuid).url + let request = Session.default.request(countUrl) + let countResponse: ValidServerResponse = try await perform(request: request) + return countResponse.validApiResponse.data + } } From b7cfac708e2cbdc640ca5d0ba4792aa80f52ea80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 4 Dec 2024 09:27:32 +0100 Subject: [PATCH 057/129] fix: Working select all --- .../File List/MultipleSelectionFileListViewModel.swift | 10 ++++++++-- kDriveCore/Data/Api/Endpoint+Share.swift | 4 ++-- kDriveCore/Data/Api/PublicShareApiFetcher.swift | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index a2b8565a1..c09a7cf79 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -122,7 +122,12 @@ class MultipleSelectionFileListViewModel { init(configuration: FileListViewModel.Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { isMultipleSelectionEnabled = false selectedCount = 0 - multipleSelectionActions = [.move, .delete, .more] + + if driveFileManager.isPublicShare { + multipleSelectionActions = [.more] + } else { + multipleSelectionActions = [.move, .delete, .more] + } self.driveFileManager = driveFileManager self.currentDirectory = currentDirectory @@ -223,7 +228,8 @@ class MultipleSelectionFileListViewModel { if let publicShareProxy = driveFileManager.publicShareProxy { directoryCount = try await PublicShareApiFetcher() .countPublicShare(drive: publicShareProxy.proxyDrive, - linkUuid: publicShareProxy.shareLinkUid) + linkUuid: publicShareProxy.shareLinkUid, + fileId: publicShareProxy.fileId) } else { directoryCount = try await driveFileManager.apiFetcher.count(of: proxyCurrentDirectory) } diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 8fc76c170..a07eb050d 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -95,8 +95,8 @@ public extension Endpoint { } /// Count files of a public share folder - static func countPublicShare(drive: AbstractDrive, linkUuid: String) -> Endpoint { - return publicShareArchive(driveId: drive.id, linkUuid: linkUuid).appending(path: "/count") + static func countPublicShare(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/count") } func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index e59c86686..b97c52e47 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -103,8 +103,8 @@ public extension PublicShareApiFetcher { return archiveResponse.validApiResponse.data } - func countPublicShare(drive: AbstractDrive, linkUuid: String) async throws -> FileCount { - let countUrl = Endpoint.countPublicShare(drive: drive, linkUuid: linkUuid).url + func countPublicShare(drive: AbstractDrive, linkUuid: String, fileId: Int) async throws -> FileCount { + let countUrl = Endpoint.countPublicShare(driveId: drive.id, linkUuid: linkUuid, fileId: fileId).url let request = Session.default.request(countUrl) let countResponse: ValidServerResponse = try await perform(request: request) return countResponse.validApiResponse.data From f8932cad83f7498db43d31e17a23d068cdff651b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 5 Dec 2024 19:04:48 +0100 Subject: [PATCH 058/129] fix(DriveFileManager): Fix pagination for public share. --- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 1 + kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 115927d16..2b4db48de 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -77,6 +77,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy, publicShareProxy: publicShareProxy, + cursor: cursor, publicShareApiFetcher: publicShareApiFetcher) endRefreshing() if let nextCursor { diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 62a02f736..e8aee3859 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift @@ -447,7 +447,8 @@ public final class DriveFileManager { let mySharedFiles = try await publicShareApiFetcher.shareLinkFileChildren( rootFolderId: rootProxy.id, publicShareProxy: publicShareProxy, - sortType: sortType + sortType: sortType, + cursor: cursor ) return mySharedFiles }, From 5bd372948a49b04261203b7b9f25223e3386a7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 6 Dec 2024 13:54:40 +0100 Subject: [PATCH 059/129] refactor(SaveFileViewController): Split this class in some digestible chunks. --- ...leViewController+SelectDriveDelegate.swift | 36 +++ ...eViewController+SelectFolderDelegate.swift | 34 ++ ...Controller+SelectPhotoFormatDelegate.swift | 35 ++ ...ViewController+UITableViewDataSource.swift | 125 ++++++++ ...leViewController+UITableViewDelegate.swift | 69 ++++ .../Save File/SaveFileViewController.swift | 300 ++++-------------- 6 files changed, 355 insertions(+), 244 deletions(-) create mode 100644 kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectDriveDelegate.swift create mode 100644 kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift create mode 100644 kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectPhotoFormatDelegate.swift create mode 100644 kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift create mode 100644 kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDelegate.swift diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectDriveDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectDriveDelegate.swift new file mode 100644 index 000000000..889ded040 --- /dev/null +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectDriveDelegate.swift @@ -0,0 +1,36 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveCore + +// MARK: - SelectDriveDelegate + +extension SaveFileViewController: SelectDriveDelegate { + func didSelectDrive(_ drive: Drive) { + if let selectedDriveFileManager = accountManager.getDriveFileManager(for: drive.id, userId: drive.userId) { + self.selectedDriveFileManager = selectedDriveFileManager + selectedDirectory = getBestDirectory() + sections = [.fileName, .driveSelection, .directorySelection] + if itemProvidersContainHeicPhotos { + sections.append(.photoFormatOption) + } + } + updateButton() + } +} diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift new file mode 100644 index 000000000..c2c24e03a --- /dev/null +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift @@ -0,0 +1,34 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveCore + +// MARK: - SelectFolderDelegate + +extension SaveFileViewController: SelectFolderDelegate { + func didSelectFolder(_ folder: File) { + if folder.id == DriveFileManager.constants.rootID { + selectedDirectory = selectedDriveFileManager?.getCachedRootFile() + } else { + selectedDirectory = folder + } + updateButton() + tableView.reloadData() + } +} diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectPhotoFormatDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectPhotoFormatDelegate.swift new file mode 100644 index 000000000..7d1fba2c9 --- /dev/null +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectPhotoFormatDelegate.swift @@ -0,0 +1,35 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveCore + +// MARK: - SelectPhotoFormatDelegate + +extension SaveFileViewController: SelectPhotoFormatDelegate { + func didSelectPhotoFormat(_ format: PhotoFileFormat) { + if userPreferredPhotoFormat != format { + userPreferredPhotoFormat = format + if itemProviders?.isEmpty == false { + setItemProviders() + } else { + setAssetIdentifiers() + } + } + } +} diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift new file mode 100644 index 000000000..2d5ea2158 --- /dev/null +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift @@ -0,0 +1,125 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveResources +import UIKit + +// MARK: - UITableViewDataSource + +extension SaveFileViewController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let section = sections[section] + if section == .fileName { + return items.count + } else { + return 1 + } + } + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch sections[indexPath.section] { + case .alert: + let cell = tableView.dequeueReusableCell(type: AlertTableViewCell.self, for: indexPath) + cell.configure(with: .warning, message: KDriveResourcesStrings.Localizable.snackBarUploadError(errorCount)) + return cell + case .fileName: + let item = items[indexPath.row] + if items.count > 1 { + let cell = tableView.dequeueReusableCell(type: UploadTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow( + isFirst: indexPath.row == 0, + isLast: indexPath.row == self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 + ) + cell.configureWith(importedFile: item) + return cell + } else { + let cell = tableView.dequeueReusableCell(type: FileNameTableViewCell.self, for: indexPath) + cell.textField.text = item.name + cell.textDidChange = { [weak self] text in + guard let self else { return } + item.name = text ?? KDriveResourcesStrings.Localizable.allUntitledFileName + if let text, !text.isEmpty { + updateButton() + } else { + enableButton = false + } + } + return cell + } + case .driveSelection: + let cell = tableView.dequeueReusableCell(type: LocationTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + cell.configure(with: selectedDriveFileManager?.drive) + return cell + case .directorySelection: + let cell = tableView.dequeueReusableCell(type: LocationTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + cell.configure(with: selectedDirectory, drive: selectedDriveFileManager!.drive) + return cell + case .photoFormatOption: + let cell = tableView.dequeueReusableCell(type: PhotoFormatTableViewCell.self, for: indexPath) + cell.initWithPositionAndShadow(isFirst: true, isLast: true) + cell.configure(with: userPreferredPhotoFormat) + return cell + case .importing: + let cell = tableView.dequeueReusableCell(type: ImportingTableViewCell.self, for: indexPath) + cell.importationProgressView.observedProgress = importProgress + return cell + default: + fatalError("Not supported by this datasource") + } + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + if section == tableView.numberOfSections - 1 && !importInProgress { + let view = FooterButtonView.instantiate(title: KDriveResourcesStrings.Localizable.buttonSave) + view.delegate = self + view.footerButton.isEnabled = enableButton + return view + } + return nil + } +} + +extension SaveFileViewController { + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + switch sections[section] { + case .fileName: + return HomeTitleView.instantiate(title: "") + case .driveSelection: + return HomeTitleView.instantiate(title: "kDrive") + case .directorySelection: + return HomeTitleView.instantiate(title: KDriveResourcesStrings.Localizable.allPathTitle) + case .photoFormatOption: + return HomeTitleView.instantiate(title: KDriveResourcesStrings.Localizable.photoFormatTitle) + default: + return nil + } + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if section == tableView.numberOfSections - 1 && !importInProgress { + return 124 + } + return 32 + } +} diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDelegate.swift new file mode 100644 index 000000000..a8a1fdf4a --- /dev/null +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDelegate.swift @@ -0,0 +1,69 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveCore +import kDriveResources +import UIKit + +// MARK: - UITableViewDelegate + +extension SaveFileViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch sections[indexPath.section] { + case .fileName: + let item = items[indexPath.row] + if items.count > 1 { + let alert = AlertFieldViewController( + title: KDriveResourcesStrings.Localizable.buttonRename, + placeholder: KDriveResourcesStrings.Localizable.hintInputFileName, + text: item.name, + action: KDriveResourcesStrings.Localizable.buttonSave, + loading: false + ) { newName in + item.name = newName + tableView.reloadRows(at: [indexPath], with: .automatic) + } + alert.textFieldConfiguration = .fileNameConfiguration + alert.textFieldConfiguration.selectedRange = item.name + .startIndex ..< (item.name.lastIndex { $0 == "." } ?? item.name.endIndex) + present(alert, animated: true) + } + case .driveSelection: + let selectDriveViewController = SelectDriveViewController.instantiate() + selectDriveViewController.selectedDrive = selectedDriveFileManager?.drive + selectDriveViewController.delegate = self + navigationController?.pushViewController(selectDriveViewController, animated: true) + case .directorySelection: + guard let driveFileManager = selectedDriveFileManager else { return } + let selectFolderNavigationController = SelectFolderViewController.instantiateInNavigationController( + driveFileManager: driveFileManager, + startDirectory: selectedDirectory, + delegate: self + ) + present(selectFolderNavigationController, animated: true) + case .photoFormatOption: + let selectPhotoFormatViewController = SelectPhotoFormatViewController + .instantiate(selectedFormat: userPreferredPhotoFormat) + selectPhotoFormatViewController.delegate = self + navigationController?.pushViewController(selectPhotoFormatViewController, animated: true) + default: + break + } + } +} diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index acdc1a034..26e88d3fc 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -92,9 +92,9 @@ class SaveFileViewController: UIViewController { return false } - private var errorCount = 0 - private var importProgress: Progress? - private var enableButton = false { + var errorCount = 0 + var importProgress: Progress? + var enableButton = false { didSet { guard let footer = tableView.footerView(forSection: tableView.numberOfSections - 1) as? FooterButtonView else { return @@ -103,7 +103,7 @@ class SaveFileViewController: UIViewController { } } - private var importInProgress: Bool { + var importInProgress: Bool { if let progress = importProgress { return progress.fractionCompleted < 1 } else { @@ -114,6 +114,8 @@ class SaveFileViewController: UIViewController { @IBOutlet var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! + // MARK: View lifecycle + override func viewDidLoad() { super.viewDidLoad() @@ -159,6 +161,51 @@ class SaveFileViewController: UIViewController { ) } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setInfomaniakAppearanceNavigationBar() + tableView.reloadData() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + MatomoUtils.track(view: [MatomoUtils.Views.save.displayName, "SaveFile"]) + } + + deinit { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + + // MARK: Objc + + @objc func keyboardWillShow(_ notification: Notification) { + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + tableView.contentInset.bottom = keyboardSize.height + + UIView.animate(withDuration: 0.1) { + self.view.layoutIfNeeded() + } + } + } + + @objc func keyboardWillHide(_ notification: Notification) { + tableView.contentInset.bottom = 0 + UIView.animate(withDuration: 0.1) { + self.view.layoutIfNeeded() + } + } + + @IBAction func close(_ sender: Any) { + importProgress?.cancel() + dismiss(animated: true) + if let extensionContext { + extensionContext.completeRequest(returningItems: nil, completionHandler: nil) + } + } + + // MARK: Helpers + func getBestDirectory() -> File? { if lastSelectedDirectory?.driveId == selectedDriveFileManager?.drive.id { return lastSelectedDirectory @@ -187,28 +234,6 @@ class SaveFileViewController: UIViewController { return firstAvailableSharedDriveDirectory?.freezeIfNeeded() } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - MatomoUtils.track(view: [MatomoUtils.Views.save.displayName, "SaveFile"]) - } - - @objc func keyboardWillShow(_ notification: Notification) { - if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { - tableView.contentInset.bottom = keyboardSize.height - - UIView.animate(withDuration: 0.1) { - self.view.layoutIfNeeded() - } - } - } - - @objc func keyboardWillHide(_ notification: Notification) { - tableView.contentInset.bottom = 0 - UIView.animate(withDuration: 0.1) { - self.view.layoutIfNeeded() - } - } - func dismiss(animated: Bool, clean: Bool = true, completion: (() -> Void)? = nil) { Task { // Cleanup file that were duplicated to appGroup on extension mode @@ -222,12 +247,7 @@ class SaveFileViewController: UIViewController { } } - deinit { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - private func setAssetIdentifiers() { + func setAssetIdentifiers() { guard let assetIdentifiers else { return } sections = [.importing] importProgress = fileImportHelper.importAssets( @@ -246,7 +266,7 @@ class SaveFileViewController: UIViewController { } } - private func setItemProviders() { + func setItemProviders() { guard let itemProviders else { return } sections = [.importing] importProgress = fileImportHelper @@ -264,11 +284,11 @@ class SaveFileViewController: UIViewController { } } - private func updateButton() { + func updateButton() { enableButton = selectedDirectory != nil && items.allSatisfy { !$0.name.isEmpty } && !items.isEmpty && !importInProgress } - private func updateTableViewAfterImport() { + func updateTableViewAfterImport() { guard !importInProgress else { return } // Update table view var newSections = [SaveFileSection]() @@ -298,11 +318,7 @@ class SaveFileViewController: UIViewController { } } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setInfomaniakAppearanceNavigationBar() - tableView.reloadData() - } + // MARK: Class methods class func instantiate(driveFileManager: DriveFileManager?) -> SaveFileViewController { let viewController = Storyboard.saveFile @@ -321,208 +337,4 @@ class SaveFileViewController: UIViewController { navigationController.navigationBar.prefersLargeTitles = true return navigationController } - - @IBAction func close(_ sender: Any) { - importProgress?.cancel() - dismiss(animated: true) - if let extensionContext { - extensionContext.completeRequest(returningItems: nil, completionHandler: nil) - } - } -} - -// MARK: - UITableViewDataSource - -extension SaveFileViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let section = sections[section] - if section == .fileName { - return items.count - } else { - return 1 - } - } - - func numberOfSections(in tableView: UITableView) -> Int { - return sections.count - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch sections[indexPath.section] { - case .alert: - let cell = tableView.dequeueReusableCell(type: AlertTableViewCell.self, for: indexPath) - cell.configure(with: .warning, message: KDriveResourcesStrings.Localizable.snackBarUploadError(errorCount)) - return cell - case .fileName: - let item = items[indexPath.row] - if items.count > 1 { - let cell = tableView.dequeueReusableCell(type: UploadTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow( - isFirst: indexPath.row == 0, - isLast: indexPath.row == self.tableView(tableView, numberOfRowsInSection: indexPath.section) - 1 - ) - cell.configureWith(importedFile: item) - return cell - } else { - let cell = tableView.dequeueReusableCell(type: FileNameTableViewCell.self, for: indexPath) - cell.textField.text = item.name - cell.textDidChange = { [weak self] text in - guard let self else { return } - item.name = text ?? KDriveResourcesStrings.Localizable.allUntitledFileName - if let text, !text.isEmpty { - updateButton() - } else { - enableButton = false - } - } - return cell - } - case .driveSelection: - let cell = tableView.dequeueReusableCell(type: LocationTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow(isFirst: true, isLast: true) - cell.configure(with: selectedDriveFileManager?.drive) - return cell - case .directorySelection: - let cell = tableView.dequeueReusableCell(type: LocationTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow(isFirst: true, isLast: true) - cell.configure(with: selectedDirectory, drive: selectedDriveFileManager!.drive) - return cell - case .photoFormatOption: - let cell = tableView.dequeueReusableCell(type: PhotoFormatTableViewCell.self, for: indexPath) - cell.initWithPositionAndShadow(isFirst: true, isLast: true) - cell.configure(with: userPreferredPhotoFormat) - return cell - case .importing: - let cell = tableView.dequeueReusableCell(type: ImportingTableViewCell.self, for: indexPath) - cell.importationProgressView.observedProgress = importProgress - return cell - default: - fatalError("Not supported by this datasource") - } - } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - switch sections[section] { - case .fileName: - return HomeTitleView.instantiate(title: "") - case .driveSelection: - return HomeTitleView.instantiate(title: "kDrive") - case .directorySelection: - return HomeTitleView.instantiate(title: KDriveResourcesStrings.Localizable.allPathTitle) - case .photoFormatOption: - return HomeTitleView.instantiate(title: KDriveResourcesStrings.Localizable.photoFormatTitle) - default: - return nil - } - } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == tableView.numberOfSections - 1 && !importInProgress { - return 124 - } - return 32 - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - if section == tableView.numberOfSections - 1 && !importInProgress { - let view = FooterButtonView.instantiate(title: KDriveResourcesStrings.Localizable.buttonSave) - view.delegate = self - view.footerButton.isEnabled = enableButton - return view - } - return nil - } -} - -// MARK: - UITableViewDelegate - -extension SaveFileViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - switch sections[indexPath.section] { - case .fileName: - let item = items[indexPath.row] - if items.count > 1 { - let alert = AlertFieldViewController( - title: KDriveResourcesStrings.Localizable.buttonRename, - placeholder: KDriveResourcesStrings.Localizable.hintInputFileName, - text: item.name, - action: KDriveResourcesStrings.Localizable.buttonSave, - loading: false - ) { newName in - item.name = newName - tableView.reloadRows(at: [indexPath], with: .automatic) - } - alert.textFieldConfiguration = .fileNameConfiguration - alert.textFieldConfiguration.selectedRange = item.name - .startIndex ..< (item.name.lastIndex { $0 == "." } ?? item.name.endIndex) - present(alert, animated: true) - } - case .driveSelection: - let selectDriveViewController = SelectDriveViewController.instantiate() - selectDriveViewController.selectedDrive = selectedDriveFileManager?.drive - selectDriveViewController.delegate = self - navigationController?.pushViewController(selectDriveViewController, animated: true) - case .directorySelection: - guard let driveFileManager = selectedDriveFileManager else { return } - let selectFolderNavigationController = SelectFolderViewController.instantiateInNavigationController( - driveFileManager: driveFileManager, - startDirectory: selectedDirectory, - delegate: self - ) - present(selectFolderNavigationController, animated: true) - case .photoFormatOption: - let selectPhotoFormatViewController = SelectPhotoFormatViewController - .instantiate(selectedFormat: userPreferredPhotoFormat) - selectPhotoFormatViewController.delegate = self - navigationController?.pushViewController(selectPhotoFormatViewController, animated: true) - default: - break - } - } -} - -// MARK: - SelectFolderDelegate - -extension SaveFileViewController: SelectFolderDelegate { - func didSelectFolder(_ folder: File) { - if folder.id == DriveFileManager.constants.rootID { - selectedDirectory = selectedDriveFileManager?.getCachedRootFile() - } else { - selectedDirectory = folder - } - updateButton() - tableView.reloadData() - } -} - -// MARK: - SelectDriveDelegate - -extension SaveFileViewController: SelectDriveDelegate { - func didSelectDrive(_ drive: Drive) { - if let selectedDriveFileManager = accountManager.getDriveFileManager(for: drive.id, userId: drive.userId) { - self.selectedDriveFileManager = selectedDriveFileManager - selectedDirectory = getBestDirectory() - sections = [.fileName, .driveSelection, .directorySelection] - if itemProvidersContainHeicPhotos { - sections.append(.photoFormatOption) - } - } - updateButton() - } -} - -// MARK: - SelectPhotoFormatDelegate - -extension SaveFileViewController: SelectPhotoFormatDelegate { - func didSelectPhotoFormat(_ format: PhotoFileFormat) { - if userPreferredPhotoFormat != format { - userPreferredPhotoFormat = format - if itemProviders?.isEmpty == false { - setItemProviders() - } else { - setAssetIdentifiers() - } - } - } } From 65f7160dfc2230d9b88b870122ee23e43cb3a74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 6 Dec 2024 14:11:10 +0100 Subject: [PATCH 060/129] chore: SonarCloud --- kDrive/UI/View/Files/FileCollectionViewCell.swift | 2 +- kDriveCore/Data/Api/Endpoint+Trash.swift | 14 +++++++++----- kDriveCore/Data/Cache/AccountManager.swift | 1 - 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 5f55ada8e..7df7ac8a7 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -364,7 +364,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { func configureForSelection() { guard let viewModel, - viewModel.selectionMode == true else { + viewModel.selectionMode else { return } diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift index d90a1a6d6..c192ad306 100644 --- a/kDriveCore/Data/Api/Endpoint+Trash.swift +++ b/kDriveCore/Data/Api/Endpoint+Trash.swift @@ -23,20 +23,24 @@ import RealmSwift // MARK: - Trash public extension Endpoint { + private static let trashPath: String = "/trash" + + private static let countPath: String = "/count" + static func trash(drive: AbstractDrive) -> Endpoint { - return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()]) + return .driveInfo(drive: drive).appending(path: trashPath, queryItems: [FileWith.fileMinimal.toQueryItem()]) } static func trashV2(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") + return .driveInfoV2(drive: drive).appending(path: trashPath) } static func emptyTrash(drive: AbstractDrive) -> Endpoint { - return .driveInfoV2(drive: drive).appending(path: "/trash") + return .driveInfoV2(drive: drive).appending(path: trashPath) } static func trashCount(drive: AbstractDrive) -> Endpoint { - return .trash(drive: drive).appending(path: "/count") + return .trash(drive: drive).appending(path: countPath) } static func trashedInfo(file: AbstractFile) -> Endpoint { @@ -65,6 +69,6 @@ public extension Endpoint { } static func trashCount(of directory: AbstractFile) -> Endpoint { - return .trashedInfo(file: directory).appending(path: "/count") + return .trashedInfo(file: directory).appending(path: countPath) } } diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 9dee5130e..fdb96b513 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -214,7 +214,6 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { // FileViewModel K.O. without a valid drive in Realm, therefore add one let publicShareDrive = Drive() publicShareDrive.objectId = publicShareId - @LazyInjectService var driveInfosManager: DriveInfosManager do { try driveInfosManager.storePublicShareDrive(drive: publicShareDrive) } catch { From db10f5a5340fc6d4a92a37967ec57983b42e86de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 9 Dec 2024 14:48:38 +0100 Subject: [PATCH 061/129] feat: Base work for add to my drive feature. --- kDrive/AppDelegate.swift | 15 ++++--- .../File List/FileListViewController.swift | 16 +++++++- .../Files/File List/FileListViewModel.swift | 1 + ...eViewController+FooterButtonDelegate.swift | 41 +++++++++++++++---- .../Save File/SaveFileViewController.swift | 24 ++++++++++- .../Menu/Share/PublicShareViewModel.swift | 39 +++++++++++++----- kDrive/UI/View/Files/FileListBarButton.swift | 4 ++ kDriveCore/Data/Api/Endpoint+Share.swift | 5 ++- .../Data/Api/PublicShareApiFetcher.swift | 32 +++++++++++++++ 9 files changed, 149 insertions(+), 28 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 2247a0e86..483f10e6e 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -107,11 +107,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { Task { try! await Task.sleep(nanoseconds: 5_000_000_000) - @InjectService var router: AppNavigable - //let upsaleViewController = UpsaleViewController() - let noDriveViewController = NoDriveUpsaleViewController() - let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) - router.topMostViewController?.present(floatingPanel, animated: true, completion: nil) +// @InjectService var router: AppNavigable +// //let upsaleViewController = UpsaleViewController() +// let noDriveViewController = NoDriveUpsaleViewController() +// let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) +// router.topMostViewController?.present(floatingPanel, animated: true, completion: nil) /* Temp code to test out the feature. TODO: Remove later // a public share expired @@ -123,6 +123,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { await UniversalLinksHelper.handleURL(somePublicShare!) */ + + // a valid public share + let somePublicShare = + URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") + await UniversalLinksHelper.handleURL(somePublicShare!) } return true diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 1b818956b..b9abc2288 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -288,8 +288,20 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ]) } - @objc func addToMyDriveButtonTapped(_ sender: UIView?) { - viewModel.barButtonPressed(sender: sender, type: .downloadAll) + @objc func addToMyDriveButtonTapped(_ sender: UIButton?) { + defer { + sender?.isSelected = false + sender?.isEnabled = true + sender?.isHighlighted = false + } + + guard accountManager.currentAccount != nil else { + // Let the user login with the onboarding + dismiss(animated: true) + return + } + + viewModel.barButtonPressed(sender: sender, type: .addToMyDrive) } func reloadCollectionViewWith(files: [File]) { diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index cf6f4bb84..ba0a49b36 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -35,6 +35,7 @@ enum FileListBarButtonType { case photoSort case addFolder case downloadAll + case addToMyDrive } enum FileListQuickActionType { diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index b4684065d..d725801c0 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -36,18 +36,41 @@ extension SaveFileViewController: FooterButtonDelegate { let button = sender as? IKLargeButton button?.setLoading(true) - let items = items - guard !items.isEmpty else { - dismiss(animated: true) - return + if let publicShareProxy { + Task { + await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, + destinationDriveId: drive.accountId, + fileIds: publicShareFileIds, + exceptIds: publicShareExceptIds) + } + } else { + guard !items.isEmpty else { + dismiss(animated: true) + return + } + + Task { + await saveAndDismiss(files: items, directory: directory, drive: drive) + } } + } - Task { - await presentSnackBarSaveAndDismiss(files: items, directory: directory, drive: drive) + private func savePublicShareToDrive(sourceDriveId: Int, + destinationDriveId: Int, + fileIds: [Int], + exceptIds: [Int]) async { + defer { + dismiss(animated: true) } + + try? await PublicShareApiFetcher().importShareLinkFiles(sourceDriveId: sourceDriveId, + destinationDriveId: destinationDriveId, + fileIds: fileIds, + exceptIds: exceptIds) + print("savePublicShareToDrive") } - private func presentSnackBarSaveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { + private func saveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { let message: String do { try await processForUpload(files: files, directory: directory, drive: drive) @@ -59,6 +82,10 @@ extension SaveFileViewController: FooterButtonDelegate { message = error.localizedDescription } + presentSnackBar(message) + } + + private func presentSnackBar(_ message: String) { Task { @MainActor in self.dismiss(animated: true, clean: false) { UIConstants.showSnackBar(message: message) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 26e88d3fc..fc5690a55 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -72,6 +72,13 @@ class SaveFileViewController: UIViewController { } } + var publicShareExceptIds = [Int]() + var publicShareFileIds = [Int]() + var publicShareProxy: PublicShareProxy? + var isPublicShareFiles: Bool { + publicShareProxy != nil + } + var items = [ImportedFile]() var userPreferredPhotoFormat = UserDefaults.shared.importPhotoFormat { didSet { @@ -285,7 +292,22 @@ class SaveFileViewController: UIViewController { } func updateButton() { - enableButton = selectedDirectory != nil && items.allSatisfy { !$0.name.isEmpty } && !items.isEmpty && !importInProgress + guard selectedDirectory != nil, !importInProgress else { + enableButton = false + return + } + + guard !isPublicShareFiles else { + enableButton = true + return + } + + guard !items.isEmpty, + items.allSatisfy({ !$0.name.isEmpty }) else { + enableButton = false + return + } + enableButton = true } func updateTableViewAfterImport() { diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 115927d16..081735273 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -34,7 +34,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { guard let currentDirectory else { fatalError("PublicShareViewModel requires a currentDirectory to work") } - + let configuration = Configuration(selectAllSupported: false, rootTitle: KDriveCoreStrings.Localizable.sharedWithMeTitle, emptyViewType: .emptyFolder, @@ -84,26 +84,26 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } } - // TODO: Move away from view model override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { guard downloadObserver == nil else { return } - guard type == .downloadAll, - let publicShareProxy = publicShareProxy else { + guard let publicShareProxy = publicShareProxy else { return } + if type == .downloadAll { + downloadAll(sender: sender, publicShareProxy: publicShareProxy) + } else if type == .addToMyDrive { + addToMyDrive(sender: sender, publicShareProxy: publicShareProxy) + } + } + + private func downloadAll(sender: Any?, publicShareProxy: PublicShareProxy) { let button = sender as? UIButton button?.isEnabled = false - // TODO: Abstract sheet presentation - @InjectService var appNavigable: AppNavigable - guard let topMostViewController = appNavigable.topMostViewController else { - return - } - downloadObserver = DownloadQueue.instance .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in Task { @MainActor in @@ -139,7 +139,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { fatalError("No sender button") } - topMostViewController.present(activityViewController, animated: true) + self.onPresentViewController?(.modal, activityViewController, true) } } @@ -147,4 +147,21 @@ final class PublicShareViewModel: InMemoryFileListViewModel { driveFileManager: driveFileManager, publicShareProxy: publicShareProxy) } + + private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { + let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] + let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] + + let saveNavigationViewController = SaveFileViewController + .instantiateInNavigationController(driveFileManager: driveFileManager) + + if let saveViewController = saveNavigationViewController.viewControllers.first as? SaveFileViewController { + saveViewController.publicShareFileIds = selectedItemsIds + saveViewController.publicShareExceptIds = exceptItemIds + saveViewController.publicShareProxy = publicShareProxy + saveViewController.selectedDirectory = currentDirectory + } + + onPresentViewController?(.modal, saveNavigationViewController, true) + } } diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index 62984460f..7ecd84c35 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -53,6 +53,10 @@ final class FileListBarButton: UIBarButtonItem { let image = KDriveResourcesAsset.download.image self.init(image: image, style: .plain, target: target, action: action) accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload + case .addToMyDrive: + let image = KDriveResourcesAsset.drive.image + self.init(image: image, style: .plain, target: target, action: action) + accessibilityLabel = KDriveResourcesStrings.Localizable.buttonAddToKDrive } self.type = type } diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index ee80590b4..349c7d036 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -88,7 +88,8 @@ public extension Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } - func importShareLinkFiles(driveId: Int) -> Endpoint { - return Self.shareUrlV2.appending(path: "/\(driveId)/imports/sharelink") + /// Add to my Drive + static func importShareLinkFiles(driveId: Int) -> Endpoint { + return Endpoint(hostKeypath: \.driveHost, path: "/2/drive").appending(path: "/\(driveId)/imports/sharelink") } } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 8005dc1f2..8cc14fb95 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -29,6 +29,13 @@ public enum PublicShareLimitation: String { case expired = "link_is_not_valid" } +enum APIPublicShareParameter { + static let sourceDriveId = "source_drive_id" + static let fileIds = "file_ids" + static let exceptFileIds = "except_file_ids" + static let password = "password" +} + public class PublicShareApiFetcher: ApiFetcher { override public init() { super.init() @@ -90,4 +97,29 @@ public extension PublicShareApiFetcher { let shareLinkFiles: ValidServerResponse<[File]> = try await perform(request: request) return shareLinkFiles } + + func importShareLinkFiles(sourceDriveId: Int, + destinationDriveId: Int, + fileIds: [Int]?, + exceptIds: [Int]?, + password: String? = nil) async throws -> ValidServerResponse { + let importShareLinkFilesUrl = Endpoint.importShareLinkFiles(driveId: destinationDriveId).url + var requestParameters: [String: AnyHashable] = [ + APIPublicShareParameter.sourceDriveId: sourceDriveId + ] + + if let fileIds, !fileIds.isEmpty { + requestParameters[APIPublicShareParameter.fileIds] = fileIds + } else if let exceptIds, !exceptIds.isEmpty { + requestParameters[APIPublicShareParameter.exceptFileIds] = exceptIds + } + + if let password { + requestParameters[APIPublicShareParameter.password] = password + } + + let request = Session.default.request(importShareLinkFilesUrl) + let externalImport: ValidServerResponse = try await perform(request: request) + return externalImport + } } From f802366bb9f1b54b1355473ba487a51b3ef2bb44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 10 Dec 2024 14:29:04 +0100 Subject: [PATCH 062/129] feat: Public share offline routing. --- kDrive/AppRouter.swift | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 2c7784867..cb20f8024 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -587,8 +587,7 @@ public struct AppRouter: AppNavigable { @MainActor public func presentPublicShareLocked(_ destinationURL: URL) { guard let window, - let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("TODO: fix offline routing - presentPublicShareLocked") + let rootViewController = window.rootViewController else { return } @@ -599,20 +598,13 @@ public struct AppRouter: AppNavigable { publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical - rootViewController.selectedIndex = MainTabBarIndex.files.rawValue - - guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { - return - } - - navigationController.present(publicShareNavigationController, animated: true, completion: nil) + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) } } @MainActor public func presentPublicShareExpired() { guard let window, - let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("TODO: fix offline routing - presentPublicShareExpired") + let rootViewController = window.rootViewController else { return } @@ -622,13 +614,7 @@ public struct AppRouter: AppNavigable { publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical - rootViewController.selectedIndex = MainTabBarIndex.files.rawValue - - guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { - return - } - - navigationController.present(publicShareNavigationController, animated: true, completion: nil) + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) } } @@ -639,21 +625,15 @@ public struct AppRouter: AppNavigable { apiFetcher: PublicShareApiFetcher ) { guard let window, - let rootViewController = window.rootViewController as? MainTabViewController else { - fatalError("TODO: fix offline routing - presentPublicShare") - return - } - - // TODO: Fix access right - guard !frozenRootFolder.isDisabled else { - fatalError("isDisabled") + let rootViewController = window.rootViewController else { return } rootViewController.dismiss(animated: false) { - rootViewController.selectedIndex = MainTabBarIndex.files.rawValue - - guard let navigationController = rootViewController.selectedViewController as? UINavigationController else { + guard !frozenRootFolder.isDisabled else { + let noDriveViewController = NoDriveUpsaleViewController() + let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) + rootViewController.present(floatingPanel, animated: true, completion: nil) return } @@ -667,7 +647,7 @@ public struct AppRouter: AppNavigable { publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical - navigationController.present(publicShareNavigationController, animated: true, completion: nil) + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) } } From ca518b7c54cc7d66a6d2e6e6bff7478b5d8e00d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 10 Dec 2024 14:41:15 +0100 Subject: [PATCH 063/129] feat: Offline in-memory DriveFileManager. --- kDrive/Utils/UniversalLinksHelper.swift | 6 ++++-- kDriveCore/Data/Cache/AccountManager.swift | 18 +++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift index c0ffd9a2f..a343fbf54 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDrive/Utils/UniversalLinksHelper.swift @@ -148,11 +148,13 @@ enum UniversalLinksHelper { MatomoUtils.trackDeeplink(name: "publicShare") - let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( + guard let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( for: shareLinkUid, driveId: driveId, rootFileId: metadata.fileId - ) + ) else { + return false + } openPublicShare(driveId: driveId, linkUuid: shareLinkUid, diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index fdb96b513..43d426e2b 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -70,7 +70,7 @@ public protocol AccountManageable: AnyObject { func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager /// Create on the fly an "in memory" DriveFileManager for a specific share - func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager + func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager? func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -201,31 +201,27 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } - public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager { + public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager? { if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { return inMemoryDriveFileManager } - // TODO: Big hack, refactor to allow for non authenticated requests - guard let someToken = apiFetchers.values.first?.currentToken else { - fatalError("probably no account available") - } - // FileViewModel K.O. without a valid drive in Realm, therefore add one let publicShareDrive = Drive() publicShareDrive.objectId = publicShareId + do { try driveInfosManager.storePublicShareDrive(drive: publicShareDrive) } catch { - fatalError("unable to update public share drive in base, \(error)") + DDLogError("Failed to store public share drive in base, \(error)") + return nil } - let frozenPublicShareDrive = publicShareDrive.freeze() - let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate()) + let frozenPublicShareDrive = publicShareDrive.freeze() let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId) let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy) - return DriveFileManager(drive: frozenPublicShareDrive, apiFetcher: apiFetcher, context: context) + return DriveFileManager(drive: frozenPublicShareDrive, apiFetcher: DriveApiFetcher(), context: context) } public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { From 93bbb8ee831d04d54eff79d14a11c18fd323e461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 10 Dec 2024 16:21:16 +0100 Subject: [PATCH 064/129] feat: Authenticating add to kDrive call. --- kDrive/AppRouter.swift | 3 ++ .../File List/FileListViewController.swift | 20 +++++++++++ ...eViewController+FooterButtonDelegate.swift | 21 ++++++----- .../Upsale/NoDriveUpsaleViewController.swift | 3 ++ .../UI/View/Upsale/UpsaleViewController.swift | 7 ++-- kDriveCore/Data/Api/DriveApiFetcher.swift | 36 +++++++++++++++++++ kDriveCore/Data/Api/Endpoint+Share.swift | 6 ++-- .../Data/Api/PublicShareApiFetcher.swift | 32 ----------------- 8 files changed, 82 insertions(+), 46 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index cb20f8024..483edd31e 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -632,6 +632,9 @@ public struct AppRouter: AppNavigable { rootViewController.dismiss(animated: false) { guard !frozenRootFolder.isDisabled else { let noDriveViewController = NoDriveUpsaleViewController() + noDriveViewController.dismissCallback = { + rootViewController.dismiss(animated: false) {} + } let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) rootViewController.present(floatingPanel, animated: true, completion: nil) return diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index b9abc2288..9c01ca9f2 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -296,8 +296,28 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } guard accountManager.currentAccount != nil else { + #if !ISEXTENSION + let upsaleViewController = UpsaleViewController() + + // Create an account + upsaleViewController.freeTrialCallback = { [weak self] in + self?.dismiss(animated: true) + // TODO: Present login +// MatomoUtils.track(eventWithCategory: .account, name: "openCreationWebview") +// present(RegisterViewController.instantiateInNavigationController(delegate: self), animated: true) + } + // Let the user login with the onboarding + upsaleViewController.loginCallback = { [weak self] in + self?.dismiss(animated: true) + } + + let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: upsaleViewController) + present(floatingPanel, animated: true, completion: nil) + #else dismiss(animated: true) + #endif + return } diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index d725801c0..944b44daa 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -38,10 +38,10 @@ extension SaveFileViewController: FooterButtonDelegate { if let publicShareProxy { Task { - await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, - destinationDriveId: drive.accountId, - fileIds: publicShareFileIds, - exceptIds: publicShareExceptIds) + try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, + destinationDriveId: drive.accountId, + fileIds: publicShareFileIds, + exceptIds: publicShareExceptIds) } } else { guard !items.isEmpty else { @@ -63,11 +63,14 @@ extension SaveFileViewController: FooterButtonDelegate { dismiss(animated: true) } - try? await PublicShareApiFetcher().importShareLinkFiles(sourceDriveId: sourceDriveId, - destinationDriveId: destinationDriveId, - fileIds: fileIds, - exceptIds: exceptIds) - print("savePublicShareToDrive") + guard let currentDriveFileManager = accountManager.currentDriveFileManager else { + return + } + + try await currentDriveFileManager.apiFetcher.importShareLinkFiles(sourceDriveId: sourceDriveId, + destinationDriveId: destinationDriveId, + fileIds: fileIds, + exceptIds: exceptIds) } private func saveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { diff --git a/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift index ad7e0844c..34f4d9f2c 100644 --- a/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift @@ -22,6 +22,8 @@ import kDriveResources import UIKit public class NoDriveUpsaleViewController: UpsaleViewController { + var dismissCallback: (() -> Void)? + override func configureButtons() { dismissButton.style = .primaryButton freeTrialButton.setTitle(KDriveStrings.Localizable.obtainkDriveAdFreeTrialButton, for: .normal) @@ -39,5 +41,6 @@ public class NoDriveUpsaleViewController: UpsaleViewController { @objc public func dismissViewController() { dismiss(animated: true, completion: nil) + dismissCallback?() } } diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift index bc1c28f40..a6bf7ba8d 100644 --- a/kDrive/UI/View/Upsale/UpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -22,6 +22,9 @@ import kDriveResources import UIKit public class UpsaleViewController: UIViewController { + var loginCallback: (() -> Void)? + var freeTrialCallback: (() -> Void)? + let titleImageView = UIImageView() let titleLabel: UILabel = { @@ -217,12 +220,12 @@ public class UpsaleViewController: UIViewController { } @objc public func freeTrial() { - // TODO: Hook free trial dismiss(animated: true, completion: nil) + freeTrialCallback?() } @objc public func login() { - // TODO: Hook login dismiss(animated: true, completion: nil) + loginCallback?() } } diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index a3cd50e5e..934afc45f 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -549,6 +549,42 @@ public class DriveApiFetcher: ApiFetcher { public func file(_ file: AbstractFile) async throws -> File { try await perform(request: authenticatedRequest(.file(file))) } + + public func importShareLinkFiles(sourceDriveId: Int, + destinationDriveId: Int, + fileIds: [Int]?, + exceptIds: [Int]?, + password: String? = nil) async throws -> ValidServerResponse { + let destinationDrive = ProxyDrive(id: destinationDriveId) + let importShareLinkFiles = Endpoint.importShareLinkFiles(destinationDrive: destinationDrive) + var requestParameters: Parameters = [ + APIPublicShareParameter.sourceDriveId: sourceDriveId + ] + + if let fileIds, !fileIds.isEmpty { + requestParameters[APIPublicShareParameter.fileIds] = fileIds + } else if let exceptIds, !exceptIds.isEmpty { + requestParameters[APIPublicShareParameter.exceptFileIds] = exceptIds + } + + if let password { + requestParameters[APIPublicShareParameter.password] = password + } + + let result: ValidServerResponse = try await perform(request: authenticatedRequest( + importShareLinkFiles, + method: .post, + parameters: requestParameters + )) + return result + } +} + +enum APIPublicShareParameter { + static let sourceDriveId = "source_drive_id" + static let fileIds = "file_ids" + static let exceptFileIds = "except_file_ids" + static let password = "password" } class SyncedAuthenticator: OAuthAuthenticator { diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 349c7d036..796a00c7a 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -88,8 +88,8 @@ public extension Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } - /// Add to my Drive - static func importShareLinkFiles(driveId: Int) -> Endpoint { - return Endpoint(hostKeypath: \.driveHost, path: "/2/drive").appending(path: "/\(driveId)/imports/sharelink") + /// Add Public Share to my Drive + static func importShareLinkFiles(destinationDrive: AbstractDrive) -> Endpoint { + return Endpoint.driveInfoV2(drive: destinationDrive).appending(path: "/imports/sharelink") } } diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift index 8cc14fb95..8005dc1f2 100644 --- a/kDriveCore/Data/Api/PublicShareApiFetcher.swift +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -29,13 +29,6 @@ public enum PublicShareLimitation: String { case expired = "link_is_not_valid" } -enum APIPublicShareParameter { - static let sourceDriveId = "source_drive_id" - static let fileIds = "file_ids" - static let exceptFileIds = "except_file_ids" - static let password = "password" -} - public class PublicShareApiFetcher: ApiFetcher { override public init() { super.init() @@ -97,29 +90,4 @@ public extension PublicShareApiFetcher { let shareLinkFiles: ValidServerResponse<[File]> = try await perform(request: request) return shareLinkFiles } - - func importShareLinkFiles(sourceDriveId: Int, - destinationDriveId: Int, - fileIds: [Int]?, - exceptIds: [Int]?, - password: String? = nil) async throws -> ValidServerResponse { - let importShareLinkFilesUrl = Endpoint.importShareLinkFiles(driveId: destinationDriveId).url - var requestParameters: [String: AnyHashable] = [ - APIPublicShareParameter.sourceDriveId: sourceDriveId - ] - - if let fileIds, !fileIds.isEmpty { - requestParameters[APIPublicShareParameter.fileIds] = fileIds - } else if let exceptIds, !exceptIds.isEmpty { - requestParameters[APIPublicShareParameter.exceptFileIds] = exceptIds - } - - if let password { - requestParameters[APIPublicShareParameter.password] = password - } - - let request = Session.default.request(importShareLinkFilesUrl) - let externalImport: ValidServerResponse = try await perform(request: request) - return externalImport - } } From 31051668d57d7d3ad0eb6805ca09d156b2e44efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 11 Dec 2024 09:22:22 +0100 Subject: [PATCH 065/129] refactor: Substitute ViewControllerDismissable for a simple closure. --- kDrive/AppRouter.swift | 4 +++- .../Controller/Files/File List/FileListViewController.swift | 2 +- kDrive/UI/Controller/Files/File List/FileListViewModel.swift | 3 +-- .../Controller/FloatingPanelSelectOptionViewController.swift | 5 ----- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 5 ++--- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 38685cb79..70fa70cb5 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -675,7 +675,9 @@ public struct AppRouter: AppNavigable { apiFetcher: apiFetcher, configuration: configuration) let viewController = FileListViewController(viewModel: viewModel) - viewModel.viewControllerDismissable = viewController + viewModel.dismissClosure = { [weak viewController] in + viewController?.dismiss(animated: true) + } let publicShareNavigationController = UINavigationController(rootViewController: viewController) publicShareNavigationController.modalPresentationStyle = .fullScreen diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 046acbcc3..394091956 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -49,7 +49,7 @@ extension SortType: Selectable { } class FileListViewController: UICollectionViewController, SwipeActionCollectionViewDelegate, - SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable, ViewControllerDismissable { + SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable { @LazyInjectService var accountManager: AccountManageable // MARK: - Constants diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 9c15a79fe..93cc3086c 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -93,8 +93,7 @@ class FileListViewModel: SelectDelegate { var matomoViewPath = ["FileList"] } - weak var viewControllerDismissable: ViewControllerDismissable? - + var dismissClosure: (() -> Void)? var realmObservationToken: NotificationToken? var currentDirectoryObservationToken: NotificationToken? diff --git a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift index 9a34c195a..88bcb3c7b 100644 --- a/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift +++ b/kDrive/UI/Controller/FloatingPanelSelectOptionViewController.swift @@ -37,11 +37,6 @@ protocol SelectDelegate: AnyObject { func didSelect(option: Selectable) } -@MainActor -public protocol ViewControllerDismissable: AnyObject { - func dismiss(animated flag: Bool, completion: (() -> Void)?) -} - class FloatingPanelSelectOptionViewController: UITableViewController { var headerTitle = "" var selectedOption: T? diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 7f0939534..22408ce7c 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -83,9 +83,8 @@ final class PublicShareViewModel: InMemoryFileListViewModel { guard type == .downloadAll else { // We try to close the "Public Share screen" if type == .cancel, - !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true), - let viewControllerDismissable { - viewControllerDismissable.dismiss(animated: true, completion: nil) + !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true) { + dismissClosure?() return } From fa0097e328499a95fdb6c78ff8250d2018aded0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 11 Dec 2024 15:19:21 +0100 Subject: [PATCH 066/129] refactor(OnboardingViewController): Abstracted away the post login and register code to be able to use it from elsewhere. feat: UpsaleViewController actions are performing the auth actions like expected. --- kDrive/AppRouter.swift | 22 ++++ .../File List/FileListViewController.swift | 16 ++- ...eViewController+FooterButtonDelegate.swift | 2 +- .../UI/Controller/LoginDelegateHandler.swift | 117 ++++++++++++++++++ .../Controller/OnboardingViewController.swift | 102 ++++----------- kDriveCore/Utils/AppNavigable.swift | 7 ++ 6 files changed, 183 insertions(+), 83 deletions(-) create mode 100644 kDrive/UI/Controller/LoginDelegateHandler.swift diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 483edd31e..bf96e98bb 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -19,6 +19,7 @@ import InfomaniakCore import InfomaniakCoreUIKit import InfomaniakDI +import InfomaniakLogin import kDriveCore import kDriveResources import SafariServices @@ -33,6 +34,7 @@ public struct AppRouter: AppNavigable { @LazyInjectService private var reviewManager: ReviewManageable @LazyInjectService private var availableOfflineManager: AvailableOfflineManageable @LazyInjectService private var accountManager: AccountManageable + @LazyInjectService private var infomaniakLogin: InfomaniakLoginable @LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager @LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager @@ -444,6 +446,26 @@ public struct AppRouter: AppNavigable { viewController.present(vc, animated: true) } + @MainActor public func showRegister(delegate: InfomaniakLoginDelegate) { + guard let topMostViewController else { + return + } + + MatomoUtils.track(eventWithCategory: .account, name: "openCreationWebview") + let registerViewController = RegisterViewController.instantiateInNavigationController(delegate: delegate) + topMostViewController.present(registerViewController, animated: true) + } + + @MainActor public func showLogin(delegate: InfomaniakLoginDelegate) { + guard let topMostViewController else { + return + } + + infomaniakLogin.webviewLoginFrom(viewController: topMostViewController, + hideCreateAccountButton: true, + delegate: delegate) + } + // MARK: AppExtensionRouter public func showStore(from viewController: UIViewController, driveFileManager: DriveFileManager) { diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 9c01ca9f2..f03fe24a1 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -51,6 +51,7 @@ extension SortType: Selectable { class FileListViewController: UICollectionViewController, SwipeActionCollectionViewDelegate, SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable { @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var router: AppNavigable // MARK: - Constants @@ -301,15 +302,20 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV // Create an account upsaleViewController.freeTrialCallback = { [weak self] in - self?.dismiss(animated: true) - // TODO: Present login -// MatomoUtils.track(eventWithCategory: .account, name: "openCreationWebview") -// present(RegisterViewController.instantiateInNavigationController(delegate: self), animated: true) + guard let self else { return } + self.dismiss(animated: true) { + let loginDelegateHandler = LoginDelegateHandler() + self.router.showRegister(delegate: loginDelegateHandler) + } } // Let the user login with the onboarding upsaleViewController.loginCallback = { [weak self] in - self?.dismiss(animated: true) + guard let self else { return } + self.dismiss(animated: true) { + let loginDelegateHandler = LoginDelegateHandler() + self.router.showLogin(delegate: loginDelegateHandler) + } } let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: upsaleViewController) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index 944b44daa..8a6942358 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -58,7 +58,7 @@ extension SaveFileViewController: FooterButtonDelegate { private func savePublicShareToDrive(sourceDriveId: Int, destinationDriveId: Int, fileIds: [Int], - exceptIds: [Int]) async { + exceptIds: [Int]) async throws { defer { dismiss(animated: true) } diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift new file mode 100644 index 000000000..992c4f12b --- /dev/null +++ b/kDrive/UI/Controller/LoginDelegateHandler.swift @@ -0,0 +1,117 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 CocoaLumberjackSwift +import InfomaniakCore +import InfomaniakDI +import InfomaniakLogin +import kDriveCore +import kDriveResources +import Lottie +import UIKit + +/// Something to abstract away from the view the post login/register logic +public class LoginDelegateHandler: InfomaniakLoginDelegate { + @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var router: AppNavigable + + var didStartLoginCallback: (() -> Void)? + var didCompleteLoginCallback: (() -> Void)? + var didFailLoginWithErrorCallback: ((Error) -> Void)? + + public init(didCompleteLoginCallback: (() -> Void)? = nil) { + self.didCompleteLoginCallback = didCompleteLoginCallback + } + + @MainActor public func didCompleteLoginWith(code: String, verifier: String) { + guard let topMostViewController = router.topMostViewController else { return } + + MatomoUtils.track(eventWithCategory: .account, name: "loggedIn") + let previousAccount = accountManager.currentAccount + + didStartLoginCallback?() + + Task { + do { + _ = try await accountManager.createAndSetCurrentAccount(code: code, codeVerifier: verifier) + guard let currentDriveFileManager = accountManager.currentDriveFileManager else { + throw DriveError.NoDriveError.noDriveFileManager + } + + MatomoUtils.connectUser() + goToMainScreen(with: currentDriveFileManager) + } catch { + didCompleteLoginWithError(error, previousAccount: previousAccount, topMostViewController: topMostViewController) + } + + didCompleteLoginCallback?() + } + } + + @MainActor private func goToMainScreen(with driveFileManager: DriveFileManager) { + UserDefaults.shared.legacyIsFirstLaunch = false + UserDefaults.shared.numberOfConnections = 1 + _ = router.showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) + } + + private func didCompleteLoginWithError(_ error: Error, + previousAccount: Account?, + topMostViewController: UIViewController) { + DDLogError("Error on didCompleteLoginWith \(error)") + + if let previousAccount { + accountManager.switchAccount(newAccount: previousAccount) + } + + if let noDriveError = error as? InfomaniakCore.ApiError, noDriveError.code == DriveError.noDrive.code { + let driveErrorVC = DriveErrorViewController.instantiate(errorType: .noDrive, drive: nil) + topMostViewController.present(driveErrorVC, animated: true) + } else if let driveError = error as? DriveError, + driveError == .noDrive + || driveError == .productMaintenance + || driveError == .driveMaintenance + || driveError == .blocked { + let errorViewType: DriveErrorViewController.DriveErrorViewType + switch driveError { + case .productMaintenance, .driveMaintenance: + errorViewType = .maintenance + case .blocked: + errorViewType = .blocked + default: + errorViewType = .noDrive + } + + let driveErrorVC = DriveErrorViewController.instantiate(errorType: errorViewType, drive: nil) + topMostViewController.present(driveErrorVC, animated: true) + } else { + let metadata = [ + "Underlying Error": error.asAFError?.underlyingError.debugDescription ?? "Not an AFError" + ] + SentryDebug.capture(error: error, context: metadata, contextKey: "Error") + + topMostViewController.okAlert( + title: KDriveResourcesStrings.Localizable.errorTitle, + message: KDriveResourcesStrings.Localizable.errorConnection + ) + } + } + + public func didFailLoginWith(error: Error) { + didFailLoginWithErrorCallback?(error) + } +} diff --git a/kDrive/UI/Controller/OnboardingViewController.swift b/kDrive/UI/Controller/OnboardingViewController.swift index 0bda0794d..42c848245 100644 --- a/kDrive/UI/Controller/OnboardingViewController.swift +++ b/kDrive/UI/Controller/OnboardingViewController.swift @@ -26,6 +26,8 @@ import Lottie import UIKit class OnboardingViewController: UIViewController { + @LazyInjectService var router: AppNavigable + @IBOutlet var navigationBar: UINavigationBar! @IBOutlet var collectionView: UICollectionView! @IBOutlet var pageControl: UIPageControl! @@ -47,6 +49,27 @@ class OnboardingViewController: UIViewController { var addUser = false var slides: [Slide] = [] + private lazy var loginDelegateHandler: LoginDelegateHandler = { + let loginDelegateHandler = LoginDelegateHandler() + loginDelegateHandler.didStartLoginCallback = { [weak self] in + guard let self else { return } + signInButton.setLoading(true) + registerButton.isEnabled = false + } + loginDelegateHandler.didCompleteLoginCallback = { [weak self] in + guard let self else { return } + self.signInButton.setLoading(false) + self.registerButton.isEnabled = true + self.endBackgroundTask() + } + loginDelegateHandler.didFailLoginWithErrorCallback = { [weak self] _ in + guard let self else { return } + self.signInButton.setLoading(false) + self.registerButton.isEnabled = true + } + return loginDelegateHandler + }() + private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid override func viewDidLoad() { @@ -130,26 +153,17 @@ class OnboardingViewController: UIViewController { SentryDebug.capture(message: "Background task expired while logging in") self?.endBackgroundTask() } - infomaniakLogin.webviewLoginFrom(viewController: self, - hideCreateAccountButton: true, - delegate: self) + router.showLogin(delegate: loginDelegateHandler) } @IBAction func registerButtonPressed(_ sender: Any) { - MatomoUtils.track(eventWithCategory: .account, name: "openCreationWebview") - present(RegisterViewController.instantiateInNavigationController(delegate: self), animated: true) + router.showRegister(delegate: loginDelegateHandler) } @IBAction func closeButtonPressed(_ sender: Any) { dismiss(animated: true) } - private func goToMainScreen(with driveFileManager: DriveFileManager) { - UserDefaults.shared.legacyIsFirstLaunch = false - UserDefaults.shared.numberOfConnections = 1 - appNavigable.showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) - } - private func updateButtonsState() { if pageControl.currentPage == slides.count - 1 { if buttonContentView.isHidden { @@ -260,69 +274,3 @@ extension OnboardingViewController: UICollectionViewDelegate, UICollectionViewDa updateButtonsState() } } - -// MARK: - Infomaniak Login Delegate - -extension OnboardingViewController: InfomaniakLoginDelegate { - func didCompleteLoginWith(code: String, verifier: String) { - MatomoUtils.track(eventWithCategory: .account, name: "loggedIn") - let previousAccount = accountManager.currentAccount - signInButton.setLoading(true) - registerButton.isEnabled = false - Task { - do { - _ = try await accountManager.createAndSetCurrentAccount(code: code, codeVerifier: verifier) - guard let currentDriveFileManager = accountManager.currentDriveFileManager else { - throw DriveError.NoDriveError.noDriveFileManager - } - signInButton.setLoading(false) - registerButton.isEnabled = true - MatomoUtils.connectUser() - goToMainScreen(with: currentDriveFileManager) - } catch { - DDLogError("Error on didCompleteLoginWith \(error)") - - if let previousAccount { - accountManager.switchAccount(newAccount: previousAccount) - } - signInButton.setLoading(false) - registerButton.isEnabled = true - if let noDriveError = error as? InfomaniakCore.ApiError, noDriveError.code == DriveError.noDrive.code { - let driveErrorVC = DriveErrorViewController.instantiate(errorType: .noDrive, drive: nil) - present(driveErrorVC, animated: true) - } else if let driveError = error as? DriveError, - driveError == .noDrive - || driveError == .productMaintenance - || driveError == .driveMaintenance - || driveError == .blocked { - let errorViewType: DriveErrorViewController.DriveErrorViewType - switch driveError { - case .productMaintenance, .driveMaintenance: - errorViewType = .maintenance - case .blocked: - errorViewType = .blocked - default: - errorViewType = .noDrive - } - let driveErrorVC = DriveErrorViewController.instantiate(errorType: errorViewType, drive: nil) - present(driveErrorVC, animated: true) - } else { - let metadata = [ - "Underlying Error": error.asAFError?.underlyingError.debugDescription ?? "Not an AFError" - ] - SentryDebug.capture(error: error, context: metadata, contextKey: "Error") - okAlert( - title: KDriveResourcesStrings.Localizable.errorTitle, - message: KDriveResourcesStrings.Localizable.errorConnection - ) - } - } - endBackgroundTask() - } - } - - func didFailLoginWith(error: Error) { - signInButton.setLoading(false) - registerButton.isEnabled = true - } -} diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index c55019227..a0e029a52 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -18,6 +18,7 @@ import Foundation import InfomaniakCore +import InfomaniakLogin import UIKit /// Something that can navigate to specific places of the kDrive app @@ -45,6 +46,12 @@ public protocol RouterAppNavigable { driveFileManager: DriveFileManager, files: [ImportedFile] ) + + /// Present login webView on top of the topMostViewController + @MainActor func showLogin(delegate: InfomaniakLoginDelegate) + + /// Present register webView on top of the topMostViewController + @MainActor func showRegister(delegate: InfomaniakLoginDelegate) } /// Routing methods available from both the AppExtension mode and App From 1dea01e683f2e5c6f72e3ead2c49874cf2fa2b20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 12 Dec 2024 10:58:59 +0100 Subject: [PATCH 067/129] fix: Allow disabled public share files --- kDrive/AppRouter.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index bf96e98bb..bd6911989 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -652,16 +652,6 @@ public struct AppRouter: AppNavigable { } rootViewController.dismiss(animated: false) { - guard !frozenRootFolder.isDisabled else { - let noDriveViewController = NoDriveUpsaleViewController() - noDriveViewController.dismissCallback = { - rootViewController.dismiss(animated: false) {} - } - let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) - rootViewController.present(floatingPanel, animated: true, completion: nil) - return - } - let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, sortType: .nameAZ, driveFileManager: driveFileManager, From eb5bc3404956651d15a154eda07d6c010d8a4f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 12 Dec 2024 16:32:30 +0100 Subject: [PATCH 068/129] feat: Add to my drive working --- kDrive/AppRouter.swift | 3 ++ .../Files/File List/FileListViewModel.swift | 2 + .../UI/Controller/Files/FilePresenter.swift | 4 ++ .../Files/RootMenuViewController.swift | 4 ++ ...eViewController+FooterButtonDelegate.swift | 39 +++++++++++-------- .../Save File/SaveFileViewController.swift | 14 +++++-- .../Menu/Share/PublicShareViewModel.swift | 14 ++++++- kDriveCore/Data/Api/DriveApiFetcher.swift | 8 +++- 8 files changed, 67 insertions(+), 21 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index bd6911989..f9cce9087 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -658,6 +658,9 @@ public struct AppRouter: AppNavigable { currentDirectory: frozenRootFolder, apiFetcher: apiFetcher) let viewController = FileListViewController(viewModel: viewModel) + viewModel.onDismissViewController = { [weak viewController] in + viewController?.dismiss(animated: false) + } let publicShareNavigationController = UINavigationController(rootViewController: viewController) publicShareNavigationController.modalPresentationStyle = .fullScreen publicShareNavigationController.modalTransitionStyle = .coverVertical diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index ba0a49b36..ce66d54ff 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -139,6 +139,8 @@ class FileListViewModel: SelectDelegate { } } + var onDismissViewController: (() -> Void)? + var sortTypeObservation: AnyCancellable? var listStyleObservation: AnyCancellable? var bindStore = Set() diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index be01bcbeb..29708f53e 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -158,6 +158,10 @@ final class FilePresenter { } let nextVC = FileListViewController(viewModel: viewModel) + viewModel.onDismissViewController = { [weak nextVC] in + nextVC?.dismiss(animated: true) + } + guard file.isDisabled else { navigationController?.pushViewController(nextVC, animated: animated) return diff --git a/kDrive/UI/Controller/Files/RootMenuViewController.swift b/kDrive/UI/Controller/Files/RootMenuViewController.swift index 99e7b00a0..26f5c504f 100644 --- a/kDrive/UI/Controller/Files/RootMenuViewController.swift +++ b/kDrive/UI/Controller/Files/RootMenuViewController.swift @@ -275,6 +275,10 @@ class RootMenuViewController: CustomLargeTitleCollectionViewController, SelectSw } let destinationViewController = FileListViewController(viewModel: destinationViewModel) + destinationViewModel.onDismissViewController = { [weak destinationViewController] in + destinationViewController?.dismiss(animated: true) + } + navigationController?.pushViewController(destinationViewController, animated: true) } diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index 8a6942358..db1fb7cf0 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -27,10 +27,11 @@ import UIKit extension SaveFileViewController: FooterButtonDelegate { @objc func didClickOnButton(_ sender: AnyObject) { - guard let drive = selectedDriveFileManager?.drive, + guard let selectedDriveFileManager, let directory = selectedDirectory else { return } + let drive = selectedDriveFileManager.drive // Making sure the user cannot spam the button on tasks that may take a while let button = sender as? IKLargeButton @@ -38,14 +39,18 @@ extension SaveFileViewController: FooterButtonDelegate { if let publicShareProxy { Task { + defer { dismiss() } try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, - destinationDriveId: drive.accountId, + destinationDriveId: drive.id, + destinationFolderId: directory.id, fileIds: publicShareFileIds, - exceptIds: publicShareExceptIds) + exceptIds: publicShareExceptIds, + sharelinkUuid: publicShareProxy.shareLinkUid, + driveFileManager: selectedDriveFileManager) } } else { guard !items.isEmpty else { - dismiss(animated: true) + dismiss() return } @@ -57,20 +62,22 @@ extension SaveFileViewController: FooterButtonDelegate { private func savePublicShareToDrive(sourceDriveId: Int, destinationDriveId: Int, + destinationFolderId: Int, fileIds: [Int], - exceptIds: [Int]) async throws { - defer { - dismiss(animated: true) - } - - guard let currentDriveFileManager = accountManager.currentDriveFileManager else { - return - } + exceptIds: [Int], + sharelinkUuid: String, + driveFileManager: DriveFileManager) async throws { + try await _ = driveFileManager.apiFetcher.importShareLinkFiles(sourceDriveId: sourceDriveId, + destinationDriveId: destinationDriveId, + destinationFolderId: destinationFolderId, + fileIds: fileIds, + exceptIds: exceptIds, + sharelinkUuid: sharelinkUuid) + } - try await currentDriveFileManager.apiFetcher.importShareLinkFiles(sourceDriveId: sourceDriveId, - destinationDriveId: destinationDriveId, - fileIds: fileIds, - exceptIds: exceptIds) + private func dismiss() { + completionClosure?() + dismiss(animated: true) } private func saveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index fc5690a55..b314a7318 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -118,6 +118,8 @@ class SaveFileViewController: UIViewController { } } + @MainActor var completionClosure: (() -> Void)? + @IBOutlet var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! @@ -349,9 +351,8 @@ class SaveFileViewController: UIViewController { return viewController } - class func instantiateInNavigationController(driveFileManager: DriveFileManager?, - files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { - let saveViewController = instantiate(driveFileManager: driveFileManager) + class func setInNavigationController(saveViewController: SaveFileViewController, + files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { if let files { saveViewController.items = files } @@ -359,4 +360,11 @@ class SaveFileViewController: UIViewController { navigationController.navigationBar.prefersLargeTitles = true return navigationController } + + class func instantiateInNavigationController(driveFileManager: DriveFileManager?, + files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { + let saveViewController = instantiate(driveFileManager: driveFileManager) + return setInNavigationController(saveViewController: saveViewController, + files: files) + } } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 0e3c25f5e..8de668f52 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -24,6 +24,8 @@ import UIKit /// Public share view model, loading content from memory realm final class PublicShareViewModel: InMemoryFileListViewModel { + @LazyInjectService private var accountManager: AccountManageable + private var downloadObserver: ObservationToken? var publicShareProxy: PublicShareProxy? @@ -150,11 +152,21 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager else { + return + } + let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] + let saveViewController = SaveFileViewController.instantiate(driveFileManager: driveFileManager) let saveNavigationViewController = SaveFileViewController - .instantiateInNavigationController(driveFileManager: driveFileManager) + .setInNavigationController(saveViewController: saveViewController) + + saveViewController.completionClosure = { [weak self] in + guard let self else { return } + self.onDismissViewController?() + } if let saveViewController = saveNavigationViewController.viewControllers.first as? SaveFileViewController { saveViewController.publicShareFileIds = selectedItemsIds diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 934afc45f..84c26f79f 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -552,13 +552,17 @@ public class DriveApiFetcher: ApiFetcher { public func importShareLinkFiles(sourceDriveId: Int, destinationDriveId: Int, + destinationFolderId: Int, fileIds: [Int]?, exceptIds: [Int]?, + sharelinkUuid: String, password: String? = nil) async throws -> ValidServerResponse { let destinationDrive = ProxyDrive(id: destinationDriveId) let importShareLinkFiles = Endpoint.importShareLinkFiles(destinationDrive: destinationDrive) var requestParameters: Parameters = [ - APIPublicShareParameter.sourceDriveId: sourceDriveId + APIPublicShareParameter.sourceDriveId: sourceDriveId, + APIPublicShareParameter.destinationFolderId: destinationFolderId, + APIPublicShareParameter.sharelinkUuid: sharelinkUuid ] if let fileIds, !fileIds.isEmpty { @@ -585,6 +589,8 @@ enum APIPublicShareParameter { static let fileIds = "file_ids" static let exceptFileIds = "except_file_ids" static let password = "password" + static let destinationFolderId = "destination_folder_id" + static let sharelinkUuid = "sharelink_uuid" } class SyncedAuthenticator: OAuthAuthenticator { From 384b52d4b029795a704b425832e6145b26a4d22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 12 Dec 2024 17:44:57 +0100 Subject: [PATCH 069/129] refactor: Upsale floating panel constructor method --- .../File List/FileListViewController.swift | 24 ++--------------- .../UI/View/Upsale/UpsaleViewController.swift | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index f03fe24a1..a33c972ab 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -298,28 +298,8 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV guard accountManager.currentAccount != nil else { #if !ISEXTENSION - let upsaleViewController = UpsaleViewController() - - // Create an account - upsaleViewController.freeTrialCallback = { [weak self] in - guard let self else { return } - self.dismiss(animated: true) { - let loginDelegateHandler = LoginDelegateHandler() - self.router.showRegister(delegate: loginDelegateHandler) - } - } - - // Let the user login with the onboarding - upsaleViewController.loginCallback = { [weak self] in - guard let self else { return } - self.dismiss(animated: true) { - let loginDelegateHandler = LoginDelegateHandler() - self.router.showLogin(delegate: loginDelegateHandler) - } - } - - let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: upsaleViewController) - present(floatingPanel, animated: true, completion: nil) + let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) + present(upsaleFloatingPanelController, animated: true, completion: nil) #else dismiss(animated: true) #endif diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift index a6bf7ba8d..058276c4f 100644 --- a/kDrive/UI/View/Upsale/UpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -20,6 +20,7 @@ import InfomaniakCoreUIKit import kDriveCore import kDriveResources import UIKit +import InfomaniakDI public class UpsaleViewController: UIViewController { var loginCallback: (() -> Void)? @@ -228,4 +229,30 @@ public class UpsaleViewController: UIViewController { dismiss(animated: true, completion: nil) loginCallback?() } + + public static func instantiateInFloatingPanel(rootViewController: UIViewController) -> UIViewController { + let upsaleViewController = UpsaleViewController() + + // Create an account + upsaleViewController.freeTrialCallback = { [weak rootViewController] in + guard let rootViewController else { return } + rootViewController.dismiss(animated: true) { + let loginDelegateHandler = LoginDelegateHandler() + @InjectService var router: AppNavigable + router.showRegister(delegate: loginDelegateHandler) + } + } + + // Let the user login with the onboarding + upsaleViewController.loginCallback = { [weak rootViewController] in + guard let rootViewController else { return } + rootViewController.dismiss(animated: true) { + let loginDelegateHandler = LoginDelegateHandler() + @InjectService var router: AppNavigable + router.showLogin(delegate: loginDelegateHandler) + } + } + + return UpsaleFloatingPanelController(upsaleViewController: upsaleViewController) + } } From abd2baee9e0d4863a4c9f3bc1169320590c7f2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 12 Dec 2024 17:45:19 +0100 Subject: [PATCH 070/129] feat: Add to my kDrive floating action. --- ...sFloatingPanelViewController+Actions.swift | 30 +++++++++++++++++++ ...leActionsFloatingPanelViewController.swift | 8 ++++- .../Menu/Share/PublicShareViewModel.swift | 4 --- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index d1189c7ed..6a1780298 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -165,6 +165,8 @@ extension FileActionsFloatingPanelViewController { leaveShareAction() case .cancelImport: cancelImportAction() + case .addToMyDrive: + addToMyKDrive() default: break } @@ -520,4 +522,32 @@ extension FileActionsFloatingPanelViewController { } } } + + private func addToMyKDrive() { + guard accountManager.currentAccount != nil else { + let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) + present(upsaleFloatingPanelController, animated: true, completion: nil) + return + } + + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager, + let publicShareProxy = driveFileManager.publicShareProxy else { + return + } + + let itemId = [file.id] + let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) + let saveNavigationViewController = SaveFileViewController + .setInNavigationController(saveViewController: saveViewController) + + saveViewController.completionClosure = { [weak self] in + guard let self else { return } + self.dismiss(animated: true) + } + + saveViewController.publicShareFileIds = itemId + saveViewController.publicShareProxy = publicShareProxy + + present(saveNavigationViewController, animated: true, completion: nil) + } } diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 33a74e12c..42ab0ec05 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -204,6 +204,12 @@ public class FloatingPanelAction: Equatable { image: KDriveResourcesAsset.colorBucket.image ) + static let addToMyDrive = FloatingPanelAction( + id: 22, + name: KDriveResourcesStrings.Localizable.buttonAddToKDrive, + image: KDriveResourcesAsset.drive.image + ) + static var quickActions: [FloatingPanelAction] { return [informations, sendCopy, shareAndRights, shareLink].map { $0.reset() } } @@ -213,7 +219,7 @@ public class FloatingPanelAction: Equatable { } static var publicShareActions: [FloatingPanelAction] { - return [openWith, sendCopy, download].map { $0.reset() } + return [openWith, sendCopy, download, addToMyDrive].map { $0.reset() } } static var publicShareFolderActions: [FloatingPanelAction] { diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 8de668f52..caac30f3d 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -152,10 +152,6 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { - guard let currentUserDriveFileManager = accountManager.currentDriveFileManager else { - return - } - let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] From 691feee611f7a2b70c81383e16fad1598a7f7599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 18 Dec 2024 13:48:43 +0100 Subject: [PATCH 071/129] fix(AddToMykDrive): Big button selects the correct drive by default --- .../UI/Controller/Menu/Share/PublicShareViewModel.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 8de668f52..0043d00ab 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -152,6 +152,13 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { + guard accountManager.currentAccount != nil else { + // TODO merge #1350 for API +// let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) +// onPresentViewController?(.modal, upsaleFloatingPanelController, true) + return + } + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager else { return } @@ -159,7 +166,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] - let saveViewController = SaveFileViewController.instantiate(driveFileManager: driveFileManager) + let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) let saveNavigationViewController = SaveFileViewController .setInNavigationController(saveViewController: saveViewController) From b2f40c32019d4e05a1a643f27fd6f458d1853431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 18 Dec 2024 13:48:43 +0100 Subject: [PATCH 072/129] fix(AddToMykDrive): Big button selects the correct drive by default --- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 8de668f52..45cf23c11 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -159,7 +159,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] - let saveViewController = SaveFileViewController.instantiate(driveFileManager: driveFileManager) + let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) let saveNavigationViewController = SaveFileViewController .setInNavigationController(saveViewController: saveViewController) From 711de847887a54b7731d199c920b5aca88ca5b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Dec 2024 09:01:25 +0100 Subject: [PATCH 073/129] refactor(UniversalLinksHelper): Public share deeplinks removed from UniversalLinksHelper --- kDriveCore/DI/FactoryService.swift | 3 + .../Utils/Deeplinks/DeeplinkService.swift | 61 +++++++++++++++++++ .../Utils/Deeplinks/PublicShareLink.swift | 20 ++++++ .../Utils/UniversalLinksHelper.swift | 34 ++++------- 4 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 kDriveCore/Utils/Deeplinks/DeeplinkService.swift create mode 100644 kDriveCore/Utils/Deeplinks/PublicShareLink.swift rename {kDrive => kDriveCore}/Utils/UniversalLinksHelper.swift (88%) diff --git a/kDriveCore/DI/FactoryService.swift b/kDriveCore/DI/FactoryService.swift index ed72de093..5fb0de6e3 100644 --- a/kDriveCore/DI/FactoryService.swift +++ b/kDriveCore/DI/FactoryService.swift @@ -73,6 +73,9 @@ public enum FactoryService { }, Factory(type: FileProviderServiceable.self) { _, _ in FileProviderService() + }, + Factory(type: DeeplinkServiceable.self) { _, _ in + DeeplinkService() } ] return services diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift new file mode 100644 index 000000000..2554d7b37 --- /dev/null +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -0,0 +1,61 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 SwiftRegex + +public struct PublicShareLink { + public static let parsingRegex = Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#) + + public let publicShareURL: URL + public let shareLinkUid: String + public let driveId: Int + + public init?(publicShareURL: URL) async { + guard let components = URLComponents(url: publicShareURL, resolvingAgainstBaseURL: true) else { + return nil + } + + let path = components.path + guard let matches = Self.parsingRegex?.matches(in: path) else { + return nil + } + + guard let firstMatch = matches.first, + let driveId = firstMatch[safe: 1], + let driveIdInt = Int(driveId), + let shareLinkUid = firstMatch[safe: 2] else { + return nil + } + + self.driveId = driveIdInt + self.shareLinkUid = shareLinkUid + self.publicShareURL = publicShareURL + } +} + +public protocol DeeplinkServiceable: AnyObject { + func setLastPublicShare(_ link: PublicShareLink) +} + +public class DeeplinkService: DeeplinkServiceable { + var lastPublicShareLink: PublicShareLink? + public func setLastPublicShare(_ link: PublicShareLink) { + lastPublicShareLink = link + } +} diff --git a/kDriveCore/Utils/Deeplinks/PublicShareLink.swift b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift new file mode 100644 index 000000000..deba71ed1 --- /dev/null +++ b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift @@ -0,0 +1,20 @@ +// +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDriveCore/Utils/UniversalLinksHelper.swift similarity index 88% rename from kDrive/Utils/UniversalLinksHelper.swift rename to kDriveCore/Utils/UniversalLinksHelper.swift index a343fbf54..4461c6d72 100644 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ b/kDriveCore/Utils/UniversalLinksHelper.swift @@ -37,12 +37,6 @@ enum UniversalLinksHelper { displayMode: .file ) - /// Matches a public share link - static let publicShareLink = Link( - regex: Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#)!, - displayMode: .file - ) - /// Matches a directory list link static let directoryLink = Link(regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+)$"#)!, displayMode: .file) @@ -55,7 +49,7 @@ enum UniversalLinksHelper { /// Matches an office file link static let officeLink = Link(regex: Regex(pattern: #"^/app/office/([0-9]+)/([0-9]+)$"#)!, displayMode: .office) - static let all = [privateShareLink, publicShareLink, directoryLink, filePreview, officeLink] + static let all = [privateShareLink, directoryLink, filePreview, officeLink] } private enum DisplayMode { @@ -72,10 +66,8 @@ enum UniversalLinksHelper { let path = components.path DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") - // Public share link regex - let shareLink = Link.publicShareLink - let matches = shareLink.regex.matches(in: path) - if await processPublicShareLink(matches: matches, publicShareURL: url) { + if let publicShare = await PublicShareLink(publicShareURL: url), + await processPublicShareLink(publicShare) { return true } @@ -91,22 +83,18 @@ enum UniversalLinksHelper { return false } - private static func processPublicShareLink(matches: [[String]], publicShareURL: URL) async -> Bool { - guard let firstMatch = matches.first, - let driveId = firstMatch[safe: 1], - let driveIdInt = Int(driveId), - let shareLinkUid = firstMatch[safe: 2] else { - return false - } + public static func processPublicShareLink(_ link: PublicShareLink) async -> Bool { + @InjectService var deeplinkService: DeeplinkServiceable + deeplinkService.setLastPublicShare(link) - // request metadata let apiFetcher = PublicShareApiFetcher() do { - let metadata = try await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid) + let metadata = try await apiFetcher.getMetadata(driveId: link.driveId, shareLinkUid: link.shareLinkUid) + return await processPublicShareMetadata( metadata, - driveId: driveIdInt, - shareLinkUid: shareLinkUid, + driveId: link.driveId, + shareLinkUid: link.shareLinkUid, apiFetcher: apiFetcher ) } catch { @@ -118,7 +106,7 @@ enum UniversalLinksHelper { return false } - return await processPublicShareMetadataLimitation(limitation, publicShareURL: publicShareURL) + return await processPublicShareMetadataLimitation(limitation, publicShareURL: link.publicShareURL) } } From 909c5b796796f1a11d3e5c0944d553a971c34592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Dec 2024 09:48:33 +0100 Subject: [PATCH 074/129] feat(DeeplinkService): If app started with a public share link before authentication it is replayed after --- .../UI/Controller/LoginDelegateHandler.swift | 3 ++ .../Utils/Deeplinks/DeeplinkService.swift | 43 ++++++------------- .../Utils/Deeplinks/PublicShareLink.swift | 32 +++++++++++++- .../UniversalLinksHelper.swift | 7 +-- 4 files changed, 48 insertions(+), 37 deletions(-) rename kDriveCore/Utils/{ => Deeplinks}/UniversalLinksHelper.swift (98%) diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift index 99e39567d..9c82b8c55 100644 --- a/kDrive/UI/Controller/LoginDelegateHandler.swift +++ b/kDrive/UI/Controller/LoginDelegateHandler.swift @@ -26,6 +26,7 @@ import kDriveResources public final class LoginDelegateHandler: InfomaniakLoginDelegate { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var router: AppNavigable + @LazyInjectService var deeplinkService: DeeplinkServiceable var didStartLoginCallback: (() -> Void)? var didCompleteLoginCallback: (() -> Void)? @@ -57,6 +58,8 @@ public final class LoginDelegateHandler: InfomaniakLoginDelegate { } didCompleteLoginCallback?() + + deeplinkService.processDeeplinksPostAuthentication() } } diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift index 2554d7b37..c7df36f63 100644 --- a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -17,40 +17,10 @@ */ import Foundation -import SwiftRegex - -public struct PublicShareLink { - public static let parsingRegex = Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#) - - public let publicShareURL: URL - public let shareLinkUid: String - public let driveId: Int - - public init?(publicShareURL: URL) async { - guard let components = URLComponents(url: publicShareURL, resolvingAgainstBaseURL: true) else { - return nil - } - - let path = components.path - guard let matches = Self.parsingRegex?.matches(in: path) else { - return nil - } - - guard let firstMatch = matches.first, - let driveId = firstMatch[safe: 1], - let driveIdInt = Int(driveId), - let shareLinkUid = firstMatch[safe: 2] else { - return nil - } - - self.driveId = driveIdInt - self.shareLinkUid = shareLinkUid - self.publicShareURL = publicShareURL - } -} public protocol DeeplinkServiceable: AnyObject { func setLastPublicShare(_ link: PublicShareLink) + func processDeeplinksPostAuthentication() } public class DeeplinkService: DeeplinkServiceable { @@ -58,4 +28,15 @@ public class DeeplinkService: DeeplinkServiceable { public func setLastPublicShare(_ link: PublicShareLink) { lastPublicShareLink = link } + + public func processDeeplinksPostAuthentication() { + guard let lastPublicShareLink else { + return + } + + Task { + await UniversalLinksHelper.processPublicShareLink(lastPublicShareLink) + self.lastPublicShareLink = nil + } + } } diff --git a/kDriveCore/Utils/Deeplinks/PublicShareLink.swift b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift index deba71ed1..556c48237 100644 --- a/kDriveCore/Utils/Deeplinks/PublicShareLink.swift +++ b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift @@ -1,4 +1,3 @@ -// /* Infomaniak kDrive - iOS App Copyright (C) 2024 Infomaniak Network SA @@ -18,3 +17,34 @@ */ import Foundation +import SwiftRegex + +public struct PublicShareLink: Sendable { + public static let parsingRegex = Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#) + + public let publicShareURL: URL + public let shareLinkUid: String + public let driveId: Int + + public init?(publicShareURL: URL) async { + guard let components = URLComponents(url: publicShareURL, resolvingAgainstBaseURL: true) else { + return nil + } + + let path = components.path + guard let matches = Self.parsingRegex?.matches(in: path) else { + return nil + } + + guard let firstMatch = matches.first, + let driveId = firstMatch[safe: 1], + let driveIdInt = Int(driveId), + let shareLinkUid = firstMatch[safe: 2] else { + return nil + } + + self.driveId = driveIdInt + self.shareLinkUid = shareLinkUid + self.publicShareURL = publicShareURL + } +} diff --git a/kDriveCore/Utils/UniversalLinksHelper.swift b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift similarity index 98% rename from kDriveCore/Utils/UniversalLinksHelper.swift rename to kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift index 4461c6d72..82e72631e 100644 --- a/kDriveCore/Utils/UniversalLinksHelper.swift +++ b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift @@ -20,13 +20,11 @@ import CocoaLumberjackSwift import Foundation import InfomaniakCore import InfomaniakDI -import kDriveCore import kDriveResources import SwiftRegex import UIKit -#if !ISEXTENSION -enum UniversalLinksHelper { +public enum UniversalLinksHelper { private struct Link { let regex: Regex let displayMode: DisplayMode @@ -57,7 +55,7 @@ enum UniversalLinksHelper { } @discardableResult - static func handleURL(_ url: URL) async -> Bool { + public static func handleURL(_ url: URL) async -> Bool { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { DDLogError("[UniversalLinksHelper] Failed to process url:\(url)") return false @@ -217,4 +215,3 @@ enum UniversalLinksHelper { } } } -#endif From 4d709db3bd5c4a123d9a1b60e819a4c2b4b0b759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 19 Dec 2024 15:34:28 +0100 Subject: [PATCH 075/129] chore: PR feedback --- kDrive/AppRouter.swift | 2 +- .../Controller/Files/File List/FileListViewModel.swift | 2 +- .../File List/MultipleSelectionFileListViewModel.swift | 10 +++++----- .../Controller/Menu/Share/PublicShareViewModel.swift | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 70fa70cb5..67ef64ef6 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -675,7 +675,7 @@ public struct AppRouter: AppNavigable { apiFetcher: apiFetcher, configuration: configuration) let viewController = FileListViewController(viewModel: viewModel) - viewModel.dismissClosure = { [weak viewController] in + viewModel.onDismiss = { [weak viewController] in viewController?.dismiss(animated: true) } diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 93cc3086c..6c323bdbd 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -93,7 +93,7 @@ class FileListViewModel: SelectDelegate { var matomoViewPath = ["FileList"] } - var dismissClosure: (() -> Void)? + var onDismiss: (() -> Void)? var realmObservationToken: NotificationToken? var currentDirectoryObservationToken: NotificationToken? diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index c09a7cf79..7d9223835 100644 --- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift @@ -41,27 +41,27 @@ struct MultipleSelectionAction: Equatable { } static let move = MultipleSelectionAction( - id: MultipleSelectionActionId.move, + id: .move, name: KDriveResourcesStrings.Localizable.buttonMove, icon: KDriveResourcesAsset.folderSelect ) static let delete = MultipleSelectionAction( - id: MultipleSelectionActionId.delete, + id: .delete, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) static let more = MultipleSelectionAction( - id: MultipleSelectionActionId.more, + id: .more, name: KDriveResourcesStrings.Localizable.buttonMenu, icon: KDriveResourcesAsset.menu ) static let deletePermanently = MultipleSelectionAction( - id: MultipleSelectionActionId.deletePermanently, + id: .deletePermanently, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) static let download = MultipleSelectionAction( - id: MultipleSelectionActionId.download, + id: .download, name: KDriveResourcesStrings.Localizable.buttonDownload, icon: KDriveResourcesAsset.menu ) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 7ecdec650..834ac503e 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -85,7 +85,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { // We try to close the "Public Share screen" if type == .cancel, !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true) { - dismissClosure?() + onDismiss?() return } From dfadb1d5f7c5c83b7da73dfc0d3b83a25adcb4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 30 Dec 2024 10:19:57 +0100 Subject: [PATCH 076/129] refactor(DownloadOperation): Made a dedicated DownloadPublicShareOperation --- .../DownloadArchiveOperation.swift | 0 .../DownloadOperation.swift | 59 ++---------- .../DownloadPublicShareOperation.swift | 95 +++++++++++++++++++ .../Data/DownloadQueue/DownloadQueue.swift | 2 +- 4 files changed, 103 insertions(+), 53 deletions(-) rename kDriveCore/Data/DownloadQueue/{ => DownloadOperation}/DownloadArchiveOperation.swift (100%) rename kDriveCore/Data/DownloadQueue/{ => DownloadOperation}/DownloadOperation.swift (83%) create mode 100644 kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift diff --git a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift similarity index 100% rename from kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift rename to kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift similarity index 83% rename from kDriveCore/Data/DownloadQueue/DownloadOperation.swift rename to kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift index 2002c47cc..ef54e3646 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -36,20 +36,19 @@ public class DownloadOperation: Operation, DownloadOperationable { // MARK: - Attributes private let fileManager = FileManager.default - private let driveFileManager: DriveFileManager - private let urlSession: FileDownloadSession - private let publicShareProxy: PublicShareProxy? - private let itemIdentifier: NSFileProviderItemIdentifier? - private var progressObservation: NSKeyValueObservation? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) private var uploadsDatabase: Transactionable - + @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) var uploadsDatabase: Transactionable @LazyInjectService var accountManager: AccountManageable @LazyInjectService var driveInfosManager: DriveInfosManager @LazyInjectService var downloadManager: BackgroundDownloadSessionManager @LazyInjectService var appContextService: AppContextServiceable + let urlSession: FileDownloadSession + let driveFileManager: DriveFileManager + let itemIdentifier: NSFileProviderItemIdentifier? + var progressObservation: NSKeyValueObservation? + public let file: File public var task: URLSessionDownloadTask? public var error: DriveError? @@ -94,13 +93,11 @@ public class DownloadOperation: Operation, DownloadOperationable { file: File, driveFileManager: DriveFileManager, urlSession: FileDownloadSession, - publicShareProxy: PublicShareProxy? = nil, itemIdentifier: NSFileProviderItemIdentifier? = nil ) { self.file = File(value: file) self.driveFileManager = driveFileManager self.urlSession = urlSession - self.publicShareProxy = publicShareProxy self.itemIdentifier = itemIdentifier } @@ -112,7 +109,6 @@ public class DownloadOperation: Operation, DownloadOperationable { self.driveFileManager = driveFileManager self.urlSession = urlSession self.task = task - publicShareProxy = nil itemIdentifier = nil } @@ -179,48 +175,7 @@ public class DownloadOperation: Operation, DownloadOperationable { override public func main() { DDLogInfo("[DownloadOperation] Start for \(file.id) with session \(urlSession.identifier)") - if let publicShareProxy { - downloadPublicShareFile(publicShareProxy: publicShareProxy) - } else { - downloadFile() - } - } - - private func downloadPublicShareFile(publicShareProxy: PublicShareProxy) { - DDLogInfo("[DownloadOperation] Downloading publicShare \(file.id) with session \(urlSession.identifier)") - - let url = Endpoint.download(file: file, publicShareProxy: publicShareProxy).url - - // Add download task to Realm - let downloadTask = DownloadTask( - fileId: file.id, - isDirectory: file.isDirectory, - driveId: file.driveId, - userId: driveFileManager.drive.userId, - sessionId: urlSession.identifier, - sessionUrl: url.absoluteString - ) - - try? uploadsDatabase.writeTransaction { writableRealm in - writableRealm.add(downloadTask, update: .modified) - } - - let request = URLRequest(url: url) - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: fileId) - } - if let itemIdentifier { - driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in - manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in - // META: keep SonarCloud happy - } - } - } - task?.resume() + downloadFile() } private func downloadFile() { diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift new file mode 100644 index 000000000..210678ac6 --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift @@ -0,0 +1,95 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 CocoaLumberjackSwift +import FileProvider +import Foundation +import InfomaniakCore +import InfomaniakCoreDB +import InfomaniakDI +import InfomaniakLogin + +public final class DownloadPublicShareOperation: DownloadOperation { + private let publicShareProxy: PublicShareProxy + + override public init( + file: File, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession, + itemIdentifier: NSFileProviderItemIdentifier? = nil + ) { + fatalError("Unavailable") + } + + public init( + file: File, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession, + publicShareProxy: PublicShareProxy, + itemIdentifier: NSFileProviderItemIdentifier? = nil + ) { + self.publicShareProxy = publicShareProxy + super.init(file: file, + driveFileManager: driveFileManager, + urlSession: urlSession, + itemIdentifier: itemIdentifier) + } + + override public func main() { + DDLogInfo("[DownloadPublicShareOperation] Start for \(file.id) with session \(urlSession.identifier)") + + downloadPublicShareFile(publicShareProxy: publicShareProxy) + } + + private func downloadPublicShareFile(publicShareProxy: PublicShareProxy) { + DDLogInfo("[DownloadPublicShareOperation] Downloading publicShare \(file.id) with session \(urlSession.identifier)") + + let url = Endpoint.download(file: file, publicShareProxy: publicShareProxy).url + + // Add download task to Realm + let downloadTask = DownloadTask( + fileId: file.id, + isDirectory: file.isDirectory, + driveId: file.driveId, + userId: driveFileManager.drive.userId, + sessionId: urlSession.identifier, + sessionUrl: url.absoluteString + ) + + try? uploadsDatabase.writeTransaction { writableRealm in + writableRealm.add(downloadTask, update: .modified) + } + + let request = URLRequest(url: url) + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: fileId) + } + if let itemIdentifier { + driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in + manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in + // META: keep SonarCloud happy + } + } + } + task?.resume() + } +} diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 0fd65e28b..d9fccc088 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -127,7 +127,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadOperation( + let operation = DownloadPublicShareOperation( file: file, driveFileManager: driveFileManager, urlSession: self.bestSession, From 0afa9b38243e64e7b4982ddd76b1c944ee39bcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 30 Dec 2024 10:37:08 +0100 Subject: [PATCH 077/129] refactor(DownloadOperation): Factorised download request --- .../DownloadOperation/DownloadOperation.swift | 34 +++++++++++-------- .../DownloadPublicShareOperation.swift | 16 +-------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift index ef54e3646..606fa665b 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -200,27 +200,31 @@ public class DownloadOperation: Operation, DownloadOperationable { if let token = getToken() { var request = URLRequest(url: url) request.setValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization") - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: fileId) - } - if let itemIdentifier { - driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in - manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in - // META: keep SonarCloud happy - } - } - } - task?.resume() + downloadRequest(request) } else { error = .unknownToken // Other error? end(sessionUrl: url) } } + func downloadRequest(_ request: URLRequest) { + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: fileId) + } + if let itemIdentifier { + driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in + manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in + // META: keep SonarCloud happy + } + } + } + task?.resume() + } + override public func cancel() { DDLogInfo("[DownloadOperation] Download of \(file.id) canceled") super.cancel() diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift index 210678ac6..28d755993 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift @@ -76,20 +76,6 @@ public final class DownloadPublicShareOperation: DownloadOperation { } let request = URLRequest(url: url) - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: fileId) - } - if let itemIdentifier { - driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in - manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in - // META: keep SonarCloud happy - } - } - } - task?.resume() + downloadRequest(request) } } From 42a8b052d8ca545d5f2105af6974e2a1965db807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 30 Dec 2024 11:12:46 +0100 Subject: [PATCH 078/129] chore: PR Feedback --- .../DownloadOperation/DownloadOperation.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift index 606fa665b..8e104f79f 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -36,6 +36,7 @@ public class DownloadOperation: Operation, DownloadOperationable { // MARK: - Attributes private let fileManager = FileManager.default + private let itemIdentifier: NSFileProviderItemIdentifier? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) var uploadsDatabase: Transactionable @@ -46,7 +47,6 @@ public class DownloadOperation: Operation, DownloadOperationable { let urlSession: FileDownloadSession let driveFileManager: DriveFileManager - let itemIdentifier: NSFileProviderItemIdentifier? var progressObservation: NSKeyValueObservation? public let file: File @@ -301,7 +301,10 @@ public class DownloadOperation: Operation, DownloadOperationable { return } - assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation error:\(error)") + assert( + file.isDownloaded, + "Expecting to be downloaded at the end of the downloadOperation error:\(String(describing: error))" + ) try? uploadsDatabase.writeTransaction { writableRealm in guard let task = writableRealm.objects(DownloadTask.self) From 3ff747cf5ce36778d2f913c7ccd4711154251930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 30 Dec 2024 11:38:52 +0100 Subject: [PATCH 079/129] refactor(DownloadArchiveOperation): Split dedicated public share code --- .../DownloadArchiveOperation.swift | 43 ++---------- .../DownloadPublicShareArchiveOperation.swift | 69 +++++++++++++++++++ .../Data/DownloadQueue/DownloadQueue.swift | 2 +- 3 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index 1877cad09..4ae283bac 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -29,14 +29,14 @@ public class DownloadArchiveOperation: Operation { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var appContextService: AppContextServiceable - private let archiveId: String - private let shareDrive: AbstractDrive private let driveFileManager: DriveFileManager - private let urlSession: FileDownloadSession - private let publicShareProxy: PublicShareProxy? - private var progressObservation: NSKeyValueObservation? private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + let archiveId: String + let shareDrive: AbstractDrive + let urlSession: FileDownloadSession + var progressObservation: NSKeyValueObservation? + public var task: URLSessionDownloadTask? public var error: DriveError? public var archiveUrl: URL? @@ -74,13 +74,11 @@ public class DownloadArchiveOperation: Operation { public init(archiveId: String, shareDrive: AbstractDrive, driveFileManager: DriveFileManager, - urlSession: FileDownloadSession, - publicShareProxy: PublicShareProxy? = nil) { + urlSession: FileDownloadSession) { self.archiveId = archiveId self.shareDrive = shareDrive self.driveFileManager = driveFileManager self.urlSession = urlSession - self.publicShareProxy = publicShareProxy } // MARK: - Public methods @@ -112,34 +110,7 @@ public class DownloadArchiveOperation: Operation { } override public func main() { - guard let publicShareProxy else { - authenticatedDownload() - return - } - - publicShareDownload(proxy: publicShareProxy) - } - - func publicShareDownload(proxy: PublicShareProxy) { - DDLogInfo( - "[DownloadOperation] Downloading Archive of public share files \(archiveId) with session \(urlSession.identifier)" - ) - - let url = Endpoint.downloadPublicShareArchive( - drive: shareDrive, - linkUuid: proxy.shareLinkUid, - archiveUuid: archiveId - ).url - let request = URLRequest(url: url) - - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: self.archiveId) - } - task?.resume() + authenticatedDownload() } func authenticatedDownload() { diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift new file mode 100644 index 000000000..3bbb611a1 --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift @@ -0,0 +1,69 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 CocoaLumberjackSwift +import FileProvider +import Foundation +import InfomaniakCore +import InfomaniakDI + +public final class DownloadPublicShareArchiveOperation: DownloadArchiveOperation { + private let publicShareProxy: PublicShareProxy + + public init(archiveId: String, + shareDrive: AbstractDrive, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession, + publicShareProxy: PublicShareProxy) { + self.publicShareProxy = publicShareProxy + super.init(archiveId: archiveId, shareDrive: shareDrive, driveFileManager: driveFileManager, urlSession: urlSession) + } + + override public init(archiveId: String, + shareDrive: AbstractDrive, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession) { + fatalError("Unavailable") + } + + override public func main() { + publicShareDownload() + } + + func publicShareDownload() { + DDLogInfo( + "[DownloadPublicShareArchiveOperation] Downloading Archive of public share files \(archiveId) with session \(urlSession.identifier)" + ) + + let url = Endpoint.downloadPublicShareArchive( + drive: shareDrive, + linkUuid: publicShareProxy.shareLinkUid, + archiveUuid: archiveId + ).url + let request = URLRequest(url: url) + + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: self.archiveId) + } + task?.resume() + } +} diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 8a58b05c9..4a6b5f9fc 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -195,7 +195,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { dispatchQueue.async { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadArchiveOperation( + let operation = DownloadPublicShareArchiveOperation( archiveId: archiveId, shareDrive: publicShareProxy.proxyDrive, driveFileManager: driveFileManager, From d96e98db7ca21360fd47f84543219c8198712c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 30 Dec 2024 11:48:52 +0100 Subject: [PATCH 080/129] refactor(DownloadArchiveOperation): Factorised download request --- .../DownloadArchiveOperation.swift | 22 +++++++++++-------- .../DownloadPublicShareArchiveOperation.swift | 11 ++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index 4ae283bac..e57864390 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -31,11 +31,11 @@ public class DownloadArchiveOperation: Operation { private let driveFileManager: DriveFileManager private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + private var progressObservation: NSKeyValueObservation? let archiveId: String let shareDrive: AbstractDrive let urlSession: FileDownloadSession - var progressObservation: NSKeyValueObservation? public var task: URLSessionDownloadTask? public var error: DriveError? @@ -123,14 +123,7 @@ public class DownloadArchiveOperation: Operation { if let token { var request = URLRequest(url: url) request.setValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization") - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: archiveId) - } - task?.resume() + downloadRequest(request) } else { error = .localError // Other error? end(sessionUrl: url) @@ -142,6 +135,17 @@ public class DownloadArchiveOperation: Operation { } } + func downloadRequest(_ request: URLRequest) { + task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) + progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in + guard let newValue = value.newValue else { + return + } + DownloadQueue.instance.publishProgress(newValue, for: self.archiveId) + } + task?.resume() + } + func downloadCompletion(url: URL?, response: URLResponse?, error: Error?) { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift index 3bbb611a1..45ea097a1 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift @@ -55,15 +55,8 @@ public final class DownloadPublicShareArchiveOperation: DownloadArchiveOperation linkUuid: publicShareProxy.shareLinkUid, archiveUuid: archiveId ).url - let request = URLRequest(url: url) - task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion) - progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { _, value in - guard let newValue = value.newValue else { - return - } - DownloadQueue.instance.publishProgress(newValue, for: self.archiveId) - } - task?.resume() + let request = URLRequest(url: url) + downloadRequest(request) } } From 62ddb92ef6d25895951dbdaba0cd994005cc3668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 08:53:48 +0100 Subject: [PATCH 081/129] refactor(DownloadOperation): Renamed DownloadAuthenticatedOperation --- .../Files/Preview/PreviewViewController.swift | 2 +- .../BackgroundDownloadSessionManager.swift | 4 ++-- ...tion.swift => DownloadAuthenticatedOperation.swift} | 2 +- .../DownloadPublicShareOperation.swift | 2 +- kDriveCore/Data/DownloadQueue/DownloadQueue.swift | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) rename kDriveCore/Data/DownloadQueue/DownloadOperation/{DownloadOperation.swift => DownloadAuthenticatedOperation.swift} (99%) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 6cf1941fa..c910cfea1 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -52,7 +52,7 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } } - private var currentDownloadOperation: DownloadOperation? + private var currentDownloadOperation: DownloadAuthenticatedOperation? private let pdfPageLabel = UILabel(frame: .zero) private var titleWidthConstraint: NSLayoutConstraint? private var titleHeightConstraint: NSLayoutConstraint? diff --git a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift index fbaaee339..2b1a65efc 100644 --- a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift +++ b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift @@ -59,7 +59,7 @@ public final class BackgroundDownloadSessionManager: NSObject, BackgroundDownloa public typealias Task = URLSessionDownloadTask public typealias CompletionHandler = (URL?, URLResponse?, Error?) -> Void - public typealias Operation = DownloadOperation + public typealias Operation = DownloadAuthenticatedOperation public var backgroundCompletionHandler: (() -> Void)? @@ -167,7 +167,7 @@ public final class BackgroundDownloadSessionManager: NSObject, BackgroundDownloa userId: downloadTask.userId ), let file = driveFileManager.getCachedFile(id: downloadTask.fileId) { - let operation = DownloadOperation(file: file, driveFileManager: driveFileManager, task: task, urlSession: self) + let operation = DownloadAuthenticatedOperation(file: file, driveFileManager: driveFileManager, task: task, urlSession: self) tasksCompletionHandler[taskIdentifier] = operation.downloadCompletion operations.append(operation) return operation.downloadCompletion diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift similarity index 99% rename from kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift rename to kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index 8e104f79f..2b181af79 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -32,7 +32,7 @@ public protocol DownloadOperationable: Operationable { var file: File { get } } -public class DownloadOperation: Operation, DownloadOperationable { +public class DownloadAuthenticatedOperation: Operation, DownloadOperationable { // MARK: - Attributes private let fileManager = FileManager.default diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift index 28d755993..2aff46bd2 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift @@ -24,7 +24,7 @@ import InfomaniakCoreDB import InfomaniakDI import InfomaniakLogin -public final class DownloadPublicShareOperation: DownloadOperation { +public final class DownloadPublicShareOperation: DownloadAuthenticatedOperation { private let publicShareProxy: PublicShareProxy override public init( diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 4a6b5f9fc..9bda7356c 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -75,7 +75,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { public static let instance = DownloadQueue() public static let backgroundIdentifier = "com.infomaniak.background.download" - public private(set) var operationsInQueue = SendableDictionary() + public private(set) var operationsInQueue = SendableDictionary() public private(set) var archiveOperationsInQueue = SendableDictionary() private(set) lazy var operationQueue: OperationQueue = { let queue = OperationQueue() @@ -170,7 +170,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadOperation( + let operation = DownloadAuthenticatedOperation( file: file, driveFileManager: driveFileManager, urlSession: self.bestSession, @@ -246,7 +246,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { public func temporaryDownload(file: File, userId: Int, - onOperationCreated: ((DownloadOperation?) -> Void)? = nil, + onOperationCreated: ((DownloadAuthenticatedOperation?) -> Void)? = nil, completion: @escaping (DriveError?) -> Void) { Log.downloadQueue("temporaryDownload file:\(file.id)") dispatchQueue.async(qos: .userInitiated) { [ @@ -263,7 +263,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadOperation(file: file, driveFileManager: driveFileManager, urlSession: self.foregroundSession) + let operation = DownloadAuthenticatedOperation(file: file, driveFileManager: driveFileManager, urlSession: self.foregroundSession) operation.completionBlock = { self.dispatchQueue.async { self.operationsInQueue.removeValue(forKey: fileId) @@ -296,7 +296,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { /// /// Thread safe /// Lookup O(1) as Dictionary backed - public func operation(for fileId: Int) -> DownloadOperation? { + public func operation(for fileId: Int) -> DownloadAuthenticatedOperation? { return operationsInQueue[fileId] } From 351a198bb1a724d002259d44738a2ade7645c21a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 08:58:34 +0100 Subject: [PATCH 082/129] refactor(DownloadOperationable): Renamed DownloadFileOperationable --- .../DownloadQueue/BackgroundDownloadSessionManager.swift | 2 +- .../DownloadOperation/DownloadAuthenticatedOperation.swift | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift index 2b1a65efc..a9195f75e 100644 --- a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift +++ b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift @@ -68,7 +68,7 @@ public final class BackgroundDownloadSessionManager: NSObject, BackgroundDownloa var backgroundSession: URLSession! var tasksCompletionHandler: [String: CompletionHandler] = [:] var progressObservers: [String: NSKeyValueObservation] = [:] - var operations = [DownloadOperationable]() + var operations = [DownloadFileOperationable]() override public init() { super.init() diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index 2b181af79..cdc601922 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -24,15 +24,14 @@ import InfomaniakCoreDB import InfomaniakDI import InfomaniakLogin -/// Something that can download a file. -public protocol DownloadOperationable: Operationable { +public protocol DownloadFileOperationable: Operationable { /// Called upon request completion func downloadCompletion(url: URL?, response: URLResponse?, error: Error?) var file: File { get } } -public class DownloadAuthenticatedOperation: Operation, DownloadOperationable { +public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationable { // MARK: - Attributes private let fileManager = FileManager.default From 2d60725a4ab7c9427d19bdf5afddfe7e9b9c7892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 09:19:05 +0100 Subject: [PATCH 083/129] chore(DownloadOperation): Explicit sendable requirements from base Operation class --- .../DownloadOperation/DownloadArchiveOperation.swift | 2 +- .../DownloadOperation/DownloadAuthenticatedOperation.swift | 2 +- .../DownloadOperation/DownloadPublicShareArchiveOperation.swift | 2 +- .../DownloadOperation/DownloadPublicShareOperation.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index e57864390..41126522f 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -23,7 +23,7 @@ import Foundation import InfomaniakCore import InfomaniakDI -public class DownloadArchiveOperation: Operation { +public class DownloadArchiveOperation: Operation, @unchecked Sendable { // MARK: - Attributes @LazyInjectService var accountManager: AccountManageable diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index cdc601922..2378a0181 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -31,7 +31,7 @@ public protocol DownloadFileOperationable: Operationable { var file: File { get } } -public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationable { +public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationable, @unchecked Sendable { // MARK: - Attributes private let fileManager = FileManager.default diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift index 45ea097a1..b280adf58 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift @@ -22,7 +22,7 @@ import Foundation import InfomaniakCore import InfomaniakDI -public final class DownloadPublicShareArchiveOperation: DownloadArchiveOperation { +public final class DownloadPublicShareArchiveOperation: DownloadArchiveOperation, @unchecked Sendable { private let publicShareProxy: PublicShareProxy public init(archiveId: String, diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift index 2aff46bd2..f058176e7 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift @@ -24,7 +24,7 @@ import InfomaniakCoreDB import InfomaniakDI import InfomaniakLogin -public final class DownloadPublicShareOperation: DownloadAuthenticatedOperation { +public final class DownloadPublicShareOperation: DownloadAuthenticatedOperation, @unchecked Sendable { private let publicShareProxy: PublicShareProxy override public init( From 052442b65f1bb4e52b2e35e1dc073823263bf952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 10:48:55 +0100 Subject: [PATCH 084/129] refactor(DownloadOperation): Factorise a base DownloadOperation. --- .../DownloadArchiveOperation.swift | 53 +----------- .../DownloadAuthenticatedOperation.swift | 53 +----------- .../DownloadOperation/DownloadOperation.swift | 80 +++++++++++++++++++ 3 files changed, 85 insertions(+), 101 deletions(-) create mode 100644 kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index 41126522f..a65329641 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -23,54 +23,17 @@ import Foundation import InfomaniakCore import InfomaniakDI -public class DownloadArchiveOperation: Operation, @unchecked Sendable { +public class DownloadArchiveOperation: DownloadOperation, @unchecked Sendable { // MARK: - Attributes - @LazyInjectService var accountManager: AccountManageable - @LazyInjectService var appContextService: AppContextServiceable - private let driveFileManager: DriveFileManager - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - private var progressObservation: NSKeyValueObservation? let archiveId: String let shareDrive: AbstractDrive let urlSession: FileDownloadSession - public var task: URLSessionDownloadTask? - public var error: DriveError? public var archiveUrl: URL? - private var _executing = false { - willSet { - willChangeValue(forKey: "isExecuting") - } - didSet { - didChangeValue(forKey: "isExecuting") - } - } - - private var _finished = false { - willSet { - willChangeValue(forKey: "isFinished") - } - didSet { - didChangeValue(forKey: "isFinished") - } - } - - override public var isExecuting: Bool { - return _executing - } - - override public var isFinished: Bool { - return _finished - } - - override public var isAsynchronous: Bool { - return true - } - public init(archiveId: String, shareDrive: AbstractDrive, driveFileManager: DriveFileManager, @@ -189,20 +152,8 @@ public class DownloadArchiveOperation: Operation, @unchecked Sendable { end(sessionUrl: task?.originalRequest?.url) } - override public func cancel() { - super.cancel() - task?.cancel() - } - private func end(sessionUrl: URL?) { DDLogInfo("[DownloadOperation] Download of archive \(archiveId) ended") - - progressObservation?.invalidate() - if backgroundTaskIdentifier != .invalid { - UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) - } - - _executing = false - _finished = true + endBackgroundTaskObservation() } } diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index 2378a0181..8cb2fef91 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -31,61 +31,25 @@ public protocol DownloadFileOperationable: Operationable { var file: File { get } } -public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationable, @unchecked Sendable { +public class DownloadAuthenticatedOperation: DownloadOperation, DownloadFileOperationable, @unchecked Sendable { // MARK: - Attributes private let fileManager = FileManager.default private let itemIdentifier: NSFileProviderItemIdentifier? - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) var uploadsDatabase: Transactionable - @LazyInjectService var accountManager: AccountManageable @LazyInjectService var driveInfosManager: DriveInfosManager @LazyInjectService var downloadManager: BackgroundDownloadSessionManager - @LazyInjectService var appContextService: AppContextServiceable let urlSession: FileDownloadSession let driveFileManager: DriveFileManager - var progressObservation: NSKeyValueObservation? public let file: File - public var task: URLSessionDownloadTask? - public var error: DriveError? public var fileId: Int { return file.id } - private var _executing = false { - willSet { - willChangeValue(forKey: "isExecuting") - } - didSet { - didChangeValue(forKey: "isExecuting") - } - } - - private var _finished = false { - willSet { - willChangeValue(forKey: "isFinished") - } - didSet { - didChangeValue(forKey: "isFinished") - } - } - - override public var isExecuting: Bool { - return _executing - } - - override public var isFinished: Bool { - return _finished - } - - override public var isAsynchronous: Bool { - return true - } - // MARK: - Public methods public init( @@ -107,8 +71,8 @@ public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationabl self.file = file self.driveFileManager = driveFileManager self.urlSession = urlSession - self.task = task itemIdentifier = nil + super.init(task: task) } override public func start() { @@ -224,12 +188,6 @@ public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationabl task?.resume() } - override public func cancel() { - DDLogInfo("[DownloadOperation] Download of \(file.id) canceled") - super.cancel() - task?.cancel() - } - // MARK: - methods public func downloadCompletion(url: URL?, response: URLResponse?, error: Error?) { @@ -287,12 +245,7 @@ public class DownloadAuthenticatedOperation: Operation, DownloadFileOperationabl DDLogInfo("[DownloadOperation] Download of \(file.id) ended") defer { - progressObservation?.invalidate() - if backgroundTaskIdentifier != .invalid { - UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) - } - _executing = false - _finished = true + endBackgroundTaskObservation() } // Delete download task diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift new file mode 100644 index 000000000..a917ad4cd --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -0,0 +1,80 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 InfomaniakDI +import UIKit + +public class DownloadOperation: Operation, @unchecked Sendable { + @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var appContextService: AppContextServiceable + + var task: URLSessionDownloadTask? + var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + var progressObservation: NSKeyValueObservation? + + public var error: DriveError? + + init(task: URLSessionDownloadTask? = nil) { + self.task = task + } + + var _executing = false { + willSet { + willChangeValue(forKey: "isExecuting") + } + didSet { + didChangeValue(forKey: "isExecuting") + } + } + + var _finished = false { + willSet { + willChangeValue(forKey: "isFinished") + } + didSet { + didChangeValue(forKey: "isFinished") + } + } + + override public var isExecuting: Bool { + return _executing + } + + override public var isFinished: Bool { + return _finished + } + + override public var isAsynchronous: Bool { + return true + } + + override public func cancel() { + super.cancel() + task?.cancel() + } + + func endBackgroundTaskObservation() { + progressObservation?.invalidate() + if backgroundTaskIdentifier != .invalid { + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + + _executing = false + _finished = true + } +} From e93a347ca98801ad7033e98b66ba8b8ba5ac7d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 11:12:31 +0100 Subject: [PATCH 085/129] chore: Sonar naming conventions --- .../DownloadOperation/DownloadArchiveOperation.swift | 2 +- .../DownloadAuthenticatedOperation.swift | 2 +- .../DownloadOperation/DownloadOperation.swift | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index a65329641..bce52ad19 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -68,7 +68,7 @@ public class DownloadArchiveOperation: DownloadOperation, @unchecked Sendable { } // If the operation is not canceled, begin executing the task - _executing = true + operationExecuting = true main() } diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index 8cb2fef91..ccb4e6a1b 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -116,7 +116,7 @@ public class DownloadAuthenticatedOperation: DownloadOperation, DownloadFileOper } // If the operation is not canceled, begin executing the task - _executing = true + operationExecuting = true main() } diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift index a917ad4cd..907ab3e55 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -33,7 +33,7 @@ public class DownloadOperation: Operation, @unchecked Sendable { self.task = task } - var _executing = false { + var operationExecuting = false { willSet { willChangeValue(forKey: "isExecuting") } @@ -42,7 +42,7 @@ public class DownloadOperation: Operation, @unchecked Sendable { } } - var _finished = false { + var operationFinished = false { willSet { willChangeValue(forKey: "isFinished") } @@ -52,11 +52,11 @@ public class DownloadOperation: Operation, @unchecked Sendable { } override public var isExecuting: Bool { - return _executing + return operationExecuting } override public var isFinished: Bool { - return _finished + return operationFinished } override public var isAsynchronous: Bool { @@ -74,7 +74,7 @@ public class DownloadOperation: Operation, @unchecked Sendable { UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } - _executing = false - _finished = true + operationExecuting = false + operationFinished = true } } From f23e75328017308b32aef7392cd4ae70d8547480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 15:08:32 +0100 Subject: [PATCH 086/129] chore: PR Feedback --- kDriveCore/Utils/Deeplinks/DeeplinkService.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift index c7df36f63..a9559c1d4 100644 --- a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -25,6 +25,7 @@ public protocol DeeplinkServiceable: AnyObject { public class DeeplinkService: DeeplinkServiceable { var lastPublicShareLink: PublicShareLink? + public func setLastPublicShare(_ link: PublicShareLink) { lastPublicShareLink = link } From d8eb82aef1bc2c952e7017f35c69e17a190baa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 18:30:54 +0100 Subject: [PATCH 087/129] fix(VideoPlayer): Disable streaming for VideoPlayer within a public share --- kDriveCore/VideoPlayer/VideoPlayer.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kDriveCore/VideoPlayer/VideoPlayer.swift b/kDriveCore/VideoPlayer/VideoPlayer.swift index 20c0cccb1..f833f9e52 100644 --- a/kDriveCore/VideoPlayer/VideoPlayer.swift +++ b/kDriveCore/VideoPlayer/VideoPlayer.swift @@ -91,6 +91,10 @@ public final class VideoPlayer: Pausable { } private func setupPlayer(with file: File, driveFileManager: DriveFileManager) { + guard !driveFileManager.isPublicShare else { + return + } + if !file.isLocalVersionOlderThanRemote { let localAsset = AVAsset(url: file.localUrl) asset = localAsset From 37464bca26d99a938daeb55f99ef1dc57c8c02d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 2 Jan 2025 18:40:38 +0100 Subject: [PATCH 088/129] refactor(APIPublicShareParameter): Renamed PublicShareAPIParameters --- kDriveCore/Data/Api/DriveApiFetcher.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index 84c26f79f..ef4c17838 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -560,19 +560,19 @@ public class DriveApiFetcher: ApiFetcher { let destinationDrive = ProxyDrive(id: destinationDriveId) let importShareLinkFiles = Endpoint.importShareLinkFiles(destinationDrive: destinationDrive) var requestParameters: Parameters = [ - APIPublicShareParameter.sourceDriveId: sourceDriveId, - APIPublicShareParameter.destinationFolderId: destinationFolderId, - APIPublicShareParameter.sharelinkUuid: sharelinkUuid + PublicShareAPIParameters.sourceDriveId: sourceDriveId, + PublicShareAPIParameters.destinationFolderId: destinationFolderId, + PublicShareAPIParameters.sharelinkUuid: sharelinkUuid ] if let fileIds, !fileIds.isEmpty { - requestParameters[APIPublicShareParameter.fileIds] = fileIds + requestParameters[PublicShareAPIParameters.fileIds] = fileIds } else if let exceptIds, !exceptIds.isEmpty { - requestParameters[APIPublicShareParameter.exceptFileIds] = exceptIds + requestParameters[PublicShareAPIParameters.exceptFileIds] = exceptIds } if let password { - requestParameters[APIPublicShareParameter.password] = password + requestParameters[PublicShareAPIParameters.password] = password } let result: ValidServerResponse = try await perform(request: authenticatedRequest( @@ -584,7 +584,7 @@ public class DriveApiFetcher: ApiFetcher { } } -enum APIPublicShareParameter { +enum PublicShareAPIParameters { static let sourceDriveId = "source_drive_id" static let fileIds = "file_ids" static let exceptFileIds = "except_file_ids" From 2c1fbf46562d67fbd6d4c7c18f0b557fa5f1ff67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 6 Jan 2025 08:29:40 +0100 Subject: [PATCH 089/129] chore: Self assessed PR feedback --- .../Files/File List/FileListViewController.swift | 4 ++-- .../UI/Controller/Menu/Share/PublicShareViewModel.swift | 7 ++----- kDrive/UI/View/Upsale/UpsaleViewController.swift | 8 ++++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index f03fe24a1..d9e6e8530 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -301,7 +301,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV let upsaleViewController = UpsaleViewController() // Create an account - upsaleViewController.freeTrialCallback = { [weak self] in + upsaleViewController.onFreeTrialCompleted = { [weak self] in guard let self else { return } self.dismiss(animated: true) { let loginDelegateHandler = LoginDelegateHandler() @@ -310,7 +310,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } // Let the user login with the onboarding - upsaleViewController.loginCallback = { [weak self] in + upsaleViewController.onLoginCompleted = { [weak self] in guard let self else { return } self.dismiss(animated: true) { let loginDelegateHandler = LoginDelegateHandler() diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 45cf23c11..cc634a2fa 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -88,11 +88,8 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { - guard downloadObserver == nil else { - return - } - - guard let publicShareProxy = publicShareProxy else { + guard downloadObserver == nil, + let publicShareProxy else { return } diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift index a6bf7ba8d..bcdeb799c 100644 --- a/kDrive/UI/View/Upsale/UpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -22,8 +22,8 @@ import kDriveResources import UIKit public class UpsaleViewController: UIViewController { - var loginCallback: (() -> Void)? - var freeTrialCallback: (() -> Void)? + var onLoginCompleted: (() -> Void)? + var onFreeTrialCompleted: (() -> Void)? let titleImageView = UIImageView() @@ -221,11 +221,11 @@ public class UpsaleViewController: UIViewController { @objc public func freeTrial() { dismiss(animated: true, completion: nil) - freeTrialCallback?() + onFreeTrialCompleted?() } @objc public func login() { dismiss(animated: true, completion: nil) - loginCallback?() + onLoginCompleted?() } } From 399acfbfd9031fea8db1de0158ed7b666f99d7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 6 Jan 2025 08:44:50 +0100 Subject: [PATCH 090/129] refactor(DownloadOperation): Public accessor for progress value --- .../UI/Controller/Files/Preview/PreviewViewController.swift | 6 +++--- .../DownloadQueue/DownloadOperation/DownloadOperation.swift | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index c910cfea1..1e228ff9e 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -556,7 +556,7 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } currentDownloadOperation = operation - if let progress = currentDownloadOperation?.task?.progress, + if let progress = currentDownloadOperation?.progress, let cell = collectionView.cellForItem(at: indexPath) as? DownloadProgressObserver { cell.setDownloadProgress(progress) } @@ -722,7 +722,7 @@ extension PreviewViewController: UICollectionViewDataSource { } 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, + let progress = downloadOperation.progress, downloadOperation.fileId == file.id { cell.setDownloadProgress(progress) } @@ -737,7 +737,7 @@ extension PreviewViewController: UICollectionViewDataSource { let cell = collectionView.dequeueReusableCell(type: NoPreviewCollectionViewCell.self, for: indexPath) cell.configureWith(file: file) if let downloadOperation = currentDownloadOperation, - let progress = downloadOperation.task?.progress, + let progress = downloadOperation.progress, downloadOperation.fileId == file.id { cell.setDownloadProgress(progress) } diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift index 907ab3e55..12e17760d 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -29,6 +29,10 @@ public class DownloadOperation: Operation, @unchecked Sendable { public var error: DriveError? + public var progress: Progress? { + task?.progress + } + init(task: URLSessionDownloadTask? = nil) { self.task = task } From d294a7948f8e9342233ee3eae8b874a93b532852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 7 Jan 2025 09:57:17 +0100 Subject: [PATCH 091/129] refactor(DownloadQueue): Use abstract type DownloadFileOperationable in operationsInQueue --- .../Data/DownloadQueue/DownloadQueue.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 9bda7356c..74a9c304a 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -75,7 +75,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { public static let instance = DownloadQueue() public static let backgroundIdentifier = "com.infomaniak.background.download" - public private(set) var operationsInQueue = SendableDictionary() + public private(set) var fileOperationsInQueue = SendableDictionary() public private(set) var archiveOperationsInQueue = SendableDictionary() private(set) lazy var operationQueue: OperationQueue = { let queue = OperationQueue() @@ -136,13 +136,13 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { ) operation.completionBlock = { self.dispatchQueue.async { - self.operationsInQueue.removeValue(forKey: file.id) + self.fileOperationsInQueue.removeValue(forKey: file.id) self.publishFileDownloaded(fileId: file.id, error: operation.error) - OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) } } self.operationQueue.addOperation(operation) - self.operationsInQueue[file.id] = operation + self.fileOperationsInQueue[file.id] = operation } } @@ -178,13 +178,13 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { ) operation.completionBlock = { self.dispatchQueue.async { - self.operationsInQueue.removeValue(forKey: file.id) + self.fileOperationsInQueue.removeValue(forKey: file.id) self.publishFileDownloaded(fileId: file.id, error: operation.error) - OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) } } self.operationQueue.addOperation(operation) - self.operationsInQueue[file.id] = operation + self.fileOperationsInQueue[file.id] = operation } } @@ -207,7 +207,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { self.dispatchQueue.async { self.archiveOperationsInQueue.removeValue(forKey: archiveId) self.publishArchiveDownloaded(archiveId: archiveId, archiveUrl: operation.archiveUrl, error: operation.error) - OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) } } @@ -236,7 +236,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { self.dispatchQueue.async { self.archiveOperationsInQueue.removeValue(forKey: archiveId) self.publishArchiveDownloaded(archiveId: archiveId, archiveUrl: operation.archiveUrl, error: operation.error) - OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) } } self.operationQueue.addOperation(operation) @@ -266,13 +266,13 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { let operation = DownloadAuthenticatedOperation(file: file, driveFileManager: driveFileManager, urlSession: self.foregroundSession) operation.completionBlock = { self.dispatchQueue.async { - self.operationsInQueue.removeValue(forKey: fileId) - OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty) + self.fileOperationsInQueue.removeValue(forKey: fileId) + OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) completion(operation.error) } } operation.start() - self.operationsInQueue[file.id] = operation + self.fileOperationsInQueue[file.id] = operation onOperationCreated?(operation) } } @@ -296,8 +296,8 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { /// /// Thread safe /// Lookup O(1) as Dictionary backed - public func operation(for fileId: Int) -> DownloadAuthenticatedOperation? { - return operationsInQueue[fileId] + public func operation(for fileId: Int) -> DownloadFileOperationable? { + return fileOperationsInQueue[fileId] } public func hasOperation(for fileId: Int) -> Bool { From da394c919e59e45b842674353f5c39de214b34b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 7 Jan 2025 11:41:42 +0100 Subject: [PATCH 092/129] chore: Sonar Feedback --- kDrive/UI/View/Files/FileCollectionViewCell.swift | 2 +- kDriveCore/Data/Cache/AccountManager.swift | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 5f55ada8e..7df7ac8a7 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -364,7 +364,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { func configureForSelection() { guard let viewModel, - viewModel.selectionMode == true else { + viewModel.selectionMode else { return } diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 9dee5130e..fdb96b513 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -214,7 +214,6 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { // FileViewModel K.O. without a valid drive in Realm, therefore add one let publicShareDrive = Drive() publicShareDrive.objectId = publicShareId - @LazyInjectService var driveInfosManager: DriveInfosManager do { try driveInfosManager.storePublicShareDrive(drive: publicShareDrive) } catch { From 09d410272d41853ba0407b63b9b6d7f13f91cc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 7 Jan 2025 16:27:09 +0100 Subject: [PATCH 093/129] test(MCKRouter): Adding missing mocked signatures --- kDriveTestShared/MCKRouter.swift | 17 +++++++++++++++++ .../kDrive/Launch/MockAccountManager.swift | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/kDriveTestShared/MCKRouter.swift b/kDriveTestShared/MCKRouter.swift index 1d24fda4f..318a61af0 100644 --- a/kDriveTestShared/MCKRouter.swift +++ b/kDriveTestShared/MCKRouter.swift @@ -148,4 +148,21 @@ public final class MCKRouter: AppNavigable { public func showSaveFileVC(from viewController: UIViewController, driveFileManager: DriveFileManager, files: [ImportedFile]) { logNoop() } + + @MainActor public func presentPublicShareLocked(_ destinationURL: URL) { + logNoop() + } + + @MainActor public func presentPublicShareExpired() { + logNoop() + } + + @MainActor public func presentPublicShare( + frozenRootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) { + logNoop() + } } diff --git a/kDriveTests/kDrive/Launch/MockAccountManager.swift b/kDriveTests/kDrive/Launch/MockAccountManager.swift index ca679cbf0..f2e78a8cd 100644 --- a/kDriveTests/kDrive/Launch/MockAccountManager.swift +++ b/kDriveTests/kDrive/Launch/MockAccountManager.swift @@ -92,4 +92,8 @@ class MockAccountManager: AccountManageable, RefreshTokenDelegate { func updateToken(newToken: ApiToken, oldToken: ApiToken) {} func logoutCurrentAccountAndSwitchToNextIfPossible() { fatalError("Not implemented") } + + func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager { + fatalError("Not implemented") + } } From 4d4dd6aa8951d68a5c85ecfb2378a16b23e68617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 7 Jan 2025 17:03:15 +0100 Subject: [PATCH 094/129] chore: Self review changes --- .../File List/FileListViewController.swift | 2 -- ...eViewController+FooterButtonDelegate.swift | 31 ++++++++++--------- .../Save File/SaveFileViewController.swift | 2 +- .../Menu/Share/PublicShareViewModel.swift | 2 +- .../Upsale/NoDriveUpsaleViewController.swift | 4 +-- 5 files changed, 20 insertions(+), 21 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index 279f9fd85..2b9f8996f 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -298,7 +298,6 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV #if !ISEXTENSION let upsaleViewController = UpsaleViewController() - // Create an account upsaleViewController.onFreeTrialCompleted = { [weak self] in guard let self else { return } self.dismiss(animated: true) { @@ -307,7 +306,6 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } - // Let the user login with the onboarding upsaleViewController.onLoginCompleted = { [weak self] in guard let self else { return } self.dismiss(animated: true) { diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index 1b5ee694d..1c35cda8b 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -35,26 +35,27 @@ extension SaveFileViewController: FooterButtonDelegate { let button = sender as? IKLargeButton button?.setLoading(true) - if let publicShareProxy { - Task { - defer { dismiss() } - try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, - destinationDriveId: drive.id, - destinationFolderId: directory.id, - fileIds: publicShareFileIds, - exceptIds: publicShareExceptIds, - sharelinkUuid: publicShareProxy.shareLinkUid, - driveFileManager: selectedDriveFileManager) - } - } else { + guard let publicShareProxy else { guard !items.isEmpty else { - dismiss() + dismissViewController() return } Task { await saveAndDismiss(files: items, directory: directory, drive: drive) } + return + } + + Task { + defer { dismissViewController() } + try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, + destinationDriveId: drive.id, + destinationFolderId: directory.id, + fileIds: publicShareFileIds, + exceptIds: publicShareExceptIds, + sharelinkUuid: publicShareProxy.shareLinkUid, + driveFileManager: selectedDriveFileManager) } } @@ -73,8 +74,8 @@ extension SaveFileViewController: FooterButtonDelegate { sharelinkUuid: sharelinkUuid) } - private func dismiss() { - completionClosure?() + private func dismissViewController() { + onDismissViewController?() dismiss(animated: true) } diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 75bd546f7..ab78b8011 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -117,7 +117,7 @@ class SaveFileViewController: UIViewController { } } - @MainActor var completionClosure: (() -> Void)? + @MainActor var onDismissViewController: (() -> Void)? @IBOutlet var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index d30ff8ea2..3fda3016e 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -157,7 +157,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { let saveNavigationViewController = SaveFileViewController .setInNavigationController(saveViewController: saveViewController) - saveViewController.completionClosure = { [weak self] in + saveViewController.onDismissViewController = { [weak self] in guard let self else { return } self.onDismissViewController?() } diff --git a/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift index 34f4d9f2c..25fa2d028 100644 --- a/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift @@ -22,7 +22,7 @@ import kDriveResources import UIKit public class NoDriveUpsaleViewController: UpsaleViewController { - var dismissCallback: (() -> Void)? + var onDismissViewController: (() -> Void)? override func configureButtons() { dismissButton.style = .primaryButton @@ -41,6 +41,6 @@ public class NoDriveUpsaleViewController: UpsaleViewController { @objc public func dismissViewController() { dismiss(animated: true, completion: nil) - dismissCallback?() + onDismissViewController?() } } From d4b67a5c8f49d079939bb7c1edc246bbfb89772a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 7 Jan 2025 18:16:01 +0100 Subject: [PATCH 095/129] fix: Close multiSelection since the merge --- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 3fda3016e..9d9bc9ecf 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -94,6 +94,8 @@ final class PublicShareViewModel: InMemoryFileListViewModel { addToMyDrive(sender: sender, publicShareProxy: publicShareProxy) } else if type == .cancel, !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true) { onDismissViewController?() + } else { + super.barButtonPressed(sender: sender, type: type) } } From 7907779c1d4de663665997ec999fe68a7d4649d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 11:03:58 +0100 Subject: [PATCH 096/129] feat: Keep addToMyDrive button enabled while multiselect --- .../UI/Controller/Files/File List/FileListViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index b2e5a9cea..cf22337ad 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -638,7 +638,6 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV func toggleMultipleSelection(_ on: Bool) { if on { - addToKDriveButton.isHidden = true navigationItem.title = nil headerView?.selectView.isHidden = false headerView?.selectView.setActions(viewModel.multipleSelectionViewModel?.multipleSelectionActions ?? []) @@ -648,7 +647,6 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV generator.prepare() generator.impactOccurred() } else { - addToKDriveButton.isHidden = false headerView?.selectView.isHidden = true collectionView.allowsMultipleSelection = false navigationController?.navigationBar.prefersLargeTitles = true From dd7c93c17729ef02fd31c23b950bbeee91a7fe96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 11:23:47 +0100 Subject: [PATCH 097/129] chore: Matomo event naming --- kDriveCore/Utils/MatomoUtils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index 1d0c46478..bf3406aae 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -136,7 +136,7 @@ public enum MatomoUtils { // MARK: - Public Share - public static func trackAddToMykDrive() { + public static func trackAddToMyDrive() { track(eventWithCategory: .publicShareAction, name: "saveToKDrive") } From 10332da3a8eaea8c90b453ade452829e86c73a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 11:24:17 +0100 Subject: [PATCH 098/129] chore(UpsaleViewController): Removed comments --- kDrive/UI/View/Upsale/UpsaleViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift index e405863e8..f45c4e8e3 100644 --- a/kDrive/UI/View/Upsale/UpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -233,7 +233,6 @@ public class UpsaleViewController: UIViewController { public static func instantiateInFloatingPanel(rootViewController: UIViewController) -> UIViewController { let upsaleViewController = UpsaleViewController() - // Create an account upsaleViewController.onFreeTrialCompleted = { [weak rootViewController] in guard let rootViewController else { return } rootViewController.dismiss(animated: true) { @@ -243,7 +242,6 @@ public class UpsaleViewController: UIViewController { } } - // Let the user login with the onboarding upsaleViewController.onLoginCompleted = { [weak rootViewController] in guard let rootViewController else { return } rootViewController.dismiss(animated: true) { From fe4a329594ab292d27e0b315f126f4839e5e62a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 11:42:07 +0100 Subject: [PATCH 099/129] refactor(PublicShareAction): Factorised addToMyDrive code --- ...sFloatingPanelViewController+Actions.swift | 32 ++++++------- .../Controller/Files/PublicShareAction.swift | 47 +++++++++++++++++++ .../Menu/Share/PublicShareViewModel.swift | 38 ++++++++------- 3 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 kDrive/UI/Controller/Files/PublicShareAction.swift diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index eb81d480b..7d2d34bc0 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -166,7 +166,7 @@ extension FileActionsFloatingPanelViewController { case .cancelImport: cancelImportAction() case .addToMyDrive: - addToMyKDrive() + addToMyDrive() default: break } @@ -523,8 +523,9 @@ extension FileActionsFloatingPanelViewController { } } - private func addToMyKDrive() { + private func addToMyDrive() { guard accountManager.currentAccount != nil else { + // TODO: Router let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) present(upsaleFloatingPanelController, animated: true, completion: nil) return @@ -535,19 +536,18 @@ extension FileActionsFloatingPanelViewController { return } - let itemId = [file.id] - let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) - let saveNavigationViewController = SaveFileViewController - .setInNavigationController(saveViewController: saveViewController) - - saveViewController.onDismissViewController = { [weak self] in - guard let self else { return } - self.dismiss(animated: true) - } - - saveViewController.publicShareFileIds = itemId - saveViewController.publicShareProxy = publicShareProxy - - present(saveNavigationViewController, animated: true, completion: nil) + PublicShareAction().addToMyDrive( + publicShareProxy: publicShareProxy, + currentUserDriveFileManager: currentUserDriveFileManager, + selectedItemsIds: [file.id], + exceptItemIds: [], + onPresentViewController: { saveNavigationViewController, animated in + self.present(saveNavigationViewController, animated: animated, completion: nil) + }, + onDismissViewController: { [weak self] in + guard let self else { return } + self.dismiss(animated: true) + } + ) } } diff --git a/kDrive/UI/Controller/Files/PublicShareAction.swift b/kDrive/UI/Controller/Files/PublicShareAction.swift new file mode 100644 index 000000000..97baa18fe --- /dev/null +++ b/kDrive/UI/Controller/Files/PublicShareAction.swift @@ -0,0 +1,47 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2024 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 kDriveCore +import UIKit + +struct PublicShareAction { + @MainActor func addToMyDrive( + publicShareProxy: PublicShareProxy, + currentUserDriveFileManager: DriveFileManager, + selectedItemsIds: [Int], + exceptItemIds: [Int], + onPresentViewController: (UIViewController, Bool) -> Void, + onDismissViewController: (() -> Void)? + ) { + let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) + let saveNavigationViewController = SaveFileViewController + .setInNavigationController(saveViewController: saveViewController) + + saveViewController.onDismissViewController = { + onDismissViewController?() + } + + if let saveViewController = saveNavigationViewController.viewControllers.first as? SaveFileViewController { + saveViewController.publicShareFileIds = selectedItemsIds + saveViewController.publicShareExceptIds = exceptItemIds + saveViewController.publicShareProxy = publicShareProxy + } + + onPresentViewController(saveNavigationViewController, true) + } +} diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 9d9bc9ecf..d97e806fb 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -148,29 +148,33 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { + guard accountManager.currentAccount != nil else { + // TODO: Router +// let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) +// onPresentViewController?(.modal, upsaleFloatingPanelController, true) + return + } + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager else { return } + // TODO: Check logic let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] - let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) - let saveNavigationViewController = SaveFileViewController - .setInNavigationController(saveViewController: saveViewController) - - saveViewController.onDismissViewController = { [weak self] in - guard let self else { return } - self.onDismissViewController?() - } - - if let saveViewController = saveNavigationViewController.viewControllers.first as? SaveFileViewController { - saveViewController.publicShareFileIds = selectedItemsIds - saveViewController.publicShareExceptIds = exceptItemIds - saveViewController.publicShareProxy = publicShareProxy - saveViewController.selectedDirectory = currentDirectory - } - - onPresentViewController?(.modal, saveNavigationViewController, true) + PublicShareAction().addToMyDrive( + publicShareProxy: publicShareProxy, + currentUserDriveFileManager: currentUserDriveFileManager, + selectedItemsIds: selectedItemsIds, + exceptItemIds: exceptItemIds, + onPresentViewController: { saveNavigationViewController, animated in + onPresentViewController?(.modal, saveNavigationViewController, animated) + }, + onDismissViewController: { [weak self] in + guard let self else { return } + self.onDismissViewController?() + } + ) } } From 6aa134226971ce23c35162cda7efce78d9b31487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 13:06:16 +0100 Subject: [PATCH 100/129] feat(Router): New showUpsaleFloatingPanel method --- kDrive/AppRouter.swift | 15 +++++++++++++++ ...tionsFloatingPanelViewController+Actions.swift | 4 +--- .../Menu/Share/PublicShareViewModel.swift | 5 ++--- kDriveCore/Utils/AppNavigable.swift | 2 ++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 89f1c0c96..3a84e75b1 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -414,6 +414,21 @@ public struct AppRouter: AppNavigable { } } + @MainActor public func showUpsaleFloatingPanel() { + guard let window else { + SentryDebug.captureNoWindow() + return + } + + guard let rootViewController = window.rootViewController else { + return + } + + let upsaleFloatingPanelController = UpsaleViewController + .instantiateInFloatingPanel(rootViewController: rootViewController) + rootViewController.present(upsaleFloatingPanelController, animated: true) + } + @MainActor public func showUpdateRequired() { guard let window else { SentryDebug.captureNoWindow() diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index 7d2d34bc0..754f257b4 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -525,9 +525,7 @@ extension FileActionsFloatingPanelViewController { private func addToMyDrive() { guard accountManager.currentAccount != nil else { - // TODO: Router - let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) - present(upsaleFloatingPanelController, animated: true, completion: nil) + router.showUpsaleFloatingPanel() return } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index d97e806fb..174ad4a94 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -25,6 +25,7 @@ import UIKit /// Public share view model, loading content from memory realm final class PublicShareViewModel: InMemoryFileListViewModel { @LazyInjectService private var accountManager: AccountManageable + @LazyInjectService private var router: AppNavigable private var downloadObserver: ObservationToken? @@ -149,9 +150,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { guard accountManager.currentAccount != nil else { - // TODO: Router -// let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) -// onPresentViewController?(.modal, upsaleFloatingPanelController, true) + router.showUpsaleFloatingPanel() return } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index a0e029a52..a5f9c1e60 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -37,6 +37,8 @@ public protocol RouterAppNavigable { @MainActor func showLaunchFloatingPanel() + @MainActor func showUpsaleFloatingPanel() + @MainActor func showUpdateRequired() @MainActor func showPhotoSyncSettings() From 46a882f3693748dbfe2b2f9bd2fc206fbbe0024a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 15:28:34 +0100 Subject: [PATCH 101/129] fix(addToMyDrive): Modified and checked new logic against every use case I had --- .../SaveFileViewController+FooterButtonDelegate.swift | 2 -- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index 1c35cda8b..5f9246b87 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -30,8 +30,6 @@ extension SaveFileViewController: FooterButtonDelegate { return } let drive = selectedDriveFileManager.drive - - // Making sure the user cannot spam the button on tasks that may take a while let button = sender as? IKLargeButton button?.setLoading(true) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 174ad4a94..c659b7f58 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -158,10 +158,13 @@ final class PublicShareViewModel: InMemoryFileListViewModel { return } - // TODO: Check logic - let selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] + [rootProxy.id] + var selectedItemsIds = multipleSelectionViewModel?.selectedItems.map(\.id) ?? [] let exceptItemIds = multipleSelectionViewModel?.exceptItemIds.map { $0 } ?? [] + if publicShareProxy.fileId != rootProxy.id, selectedItemsIds.isEmpty { + selectedItemsIds += [rootProxy.id] + } + PublicShareAction().addToMyDrive( publicShareProxy: publicShareProxy, currentUserDriveFileManager: currentUserDriveFileManager, From 60bba8f923457c9ae7fda07e5fdbf15df3d29198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 15:36:23 +0100 Subject: [PATCH 102/129] feat(PublicShareViewModel): Disable multi selection on addToMyDrive action --- kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index c659b7f58..136a358d4 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -176,6 +176,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { onDismissViewController: { [weak self] in guard let self else { return } self.onDismissViewController?() + self.multipleSelectionViewModel?.isMultipleSelectionEnabled = false } ) } From 3cbf6b32a5fbd403eb0a91531f46f303454a5e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 8 Jan 2025 15:45:27 +0100 Subject: [PATCH 103/129] chore(UT): Fix protocol changes in mocks --- kDriveTestShared/MCKRouter.swift | 13 +++++++++++++ kDriveTests/kDrive/Launch/MockAccountManager.swift | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/kDriveTestShared/MCKRouter.swift b/kDriveTestShared/MCKRouter.swift index 318a61af0..5fcdb1be0 100644 --- a/kDriveTestShared/MCKRouter.swift +++ b/kDriveTestShared/MCKRouter.swift @@ -18,6 +18,7 @@ import Foundation import InfomaniakCore +import InfomaniakLogin import kDrive import kDriveCore import UIKit @@ -74,6 +75,10 @@ public final class MCKRouter: AppNavigable { logNoop() } + public func showUpsaleFloatingPanel() { + logNoop() + } + public func showUpdateRequired() { logNoop() } @@ -82,6 +87,14 @@ public final class MCKRouter: AppNavigable { logNoop() } + public func showLogin(delegate: InfomaniakLoginDelegate) { + logNoop() + } + + public func showRegister(delegate: InfomaniakLoginDelegate) { + logNoop() + } + public func present(file: kDriveCore.File, driveFileManager: kDriveCore.DriveFileManager) { logNoop() } diff --git a/kDriveTests/kDrive/Launch/MockAccountManager.swift b/kDriveTests/kDrive/Launch/MockAccountManager.swift index f2e78a8cd..e475dd97f 100644 --- a/kDriveTests/kDrive/Launch/MockAccountManager.swift +++ b/kDriveTests/kDrive/Launch/MockAccountManager.swift @@ -93,7 +93,7 @@ class MockAccountManager: AccountManageable, RefreshTokenDelegate { func logoutCurrentAccountAndSwitchToNextIfPossible() { fatalError("Not implemented") } - func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager { + func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager? { fatalError("Not implemented") } } From 74eb23222c776a87260525ca5af4fe9c43c56eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 08:54:56 +0100 Subject: [PATCH 104/129] feat: Loading ui on public share download all --- .../Controller/Files/File List/FileListViewModel.swift | 9 +++++++-- .../UI/Controller/Menu/Share/PublicShareViewModel.swift | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 20f59ea2a..61f73addb 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -164,8 +164,6 @@ class FileListViewModel: SelectDelegate { listStyle = FileListOptions.instance.currentStyle isRefreshing = false isLoading = false - currentLeftBarButtons = configuration.leftBarButtons - currentRightBarButtons = configuration.rightBarButtons if self.currentDirectory.isRoot { if let rootTitle = configuration.rootTitle { @@ -199,6 +197,13 @@ class FileListViewModel: SelectDelegate { currentDirectory: self.currentDirectory ) } + + loadButtonsConfiguration() + } + + func loadButtonsConfiguration() { + currentLeftBarButtons = configuration.leftBarButtons + currentRightBarButtons = configuration.rightBarButtons } func updateRealmObservation() { diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 136a358d4..03e2cc3e0 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -103,12 +103,16 @@ final class PublicShareViewModel: InMemoryFileListViewModel { private func downloadAll(sender: Any?, publicShareProxy: PublicShareProxy) { let button = sender as? UIButton button?.isEnabled = false + configuration.rightBarButtons = [.loading] + loadButtonsConfiguration() downloadObserver = DownloadQueue.instance .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in Task { @MainActor in defer { button?.isEnabled = true + self?.configuration.rightBarButtons = [.downloadAll] + self?.loadButtonsConfiguration() } guard let self = self else { @@ -165,12 +169,14 @@ final class PublicShareViewModel: InMemoryFileListViewModel { selectedItemsIds += [rootProxy.id] } + configuration.rightBarButtons = [.loading] PublicShareAction().addToMyDrive( publicShareProxy: publicShareProxy, currentUserDriveFileManager: currentUserDriveFileManager, selectedItemsIds: selectedItemsIds, exceptItemIds: exceptItemIds, onPresentViewController: { saveNavigationViewController, animated in + self.configuration.rightBarButtons = [.downloadAll] onPresentViewController?(.modal, saveNavigationViewController, animated) }, onDismissViewController: { [weak self] in From 65db643ce118e15f0c26e1ccae3e4034035433a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 09:58:24 +0100 Subject: [PATCH 105/129] refactor(SaveFileViewController): Rework init method --- .../Controller/Files/PublicShareAction.swift | 20 ++++------- .../Save File/SaveFileViewController.swift | 34 +++++++++++++------ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/kDrive/UI/Controller/Files/PublicShareAction.swift b/kDrive/UI/Controller/Files/PublicShareAction.swift index 97baa18fe..765c6a3d6 100644 --- a/kDrive/UI/Controller/Files/PublicShareAction.swift +++ b/kDrive/UI/Controller/Files/PublicShareAction.swift @@ -28,19 +28,13 @@ struct PublicShareAction { onPresentViewController: (UIViewController, Bool) -> Void, onDismissViewController: (() -> Void)? ) { - let saveViewController = SaveFileViewController.instantiate(driveFileManager: currentUserDriveFileManager) - let saveNavigationViewController = SaveFileViewController - .setInNavigationController(saveViewController: saveViewController) - - saveViewController.onDismissViewController = { - onDismissViewController?() - } - - if let saveViewController = saveNavigationViewController.viewControllers.first as? SaveFileViewController { - saveViewController.publicShareFileIds = selectedItemsIds - saveViewController.publicShareExceptIds = exceptItemIds - saveViewController.publicShareProxy = publicShareProxy - } + let saveNavigationViewController = SaveFileViewController.instantiateInNavigationController( + driveFileManager: currentUserDriveFileManager, + publicShareProxy: publicShareProxy, + publicShareFileIds: selectedItemsIds, + publicShareExceptIds: exceptItemIds, + onDismissViewController: onDismissViewController + ) onPresentViewController(saveNavigationViewController, true) } diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index ab78b8011..1c98438a1 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -350,20 +350,34 @@ class SaveFileViewController: UIViewController { return viewController } - class func setInNavigationController(saveViewController: SaveFileViewController, - files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { - if let files { - saveViewController.items = files - } - let navigationController = TitleSizeAdjustingNavigationController(rootViewController: saveViewController) - navigationController.navigationBar.prefersLargeTitles = true - return navigationController + class func instantiateInNavigationController(driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + publicShareFileIds: [Int], + publicShareExceptIds: [Int], + onDismissViewController: (() -> Void)?) + -> TitleSizeAdjustingNavigationController { + let saveViewController = instantiate(driveFileManager: driveFileManager) + + saveViewController.publicShareFileIds = publicShareFileIds + saveViewController.publicShareExceptIds = publicShareExceptIds + saveViewController.publicShareProxy = publicShareProxy + + return wrapInNavigationController(saveViewController) } class func instantiateInNavigationController(driveFileManager: DriveFileManager?, files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { let saveViewController = instantiate(driveFileManager: driveFileManager) - return setInNavigationController(saveViewController: saveViewController, - files: files) + if let files { + saveViewController.items = files + } + + return wrapInNavigationController(saveViewController) + } + + private class func wrapInNavigationController(_ viewController: UIViewController) -> TitleSizeAdjustingNavigationController { + let navigationController = TitleSizeAdjustingNavigationController(rootViewController: viewController) + navigationController.navigationBar.prefersLargeTitles = true + return navigationController } } From e73173ec5ab8ceb5f8bb90c8a0e31b45bc5f4e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 12:11:54 +0100 Subject: [PATCH 106/129] feat: Cancel a download all action from UITabBarItem --- .../Files/File List/FileListViewModel.swift | 1 + .../Menu/Share/PublicShareViewModel.swift | 24 ++++++++++----- kDrive/UI/View/Files/FileListBarButton.swift | 21 +++++++++++++ .../Data/DownloadQueue/DownloadQueue.swift | 30 +++++++++++++++---- 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 61f73addb..d89a9d9e3 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -35,6 +35,7 @@ enum FileListBarButtonType { case photoSort case addFolder case downloadAll + case downloadingAll case addToMyDrive } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 03e2cc3e0..9827dcb3d 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -84,13 +84,14 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { - guard downloadObserver == nil, - let publicShareProxy else { + guard let publicShareProxy else { return } if type == .downloadAll { downloadAll(sender: sender, publicShareProxy: publicShareProxy) + } else if type == .downloadingAll { + cancelDownloadAll() } else if type == .addToMyDrive { addToMyDrive(sender: sender, publicShareProxy: publicShareProxy) } else if type == .cancel, !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true) { @@ -100,10 +101,17 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } } + private func cancelDownloadAll() { + DownloadQueue.instance.cancelFileOperation(for: currentDirectory.id) + clearDownloadObserver() + configuration.rightBarButtons = [.downloadAll] + loadButtonsConfiguration() + } + private func downloadAll(sender: Any?, publicShareProxy: PublicShareProxy) { let button = sender as? UIButton button?.isEnabled = false - configuration.rightBarButtons = [.loading] + configuration.rightBarButtons = [.downloadingAll] loadButtonsConfiguration() downloadObserver = DownloadQueue.instance @@ -120,8 +128,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } defer { - self.downloadObserver?.cancel() - self.downloadObserver = nil + self.clearDownloadObserver() } guard error == nil else { @@ -152,6 +159,11 @@ final class PublicShareViewModel: InMemoryFileListViewModel { publicShareProxy: publicShareProxy) } + private func clearDownloadObserver() { + downloadObserver?.cancel() + downloadObserver = nil + } + private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { guard accountManager.currentAccount != nil else { router.showUpsaleFloatingPanel() @@ -169,14 +181,12 @@ final class PublicShareViewModel: InMemoryFileListViewModel { selectedItemsIds += [rootProxy.id] } - configuration.rightBarButtons = [.loading] PublicShareAction().addToMyDrive( publicShareProxy: publicShareProxy, currentUserDriveFileManager: currentUserDriveFileManager, selectedItemsIds: selectedItemsIds, exceptItemIds: exceptItemIds, onPresentViewController: { saveNavigationViewController, animated in - self.configuration.rightBarButtons = [.downloadAll] onPresentViewController?(.modal, saveNavigationViewController, animated) }, onDismissViewController: { [weak self] in diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index 7ecd84c35..aa46e1a1e 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -23,6 +23,10 @@ import UIKit final class FileListBarButton: UIBarButtonItem { private(set) var type: FileListBarButtonType = .cancel + private var tapGestureRecognizer: UITapGestureRecognizer? + private var bridgeTarget: NSObject? + private var bridgeAction: Selector? + convenience init(type: FileListBarButtonType, target: Any?, action: Selector?) { switch type { case .selectAll: @@ -53,6 +57,18 @@ final class FileListBarButton: UIBarButtonItem { let image = KDriveResourcesAsset.download.image self.init(image: image, style: .plain, target: target, action: action) accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload + case .downloadingAll: + let activityView = UIActivityIndicatorView(style: .medium) + activityView.startAnimating() + + self.init(customView: activityView) + + bridgeTarget = target as? NSObject + bridgeAction = action + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer)) + activityView.addGestureRecognizer(tapGestureRecognizer) + self.tapGestureRecognizer = tapGestureRecognizer case .addToMyDrive: let image = KDriveResourcesAsset.drive.image self.init(image: image, style: .plain, target: target, action: action) @@ -60,4 +76,9 @@ final class FileListBarButton: UIBarButtonItem { } self.type = type } + + @objc private func handleTapGestureRecognizer() { + guard let bridgeTarget, let bridgeAction else { return } + bridgeTarget.perform(bridgeAction, with: self) + } } diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 74a9c304a..9e6179eb8 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -263,7 +263,11 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadAuthenticatedOperation(file: file, driveFileManager: driveFileManager, urlSession: self.foregroundSession) + let operation = DownloadAuthenticatedOperation( + file: file, + driveFileManager: driveFileManager, + urlSession: self.foregroundSession + ) operation.completionBlock = { self.dispatchQueue.async { self.fileOperationsInQueue.removeValue(forKey: fileId) @@ -292,14 +296,30 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { operationQueue.cancelAllOperations() } - /// Check if a file is been uploaded - /// - /// Thread safe - /// Lookup O(1) as Dictionary backed + public func cancelArchiveOperation(for archiveId: String) { + guard let operation = archiveOperation(for: archiveId) else { + return + } + operation.cancel() + archiveOperationsInQueue.removeValue(forKey: archiveId) + } + + public func cancelFileOperation(for fileId: Int) { + guard let operation = operation(for: fileId) else { + return + } + operation.cancel() + fileOperationsInQueue.removeValue(forKey: fileId) + } + public func operation(for fileId: Int) -> DownloadFileOperationable? { return fileOperationsInQueue[fileId] } + public func archiveOperation(for archiveId: String) -> DownloadArchiveOperation? { + return archiveOperationsInQueue[archiveId] + } + public func hasOperation(for fileId: Int) -> Bool { return operation(for: fileId) != nil } From d3a97759f842eaa8bc2f85d6a23a8d4a3acc8578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 14:18:35 +0100 Subject: [PATCH 107/129] refactor: Simpler UITapGestureRecognizer passthrew --- kDrive/UI/View/Files/FileListBarButton.swift | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index aa46e1a1e..e9be8dfa2 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -23,10 +23,6 @@ import UIKit final class FileListBarButton: UIBarButtonItem { private(set) var type: FileListBarButtonType = .cancel - private var tapGestureRecognizer: UITapGestureRecognizer? - private var bridgeTarget: NSObject? - private var bridgeAction: Selector? - convenience init(type: FileListBarButtonType, target: Any?, action: Selector?) { switch type { case .selectAll: @@ -58,17 +54,15 @@ final class FileListBarButton: UIBarButtonItem { self.init(image: image, style: .plain, target: target, action: action) accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload case .downloadingAll: + self.init(title: nil, style: .plain, target: target, action: action) + let activityView = UIActivityIndicatorView(style: .medium) activityView.startAnimating() - self.init(customView: activityView) - - bridgeTarget = target as? NSObject - bridgeAction = action - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer)) activityView.addGestureRecognizer(tapGestureRecognizer) - self.tapGestureRecognizer = tapGestureRecognizer + + customView = activityView case .addToMyDrive: let image = KDriveResourcesAsset.drive.image self.init(image: image, style: .plain, target: target, action: action) @@ -78,7 +72,7 @@ final class FileListBarButton: UIBarButtonItem { } @objc private func handleTapGestureRecognizer() { - guard let bridgeTarget, let bridgeAction else { return } - bridgeTarget.perform(bridgeAction, with: self) + guard let targetObject = target as? NSObject, let action else { return } + targetObject.perform(action, with: self) } } From 4ff6630f8a968272c1a81b2717fe3a8b3b9fe122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 14:27:10 +0100 Subject: [PATCH 108/129] chore: Perhaps a more consensual naming --- kDrive/UI/View/Files/FileListBarButton.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index e9be8dfa2..e45b62566 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -59,7 +59,7 @@ final class FileListBarButton: UIBarButtonItem { let activityView = UIActivityIndicatorView(style: .medium) activityView.startAnimating() - let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer)) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(cancelDownloadPressed)) activityView.addGestureRecognizer(tapGestureRecognizer) customView = activityView @@ -71,7 +71,7 @@ final class FileListBarButton: UIBarButtonItem { self.type = type } - @objc private func handleTapGestureRecognizer() { + @objc private func cancelDownloadPressed() { guard let targetObject = target as? NSObject, let action else { return } targetObject.perform(action, with: self) } From ead2fca123d7f7b7f8522a0dc3f373ab647874ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 16:55:24 +0100 Subject: [PATCH 109/129] chore(Matomo): Matomo on addToMyDrive --- .../FileActionsFloatingPanelViewController+Actions.swift | 3 +++ kDrive/UI/Controller/Files/PublicShareAction.swift | 2 ++ .../SaveFileViewController+FooterButtonDelegate.swift | 6 +++++- .../Controller/Files/Save File/SaveFileViewController.swift | 4 ++++ kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift | 3 +++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index 754f257b4..cf704bb50 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -542,6 +542,9 @@ extension FileActionsFloatingPanelViewController { onPresentViewController: { saveNavigationViewController, animated in self.present(saveNavigationViewController, animated: animated, completion: nil) }, + onSave: { + MatomoUtils.trackAddToMyDrive() + }, onDismissViewController: { [weak self] in guard let self else { return } self.dismiss(animated: true) diff --git a/kDrive/UI/Controller/Files/PublicShareAction.swift b/kDrive/UI/Controller/Files/PublicShareAction.swift index 765c6a3d6..2abc49ebf 100644 --- a/kDrive/UI/Controller/Files/PublicShareAction.swift +++ b/kDrive/UI/Controller/Files/PublicShareAction.swift @@ -26,6 +26,7 @@ struct PublicShareAction { selectedItemsIds: [Int], exceptItemIds: [Int], onPresentViewController: (UIViewController, Bool) -> Void, + onSave: (() -> Void)?, onDismissViewController: (() -> Void)? ) { let saveNavigationViewController = SaveFileViewController.instantiateInNavigationController( @@ -33,6 +34,7 @@ struct PublicShareAction { publicShareProxy: publicShareProxy, publicShareFileIds: selectedItemsIds, publicShareExceptIds: exceptItemIds, + onSave: onSave, onDismissViewController: onDismissViewController ) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift index 5f9246b87..47f458c6f 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -46,7 +46,11 @@ extension SaveFileViewController: FooterButtonDelegate { } Task { - defer { dismissViewController() } + defer { + onSave?() + dismissViewController() + } + try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, destinationDriveId: drive.id, destinationFolderId: directory.id, diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 1c98438a1..fdea7d32f 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -118,6 +118,7 @@ class SaveFileViewController: UIViewController { } @MainActor var onDismissViewController: (() -> Void)? + @MainActor var onSave: (() -> Void)? @IBOutlet var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! @@ -354,6 +355,7 @@ class SaveFileViewController: UIViewController { publicShareProxy: PublicShareProxy, publicShareFileIds: [Int], publicShareExceptIds: [Int], + onSave: (() -> Void)?, onDismissViewController: (() -> Void)?) -> TitleSizeAdjustingNavigationController { let saveViewController = instantiate(driveFileManager: driveFileManager) @@ -361,6 +363,8 @@ class SaveFileViewController: UIViewController { saveViewController.publicShareFileIds = publicShareFileIds saveViewController.publicShareExceptIds = publicShareExceptIds saveViewController.publicShareProxy = publicShareProxy + saveViewController.onSave = onSave + saveViewController.onDismissViewController = onDismissViewController return wrapInNavigationController(saveViewController) } diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 136a358d4..bbe51f969 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -173,6 +173,9 @@ final class PublicShareViewModel: InMemoryFileListViewModel { onPresentViewController: { saveNavigationViewController, animated in onPresentViewController?(.modal, saveNavigationViewController, animated) }, + onSave: { + MatomoUtils.trackAddBulkToMykDrive() + }, onDismissViewController: { [weak self] in guard let self else { return } self.onDismissViewController?() From aece1986457e8907f8dd650fdae53171d34d819b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 9 Jan 2025 17:28:44 +0100 Subject: [PATCH 110/129] chore: Matomo event on upsale sheet presentation --- kDrive/UI/View/Upsale/UpsaleViewController.swift | 2 ++ kDriveCore/Utils/MatomoUtils.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/kDrive/UI/View/Upsale/UpsaleViewController.swift b/kDrive/UI/View/Upsale/UpsaleViewController.swift index f45c4e8e3..cc3e5458a 100644 --- a/kDrive/UI/View/Upsale/UpsaleViewController.swift +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -65,6 +65,8 @@ public class UpsaleViewController: UIViewController { configureHeader() setupBody() layoutStackView() + + MatomoUtils.trackUpsalePresented() } func configureHeader() { diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index bf3406aae..44d89ad06 100644 --- a/kDriveCore/Utils/MatomoUtils.swift +++ b/kDriveCore/Utils/MatomoUtils.swift @@ -147,4 +147,8 @@ public enum MatomoUtils { public static func trackPublicSharePasswordAction() { track(eventWithCategory: .publicSharePasswordAction, name: "openInBrowser") } + + public static func trackUpsalePresented() { + track(eventWithCategory: .publicShareAction, name: "adBottomSheet") + } } From c98d71a12223200a9a3e33de190d46e70b565da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 09:32:47 +0100 Subject: [PATCH 111/129] fix(FloatingPanelLayouts): Made sure on iOS {16,17,18} iP SE 11Max that the public share action sheet size was correct --- kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift index 1e1b5f92a..848567e45 100644 --- a/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift +++ b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift @@ -48,7 +48,6 @@ class PublicShareFolderFloatingPanelLayout: FloatingPanelLayout { } } -/// Layout used for a file within a public share class PublicShareFileFloatingPanelLayout: FloatingPanelLayout { var position: FloatingPanelPosition = .bottom var initialState: FloatingPanelState = .tip @@ -59,7 +58,7 @@ class PublicShareFileFloatingPanelLayout: FloatingPanelLayout { self.initialState = initialState self.backdropAlpha = backdropAlpha let extendedAnchor = FloatingPanelLayoutAnchor( - absoluteInset: 248.0 + safeAreaInset, + absoluteInset: 320.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview ) From 34f71caaac5d78a242f82a6559f9e38481fc74c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 10:02:57 +0100 Subject: [PATCH 112/129] fix(DeeplinkService): Clear public share deeplink on user initiated exit --- .../UI/Controller/Menu/Share/PublicShareViewModel.swift | 2 ++ kDriveCore/Utils/Deeplinks/DeeplinkService.swift | 9 +++++++-- kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift index 136a358d4..d85d61fbd 100644 --- a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -26,6 +26,7 @@ import UIKit final class PublicShareViewModel: InMemoryFileListViewModel { @LazyInjectService private var accountManager: AccountManageable @LazyInjectService private var router: AppNavigable + @LazyInjectService private var deeplinkService: DeeplinkServiceable private var downloadObserver: ObservationToken? @@ -94,6 +95,7 @@ final class PublicShareViewModel: InMemoryFileListViewModel { } else if type == .addToMyDrive { addToMyDrive(sender: sender, publicShareProxy: publicShareProxy) } else if type == .cancel, !(multipleSelectionViewModel?.isMultipleSelectionEnabled ?? true) { + deeplinkService.clearLastPublicShare() onDismissViewController?() } else { super.barButtonPressed(sender: sender, type: type) diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift index a9559c1d4..4c8f13aab 100644 --- a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -20,16 +20,21 @@ import Foundation public protocol DeeplinkServiceable: AnyObject { func setLastPublicShare(_ link: PublicShareLink) + func clearLastPublicShare() func processDeeplinksPostAuthentication() } public class DeeplinkService: DeeplinkServiceable { - var lastPublicShareLink: PublicShareLink? + private var lastPublicShareLink: PublicShareLink? public func setLastPublicShare(_ link: PublicShareLink) { lastPublicShareLink = link } + public func clearLastPublicShare() { + lastPublicShareLink = nil + } + public func processDeeplinksPostAuthentication() { guard let lastPublicShareLink else { return @@ -37,7 +42,7 @@ public class DeeplinkService: DeeplinkServiceable { Task { await UniversalLinksHelper.processPublicShareLink(lastPublicShareLink) - self.lastPublicShareLink = nil + clearLastPublicShare() } } } diff --git a/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift index 82e72631e..3255b6c52 100644 --- a/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift +++ b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift @@ -81,6 +81,7 @@ public enum UniversalLinksHelper { return false } + @discardableResult public static func processPublicShareLink(_ link: PublicShareLink) async -> Bool { @InjectService var deeplinkService: DeeplinkServiceable deeplinkService.setLastPublicShare(link) From 170ca99b5862188b742f6934531acaed00299a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 13:24:31 +0100 Subject: [PATCH 113/129] fix(AppRouter): Made sure showUpsaleFloatingPanel works offline --- kDrive/AppRouter.swift | 11 +++-------- ...leActionsFloatingPanelViewController+Actions.swift | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 3a84e75b1..7d40b9087 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -415,18 +415,13 @@ public struct AppRouter: AppNavigable { } @MainActor public func showUpsaleFloatingPanel() { - guard let window else { - SentryDebug.captureNoWindow() - return - } - - guard let rootViewController = window.rootViewController else { + guard let topMostViewController else { return } let upsaleFloatingPanelController = UpsaleViewController - .instantiateInFloatingPanel(rootViewController: rootViewController) - rootViewController.present(upsaleFloatingPanelController, animated: true) + .instantiateInFloatingPanel(rootViewController: topMostViewController) + topMostViewController.present(upsaleFloatingPanelController, animated: true) } @MainActor public func showUpdateRequired() { diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index 754f257b4..2c2484044 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift @@ -525,7 +525,9 @@ extension FileActionsFloatingPanelViewController { private func addToMyDrive() { guard accountManager.currentAccount != nil else { - router.showUpsaleFloatingPanel() + dismiss(animated: true) { + self.router.showUpsaleFloatingPanel() + } return } From 8218ad48df2904311218e7f00ceeec405e5a3dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 14:16:45 +0100 Subject: [PATCH 114/129] fix(AppRouter): Making PublicShare navigation play nice with appLock --- kDrive/AppRouter.swift | 6 ++++++ kDrive/UI/Controller/LoginDelegateHandler.swift | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 3a84e75b1..b74a7c2bc 100644 --- a/kDrive/AppRouter.swift +++ b/kDrive/AppRouter.swift @@ -35,6 +35,7 @@ public struct AppRouter: AppNavigable { @LazyInjectService private var availableOfflineManager: AvailableOfflineManageable @LazyInjectService private var accountManager: AccountManageable @LazyInjectService private var infomaniakLogin: InfomaniakLoginable + @LazyInjectService private var deeplinkService: DeeplinkServiceable @LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager @LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager @@ -146,6 +147,7 @@ public struct AppRouter: AppNavigable { Task { await askForReview() await askUserToRemovePicturesIfNecessary() + deeplinkService.processDeeplinksPostAuthentication() } case .onboarding: showOnboarding() @@ -667,6 +669,10 @@ public struct AppRouter: AppNavigable { return } + if let topMostViewController, (topMostViewController as? LockedAppViewController) != nil { + return + } + rootViewController.dismiss(animated: false) { let configuration = FileListViewModel.Configuration(selectAllSupported: true, rootTitle: nil, diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift index 9c82b8c55..99e39567d 100644 --- a/kDrive/UI/Controller/LoginDelegateHandler.swift +++ b/kDrive/UI/Controller/LoginDelegateHandler.swift @@ -26,7 +26,6 @@ import kDriveResources public final class LoginDelegateHandler: InfomaniakLoginDelegate { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var router: AppNavigable - @LazyInjectService var deeplinkService: DeeplinkServiceable var didStartLoginCallback: (() -> Void)? var didCompleteLoginCallback: (() -> Void)? @@ -58,8 +57,6 @@ public final class LoginDelegateHandler: InfomaniakLoginDelegate { } didCompleteLoginCallback?() - - deeplinkService.processDeeplinksPostAuthentication() } } From 2ac721aa929b3f95558951304c96cf7afd8d8ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 14:26:32 +0100 Subject: [PATCH 115/129] fix(PublicShare): Fix a bug where a reused PublicShare in memory database would crash during a write --- kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift index 3255b6c52..7379601ce 100644 --- a/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift +++ b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift @@ -181,7 +181,7 @@ public enum UniversalLinksHelper { fileId: fileId) // Root folder must be in database for the FileListViewModel to work try driveFileManager.database.writeTransaction { writableRealm in - writableRealm.add(rootFolder) + writableRealm.add(rootFolder, update: .modified) } let frozenRootFolder = rootFolder.freeze() From 8174becc298a8fb5648f9c8a263f36cb414891dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 10 Jan 2025 14:46:43 +0100 Subject: [PATCH 116/129] chore(AppDelegate): Clean Public Share sample links --- kDrive/AppDelegate.swift | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/kDrive/AppDelegate.swift b/kDrive/AppDelegate.swift index 4d974d317..98cc22a97 100644 --- a/kDrive/AppDelegate.swift +++ b/kDrive/AppDelegate.swift @@ -103,33 +103,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } application.registerForRemoteNotifications() - // swiftlint:disable force_try - Task { - try! await Task.sleep(nanoseconds: 5_000_000_000) - - /* TODO: Remove later - @InjectService var router: AppNavigable - let upsaleViewController = UpsaleViewController() - let noDriveViewController = NoDriveUpsaleViewController() - let floatingPanel = UpsaleFloatingPanelController(upsaleViewController: noDriveViewController) - router.topMostViewController?.present(floatingPanel, animated: true, completion: nil) - */ - - /* TODO: Remove later - // a public share expired - let somePublicShareExpired = - URL(string: "https://kdrive.infomaniak.com/app/share/140946/81de098a-3156-4ae6-93df-be7f9ae78ddd") - // a public share password protected - let somePublicShareProtected = - URL(string: "https://kdrive.infomaniak.com/app/share/140946/34844cea-db8d-4d87-b66f-e944e9759a2e") - */ - - // a valid public share - let somePublicShare = - URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981") - await UniversalLinksHelper.handleURL(somePublicShare!) - } - return true } From d32391f31e5c70ae9efe3e8f41cf954e5b393184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 14 Jan 2025 09:29:37 +0100 Subject: [PATCH 117/129] refactor: Renamed nextVC to destinationViewController --- .../Controller/Files/FileDetailViewController.swift | 6 +++--- kDrive/UI/Controller/Files/FilePresenter.swift | 10 +++++----- .../Files/Save File/SelectFolderViewController.swift | 11 ++++++++--- kDrive/UI/Controller/Home/HomeViewController.swift | 7 +++++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/kDrive/UI/Controller/Files/FileDetailViewController.swift b/kDrive/UI/Controller/Files/FileDetailViewController.swift index 0cc3b82c8..fb4363546 100644 --- a/kDrive/UI/Controller/Files/FileDetailViewController.swift +++ b/kDrive/UI/Controller/Files/FileDetailViewController.swift @@ -383,9 +383,9 @@ class FileDetailViewController: UIViewController, SceneStateRestorable { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "toShareLinkSettingsSegue" { - let nextVC = segue.destination as! ShareLinkSettingsViewController - nextVC.driveFileManager = driveFileManager - nextVC.file = file + let destinationViewController = segue.destination as! ShareLinkSettingsViewController + destinationViewController.driveFileManager = driveFileManager + destinationViewController.file = file } } diff --git a/kDrive/UI/Controller/Files/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift index a4a8bb14e..aa934c43f 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -168,13 +168,13 @@ final class FilePresenter { viewModel = ConcreteFileListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } - let nextVC = FileListViewController(viewModel: viewModel) - viewModel.onDismissViewController = { [weak nextVC] in - nextVC?.dismiss(animated: true) + let destinationViewController = FileListViewController(viewModel: viewModel) + viewModel.onDismissViewController = { [weak destinationViewController] in + destinationViewController?.dismiss(animated: true) } guard file.isDisabled else { - navigationController?.pushViewController(nextVC, animated: animated) + navigationController?.pushViewController(destinationViewController, animated: animated) return } @@ -194,7 +194,7 @@ final class FilePresenter { let response = try await driveFileManager.apiFetcher.forceAccess(to: proxyFile) if response { accessFileDriveFloatingPanelController.dismiss(animated: true) - self.navigationController?.pushViewController(nextVC, animated: true) + self.navigationController?.pushViewController(destinationViewController, animated: true) } else { UIConstants.showSnackBar(message: KDriveResourcesStrings.Localizable.errorRightModification) } diff --git a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift index 28f276d54..00787681e 100644 --- a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift @@ -73,7 +73,12 @@ class SelectFolderViewController: FileListViewController { override func viewDidLoad() { super.viewDidLoad() - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.floatingButtonPaddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: UIConstants.List.floatingButtonPaddingBottom, + right: 0 + ) view.addSubview(selectFolderButton) @@ -201,7 +206,7 @@ class SelectFolderViewController: FileListViewController { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedFile = viewModel.getFile(at: indexPath)! if selectedFile.isDirectory { - let nextVC = SelectFolderViewController( + let destinationViewController = SelectFolderViewController( viewModel: SelectFolderViewModel( driveFileManager: viewModel.driveFileManager, currentDirectory: selectedFile @@ -211,7 +216,7 @@ class SelectFolderViewController: FileListViewController { delegate: delegate, selectHandler: selectHandler ) - navigationController?.pushViewController(nextVC, animated: true) + navigationController?.pushViewController(destinationViewController, animated: true) } } } diff --git a/kDrive/UI/Controller/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift index d61de09fb..686e579f9 100644 --- a/kDrive/UI/Controller/Home/HomeViewController.swift +++ b/kDrive/UI/Controller/Home/HomeViewController.swift @@ -475,8 +475,11 @@ extension HomeViewController: RecentActivityDelegate { } if activities.count > 3 && index > 1 { - let nextVC = RecentActivityFilesViewController(activities: activities, driveFileManager: driveFileManager) - filePresenter.navigationController?.pushViewController(nextVC, animated: true) + let destinationViewController = RecentActivityFilesViewController( + activities: activities, + driveFileManager: driveFileManager + ) + filePresenter.navigationController?.pushViewController(destinationViewController, animated: true) } else { filePresenter.present( for: driveFileManager.getManagedFile(from: file), From f2822e33b385227dc57133e6fafdec85c2b37ae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 14 Jan 2025 09:36:32 +0100 Subject: [PATCH 118/129] chore: PR Feedback --- .../UI/Controller/Files/Save File/SaveFileViewController.swift | 1 - kDriveCore/Data/Api/Endpoint+Share.swift | 1 - kDriveCore/Utils/AppNavigable.swift | 2 -- 3 files changed, 4 deletions(-) diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 1c98438a1..64405a257 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -229,7 +229,6 @@ class SaveFileViewController: UIViewController { return myFilesDirectory.freezeIfNeeded() } - // If we are in a shared with me, we only have access to some folders that are shared with the user guard selectedDriveFileManager.drive.sharedWithMe else { return nil } let firstAvailableSharedDriveDirectory = selectedDriveFileManager.database.fetchResults(ofType: File.self) { lazyFiles in diff --git a/kDriveCore/Data/Api/Endpoint+Share.swift b/kDriveCore/Data/Api/Endpoint+Share.swift index 18164c8bf..a801db5fd 100644 --- a/kDriveCore/Data/Api/Endpoint+Share.swift +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -103,7 +103,6 @@ public extension Endpoint { return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") } - /// Add Public Share to my Drive static func importShareLinkFiles(destinationDrive: AbstractDrive) -> Endpoint { return Endpoint.driveInfoV2(drive: destinationDrive).appending(path: "/imports/sharelink") } diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index a5f9c1e60..1fbc024a9 100644 --- a/kDriveCore/Utils/AppNavigable.swift +++ b/kDriveCore/Utils/AppNavigable.swift @@ -49,10 +49,8 @@ public protocol RouterAppNavigable { files: [ImportedFile] ) - /// Present login webView on top of the topMostViewController @MainActor func showLogin(delegate: InfomaniakLoginDelegate) - /// Present register webView on top of the topMostViewController @MainActor func showRegister(delegate: InfomaniakLoginDelegate) } From 3d5a213377400ec121c0b27bd6105c794dfd2b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 21 Jan 2025 16:34:27 +0100 Subject: [PATCH 119/129] feat(VideoPlayer.swift): Video preview streaming enabled --- kDriveCore/VideoPlayer/VideoPlayer.swift | 41 +++++++++++++++--------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/kDriveCore/VideoPlayer/VideoPlayer.swift b/kDriveCore/VideoPlayer/VideoPlayer.swift index f833f9e52..56e7649da 100644 --- a/kDriveCore/VideoPlayer/VideoPlayer.swift +++ b/kDriveCore/VideoPlayer/VideoPlayer.swift @@ -91,10 +91,6 @@ public final class VideoPlayer: Pausable { } private func setupPlayer(with file: File, driveFileManager: DriveFileManager) { - guard !driveFileManager.isPublicShare else { - return - } - if !file.isLocalVersionOlderThanRemote { let localAsset = AVAsset(url: file.localUrl) asset = localAsset @@ -102,24 +98,37 @@ public final class VideoPlayer: Pausable { player = AVPlayer(playerItem: playerItem) updateMetadata(asset: localAsset, defaultName: file.name) observePlayer(currentItem: playerItem) + } else if let publicShareProxy = driveFileManager.publicShareProxy { + let url = Endpoint.downloadShareLinkFile( + driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + fileId: file.id + ).url + + let asset = AVURLAsset(url: url, options: nil) + setupStreamingAsset(asset, fileName: file.name) + } else if let token = driveFileManager.apiFetcher.currentToken { driveFileManager.apiFetcher.performAuthenticatedRequest(token: token) { token, _ in - if let token = token { - let url = Endpoint.download(file: file).url - let headers = ["Authorization": "Bearer \(token.accessToken)"] - let urlAsset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) - self.asset = urlAsset - Task { @MainActor in - let playerItem = AVPlayerItem(asset: urlAsset) - self.player = AVPlayer(playerItem: playerItem) - self.updateMetadata(asset: urlAsset, defaultName: file.name) - self.observePlayer(currentItem: playerItem) - } - } + guard let token else { return } + let url = Endpoint.download(file: file).url + let headers = ["Authorization": "Bearer \(token.accessToken)"] + let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + self.setupStreamingAsset(asset, fileName: file.name) } } } + private func setupStreamingAsset(_ urlAsset: AVURLAsset, fileName: String) { + asset = urlAsset + Task { @MainActor in + let playerItem = AVPlayerItem(asset: urlAsset) + self.player = AVPlayer(playerItem: playerItem) + self.updateMetadata(asset: urlAsset, defaultName: fileName) + self.observePlayer(currentItem: playerItem) + } + } + private func observePlayer(currentItem: AVPlayerItem) { NotificationCenter.default.addObserver( self, From 3e187938a10d47bd7766e24c4a5304942d9c269c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 12:47:18 +0100 Subject: [PATCH 120/129] chore: Sonar feedback --- kDriveCore/VideoPlayer/VideoPlayer.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kDriveCore/VideoPlayer/VideoPlayer.swift b/kDriveCore/VideoPlayer/VideoPlayer.swift index 56e7649da..215f6beaa 100644 --- a/kDriveCore/VideoPlayer/VideoPlayer.swift +++ b/kDriveCore/VideoPlayer/VideoPlayer.swift @@ -105,16 +105,16 @@ public final class VideoPlayer: Pausable { fileId: file.id ).url - let asset = AVURLAsset(url: url, options: nil) - setupStreamingAsset(asset, fileName: file.name) + let remoteAsset = AVURLAsset(url: url, options: nil) + setupStreamingAsset(remoteAsset, fileName: file.name) } else if let token = driveFileManager.apiFetcher.currentToken { driveFileManager.apiFetcher.performAuthenticatedRequest(token: token) { token, _ in guard let token else { return } let url = Endpoint.download(file: file).url let headers = ["Authorization": "Bearer \(token.accessToken)"] - let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) - self.setupStreamingAsset(asset, fileName: file.name) + let remoteAsset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + self.setupStreamingAsset(remoteAsset, fileName: file.name) } } } From 2a7e0b3c65600593753b85b9c5f69e3a2b150421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 12:50:31 +0100 Subject: [PATCH 121/129] chore: Swiftformat feedback --- kDriveCore/Data/Api/Endpoint+Trash.swift | 4 ++-- .../DownloadQueue/BackgroundDownloadSessionManager.swift | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift index c192ad306..17e778a22 100644 --- a/kDriveCore/Data/Api/Endpoint+Trash.swift +++ b/kDriveCore/Data/Api/Endpoint+Trash.swift @@ -23,9 +23,9 @@ import RealmSwift // MARK: - Trash public extension Endpoint { - private static let trashPath: String = "/trash" + private static let trashPath = "/trash" - private static let countPath: String = "/count" + private static let countPath = "/count" static func trash(drive: AbstractDrive) -> Endpoint { return .driveInfo(drive: drive).appending(path: trashPath, queryItems: [FileWith.fileMinimal.toQueryItem()]) diff --git a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift index a9195f75e..19cdbf1c1 100644 --- a/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift +++ b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift @@ -167,7 +167,10 @@ public final class BackgroundDownloadSessionManager: NSObject, BackgroundDownloa userId: downloadTask.userId ), let file = driveFileManager.getCachedFile(id: downloadTask.fileId) { - let operation = DownloadAuthenticatedOperation(file: file, driveFileManager: driveFileManager, task: task, urlSession: self) + let operation = DownloadAuthenticatedOperation(file: file, + driveFileManager: driveFileManager, + task: task, + urlSession: self) tasksCompletionHandler[taskIdentifier] = operation.downloadCompletion operations.append(operation) return operation.downloadCompletion From b9da8a7fe58d34cb5d1a6f264bb3052346375c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 13:44:50 +0100 Subject: [PATCH 122/129] feat(FileGridCollectionViewCell): Preview thumbnails --- .../Files/FileGridCollectionViewCell.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift index 58b6e7ec2..f56d8d5a7 100644 --- a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift @@ -31,11 +31,24 @@ final class FileGridViewModel: FileViewModel { imageView.image = nil imageView.backgroundColor = KDriveResourcesAsset.loaderDarkerDefaultColor.color thumbnailDownloadTask?.cancel() - thumbnailDownloadTask = file.getThumbnail { image, _ in - imageView.image = image - imageView.backgroundColor = nil + + if let publicShareProxy { + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { thumbnail, _ in + self.setThumbnail(thumbnail, on: imageView) + } + } else { + thumbnailDownloadTask = file.getThumbnail { thumbnail, _ in + self.setThumbnail(thumbnail, on: imageView) + } } } + + private func setThumbnail(_ thumbnail: UIImage, on imageView: UIImageView) { + imageView.image = thumbnail + imageView.backgroundColor = nil + } } class FileGridCollectionViewCell: FileCollectionViewCell { From 05fddd65379c055574c4c5b663179e82b0b2d4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 13:58:36 +0100 Subject: [PATCH 123/129] fix: Clear public share link on logout --- kDriveCore/Data/Cache/AccountManager.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 43d426e2b..dd78d4f83 100644 --- a/kDriveCore/Data/Cache/AccountManager.swift +++ b/kDriveCore/Data/Cache/AccountManager.swift @@ -98,6 +98,7 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { @LazyInjectService var notificationHelper: NotificationsHelpable @LazyInjectService var networkLogin: InfomaniakNetworkLoginable @LazyInjectService var appNavigable: AppNavigable + @LazyInjectService var deeplinkService: DeeplinkServiceable private static let appIdentifierPrefix = Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String private static let group = "com.infomaniak.drive" @@ -546,6 +547,8 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { public func logoutCurrentAccountAndSwitchToNextIfPossible() { Task { @MainActor in + deeplinkService.clearLastPublicShare() + if let currentAccount { removeTokenAndAccount(account: currentAccount) } From eab30028694116c0ff44cf3c1d2533aeba28c84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 14:33:59 +0100 Subject: [PATCH 124/129] fix: Missing entry point to reload the Public Share link post authentication --- kDrive/UI/Controller/LoginDelegateHandler.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift index 99e39567d..e8fe978f3 100644 --- a/kDrive/UI/Controller/LoginDelegateHandler.swift +++ b/kDrive/UI/Controller/LoginDelegateHandler.swift @@ -24,6 +24,7 @@ import kDriveCore import kDriveResources public final class LoginDelegateHandler: InfomaniakLoginDelegate { + @LazyInjectService var deeplinkService: DeeplinkServiceable @LazyInjectService var accountManager: AccountManageable @LazyInjectService var router: AppNavigable @@ -64,6 +65,10 @@ public final class LoginDelegateHandler: InfomaniakLoginDelegate { UserDefaults.shared.legacyIsFirstLaunch = false UserDefaults.shared.numberOfConnections = 1 _ = router.showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) + + Task { + deeplinkService.processDeeplinksPostAuthentication() + } } private func didCompleteLoginWithError(_ error: Error, From e4f70c398034602a5408b379847a9ba6a4830b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 15:42:44 +0100 Subject: [PATCH 125/129] refactor(LoginDelegateHandler): Remove extraneous Task --- kDrive/UI/Controller/LoginDelegateHandler.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift index e8fe978f3..44f820e68 100644 --- a/kDrive/UI/Controller/LoginDelegateHandler.swift +++ b/kDrive/UI/Controller/LoginDelegateHandler.swift @@ -65,10 +65,7 @@ public final class LoginDelegateHandler: InfomaniakLoginDelegate { UserDefaults.shared.legacyIsFirstLaunch = false UserDefaults.shared.numberOfConnections = 1 _ = router.showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) - - Task { - deeplinkService.processDeeplinksPostAuthentication() - } + deeplinkService.processDeeplinksPostAuthentication() } private func didCompleteLoginWithError(_ error: Error, From fedfd888082988c1832d28a1292283708ca8770c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 15:43:40 +0100 Subject: [PATCH 126/129] fix(DeeplinkService): Make sure task is ran on the main queue --- kDriveCore/Utils/Deeplinks/DeeplinkService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift index 4c8f13aab..9c0c89d31 100644 --- a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -40,7 +40,7 @@ public class DeeplinkService: DeeplinkServiceable { return } - Task { + Task { @MainActor in await UniversalLinksHelper.processPublicShareLink(lastPublicShareLink) clearLastPublicShare() } From 65fca3b26dbde1e2b80e263fc50155935b03b968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 22 Jan 2025 16:05:09 +0100 Subject: [PATCH 127/129] feat(PreviewViewController): Download in cache public share files for preview --- .../Files/Preview/PreviewViewController.swift | 91 ++++++++++++------- .../Data/DownloadQueue/DownloadQueue.swift | 6 +- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 1e228ff9e..6a51c7743 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -542,7 +542,24 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, return } - downloadFile(at: indexPath) + if let publicShareProxy = driveFileManager.publicShareProxy { + downloadPublicShareFile(at: indexPath, publicShareProxy: publicShareProxy) + } else { + downloadFile(at: indexPath) + } + } + + private func downloadPublicShareFile(at indexPath: IndexPath, publicShareProxy: PublicShareProxy) { + DownloadQueue.instance.addPublicShareToQueue( + file: currentFile, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy, + onOperationCreated: { operation in + self.trackOperationCreated(at: indexPath, downloadOperation: operation) + }, completion: { error in + self.downloadCompletion(at: indexPath, error: error) + } + ) } private func downloadFile(at indexPath: IndexPath) { @@ -550,44 +567,52 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, file: currentFile, userId: accountManager.currentUserId, onOperationCreated: { operation in - Task { @MainActor [weak self] in - guard let self else { - return - } - - currentDownloadOperation = operation - if let progress = currentDownloadOperation?.progress, - let cell = collectionView.cellForItem(at: indexPath) as? DownloadProgressObserver { - cell.setDownloadProgress(progress) - } - } + self.trackOperationCreated(at: indexPath, downloadOperation: operation) }, completion: { error in - Task { @MainActor [weak self] in - guard let self else { return } - - currentDownloadOperation = nil - - guard view.window != nil else { return } - - if let error { - if error != .taskCancelled { - previewErrors[currentFile.id] = PreviewError(fileId: currentFile.id, downloadError: error) - collectionView.reloadItems(at: [indexPath]) - } - } else { - (collectionView.cellForItem(at: indexPath) as? DownloadingPreviewCollectionViewCell)? - .previewDownloadTask?.cancel() - previewErrors[currentFile.id] = nil - collectionView.endEditing(true) - collectionView.reloadItems(at: [indexPath]) - updateNavigationBar() - } - } + self.downloadCompletion(at: indexPath, error: error) } ) } + private func trackOperationCreated(at indexPath: IndexPath, downloadOperation: DownloadAuthenticatedOperation?) { + Task { @MainActor [weak self] in + guard let self else { + return + } + + currentDownloadOperation = downloadOperation + if let progress = currentDownloadOperation?.progress, + let cell = collectionView.cellForItem(at: indexPath) as? DownloadProgressObserver { + cell.setDownloadProgress(progress) + } + } + } + + private func downloadCompletion(at indexPath: IndexPath, error: DriveError?) { + Task { @MainActor [weak self] in + guard let self else { return } + + currentDownloadOperation = nil + + guard view.window != nil else { return } + + if let error { + if error != .taskCancelled { + previewErrors[currentFile.id] = PreviewError(fileId: currentFile.id, downloadError: error) + collectionView.reloadItems(at: [indexPath]) + } + } else { + (collectionView.cellForItem(at: indexPath) as? DownloadingPreviewCollectionViewCell)? + .previewDownloadTask?.cancel() + previewErrors[currentFile.id] = nil + collectionView.endEditing(true) + collectionView.reloadItems(at: [indexPath]) + updateNavigationBar() + } + } + } + static func instantiate( files: [File], index: Int, diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 9e6179eb8..57047d578 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift @@ -115,7 +115,9 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { public func addPublicShareToQueue(file: File, driveFileManager: DriveFileManager, publicShareProxy: PublicShareProxy, - itemIdentifier: NSFileProviderItemIdentifier? = nil) { + itemIdentifier: NSFileProviderItemIdentifier? = nil, + onOperationCreated: ((DownloadPublicShareOperation?) -> Void)? = nil, + completion: ((DriveError?) -> Void)? = nil) { Log.downloadQueue("addPublicShareToQueue file:\(file.id)") let file = file.freezeIfNeeded() @@ -139,10 +141,12 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { self.fileOperationsInQueue.removeValue(forKey: file.id) self.publishFileDownloaded(fileId: file.id, error: operation.error) OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.fileOperationsInQueue.isEmpty) + completion?(operation.error) } } self.operationQueue.addOperation(operation) self.fileOperationsInQueue[file.id] = operation + onOperationCreated?(operation) } } From b0f2ab3e4b43a3daaf71a4e896f20a1104f4b5d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 23 Jan 2025 10:57:30 +0100 Subject: [PATCH 128/129] fix(PreviewViewController): Disable edit button in public share mode --- .../Controller/Files/Preview/PreviewViewController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 6a51c7743..3b4f440ed 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -52,6 +52,10 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } } + private var editButtonHidden: Bool { + driveFileManager.isPublicShare + } + private var currentDownloadOperation: DownloadAuthenticatedOperation? private let pdfPageLabel = UILabel(frame: .zero) private var titleWidthConstraint: NSLayoutConstraint? @@ -344,7 +348,7 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, private func setNavbarForEditing() { backButton.isHidden = false pdfPageLabel.isHidden = true - editButton.isHidden = false + editButton.isHidden = editButtonHidden openButton.isHidden = true } @@ -372,6 +376,7 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } @objc private func editFile() { + guard !driveFileManager.isPublicShare else { return } MatomoUtils.track(eventWithCategory: .mediaPlayer, name: "edit") floatingPanelViewController.dismiss(animated: true) OnlyOfficeViewController.open(driveFileManager: driveFileManager, file: currentFile, viewController: self) From 79ba7bce525ce45a7d4f7753858e385be3f59bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 24 Jan 2025 08:46:50 +0100 Subject: [PATCH 129/129] fix(FileListViewController): Bottom padding in public share mode --- .../Files/File List/FileListViewController.swift | 9 ++++++++- kDriveCore/UI/UIConstants.swift | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/kDrive/UI/Controller/Files/File List/FileListViewController.swift b/kDrive/UI/Controller/Files/File List/FileListViewController.swift index cf22337ad..b88ee2169 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -64,6 +64,13 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV // MARK: - Properties + private var paddingBottom: CGFloat { + guard !driveFileManager.isPublicShare else { + return UIConstants.List.publicSharePaddingBottom + } + return UIConstants.List.paddingBottom + } + var collectionViewFlowLayout: UICollectionViewFlowLayout? { collectionViewLayout as? UICollectionViewFlowLayout } @@ -131,7 +138,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerViewIdentifier ) - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIConstants.List.paddingBottom, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: paddingBottom, right: 0) collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color (collectionView as? SwipableCollectionView)?.swipeDataSource = self (collectionView as? SwipableCollectionView)?.swipeDelegate = self diff --git a/kDriveCore/UI/UIConstants.swift b/kDriveCore/UI/UIConstants.swift index d53e37ac6..30ff3a315 100644 --- a/kDriveCore/UI/UIConstants.swift +++ b/kDriveCore/UI/UIConstants.swift @@ -38,6 +38,7 @@ public enum UIConstants { public enum List { public static let paddingBottom = 50.0 + public static let publicSharePaddingBottom = 90.0 public static let floatingButtonPaddingBottom = 75.0 }