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/**", diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift index 0cbbae7ae..8bf64bc6c 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,8 @@ 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 private var deeplinkService: DeeplinkServiceable @LazyInjectService var backgroundDownloadSessionManager: BackgroundDownloadSessionManager @LazyInjectService var backgroundUploadSessionManager: BackgroundUploadSessionManager @@ -144,6 +147,7 @@ public struct AppRouter: AppNavigable { Task { await askForReview() await askUserToRemovePicturesIfNecessary() + deeplinkService.processDeeplinksPostAuthentication() } case .onboarding: showOnboarding() @@ -412,6 +416,16 @@ public struct AppRouter: AppNavigable { } } + @MainActor public func showUpsaleFloatingPanel() { + guard let topMostViewController else { + return + } + + let upsaleFloatingPanelController = UpsaleViewController + .instantiateInFloatingPanel(rootViewController: topMostViewController) + topMostViewController.present(upsaleFloatingPanelController, animated: true) + } + @MainActor public func showUpdateRequired() { guard let window else { SentryDebug.captureNoWindow() @@ -444,6 +458,27 @@ 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 + } + + MatomoUtils.track(eventWithCategory: .account, name: "openLoginWebview") + infomaniakLogin.webviewLoginFrom(viewController: topMostViewController, + hideCreateAccountButton: true, + delegate: delegate) + } + // MARK: AppExtensionRouter public func showStore(from viewController: UIViewController, driveFileManager: DriveFileManager) { @@ -585,6 +620,84 @@ public struct AppRouter: AppNavigable { // MARK: RouterFileNavigable + @MainActor public func presentPublicShareLocked(_ destinationURL: URL) { + guard let window, + let rootViewController = window.rootViewController else { + return + } + + rootViewController.dismiss(animated: false) { + let viewController = LockedFolderViewController() + viewController.destinationURL = destinationURL + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical + + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) + } + } + + @MainActor public func presentPublicShareExpired() { + guard let window, + let rootViewController = window.rootViewController else { + return + } + + rootViewController.dismiss(animated: false) { + let viewController = UnavaillableFolderViewController() + let publicShareNavigationController = UINavigationController(rootViewController: viewController) + publicShareNavigationController.modalPresentationStyle = .fullScreen + publicShareNavigationController.modalTransitionStyle = .coverVertical + + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) + } + } + + @MainActor public func presentPublicShare( + frozenRootFolder: File, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) { + guard let window, + let rootViewController = window.rootViewController else { + return + } + + if let topMostViewController, (topMostViewController as? LockedAppViewController) != nil { + return + } + + rootViewController.dismiss(animated: false) { + let configuration = FileListViewModel.Configuration(selectAllSupported: true, + rootTitle: nil, + 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, + configuration: configuration) + 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 + + rootViewController.present(publicShareNavigationController, animated: true, completion: nil) + } + } + @MainActor public func present(file: File, driveFileManager: DriveFileManager) { present(file: file, driveFileManager: driveFileManager, office: false) } 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..96eb5f71b --- /dev/null +++ b/kDrive/Resources/Assets.xcassets/UFO.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "abducted_files.svg", + "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/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/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/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/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"; diff --git a/kDrive/SceneDelegate.swift b/kDrive/SceneDelegate.swift index b084c851e..c0ce74bf5 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 else { + Log.sceneDelegate("scene continue userActivity - invalid activity", level: .error) + return + } - UniversalLinksHelper.handlePath(components.path) + await UniversalLinksHelper.handleURL(incomingURL) + } } func scene(_ scene: UIScene, didFailToContinueUserActivityWithType userActivityType: String, error: Error) { diff --git a/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift new file mode 100644 index 000000000..848567e45 --- /dev/null +++ b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift @@ -0,0 +1,140 @@ +/* + 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 + } +} + +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: 320.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..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 @@ -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/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/External/BaseInfoViewController.swift b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift new file mode 100644 index 000000000..440f2597f --- /dev/null +++ b/kDrive/UI/Controller/Files/External/BaseInfoViewController.swift @@ -0,0 +1,103 @@ +/* + 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 InfomaniakCoreCommonUI +import InfomaniakCoreUIKit +import kDriveCore +import kDriveResources +import UIKit + +class BaseInfoViewController: UIViewController { + let titleLabel: IKLabel = { + let label = IKLabel() + label.style = .header1 + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + let descriptionLabel: IKLabel = { + let label = IKLabel() + label.style = .body2 + 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.backgroundColor.color + setupCloseButton() + setupBody() + } + + private func setupCloseButton() { + let closeButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(closeButtonPressed)) + closeButton.accessibilityLabel = KDriveResourcesStrings.Localizable.buttonClose + navigationItem.leftBarButtonItem = closeButton + } + + private func setupBody() { + 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 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), + 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/External/LockedFolderViewController.swift b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift new file mode 100644 index 000000000..614f5cf7e --- /dev/null +++ b/kDrive/UI/Controller/Files/External/LockedFolderViewController.swift @@ -0,0 +1,73 @@ +/* + 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 + +class LockedFolderViewController: BaseInfoViewController { + var destinationURL: URL? + + let openWebButton = IKLargeButton(frame: .zero) + + override func viewDidLoad() { + super.viewDidLoad() + + centerImageView.image = KDriveResourcesAsset.lockExternal.image + titleLabel.text = KDriveCoreStrings.Localizable.publicSharePasswordNeededTitle + descriptionLabel.text = KDriveCoreStrings.Localizable.publicSharePasswordNotSupportedDescription + + setupOpenWebButton() + } + + private func setupOpenWebButton() { + openWebButton.setTitle(KDriveCoreStrings.Localizable.buttonOpenInBrowser, for: .normal) + openWebButton.translatesAutoresizingMaskIntoConstraints = false + openWebButton.addTarget(self, action: #selector(openWebBrowser), for: .touchUpInside) + + view.addSubview(openWebButton) + view.bringSubviewToFront(openWebButton) + + let leadingConstraint = openWebButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, + constant: 25) + leadingConstraint.priority = UILayoutPriority.defaultHigh + let trailingConstraint = openWebButton.trailingAnchor.constraint( + greaterThanOrEqualTo: view.trailingAnchor, + constant: -25 + ) + 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: -30), + openWebButton.heightAnchor.constraint(equalToConstant: 60), + widthConstraint + ]) + } + + @objc public func openWebBrowser() { + guard let destinationURL else { + return + } + + UIApplication.shared.open(destinationURL) + } +} diff --git a/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift new file mode 100644 index 000000000..b39a118af --- /dev/null +++ b/kDrive/UI/Controller/Files/External/UnavaillableFolderViewController.swift @@ -0,0 +1,30 @@ +/* + 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 + +class UnavaillableFolderViewController: BaseInfoViewController { + override func viewDidLoad() { + super.viewDidLoad() + + centerImageView.image = KDriveResourcesAsset.ufo.image + titleLabel.text = KDriveStrings.Localizable.publicShareOutdatedLinkTitle + descriptionLabel.text = KDriveStrings.Localizable.publicShareOutdatedLinkDescription + } +} 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 8203ea593..b88ee2169 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewController.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewController.swift @@ -19,6 +19,7 @@ import CocoaLumberjackSwift import Combine import DifferenceKit +import FloatingPanel import InfomaniakCore import InfomaniakDI import kDriveCore @@ -50,6 +51,7 @@ extension SortType: Selectable { class FileListViewController: UICollectionViewController, SwipeActionCollectionViewDelegate, SwipeActionCollectionViewDataSource, FilesHeaderViewDelegate, SceneStateRestorable { @LazyInjectService var accountManager: AccountManageable + @LazyInjectService var router: AppNavigable // MARK: - Constants @@ -62,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 } @@ -91,6 +100,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 { @@ -121,7 +138,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: paddingBottom, right: 0) collectionView.backgroundColor = KDriveResourcesAsset.backgroundColor.color (collectionView as? SwipableCollectionView)?.swipeDataSource = self (collectionView as? SwipableCollectionView)?.swipeDelegate = self @@ -141,6 +158,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV ) setupViewModel() + setupFooterIfNeeded() } override func viewWillAppear(_ animated: Bool) { @@ -250,6 +268,53 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + func setupFooterIfNeeded() { + guard driveFileManager.isPublicShare else { return } + + 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(_ sender: UIButton?) { + defer { + sender?.isSelected = false + sender?.isEnabled = true + sender?.isHighlighted = false + } + + guard accountManager.currentAccount != nil else { + #if !ISEXTENSION + let upsaleFloatingPanelController = UpsaleViewController.instantiateInFloatingPanel(rootViewController: self) + present(upsaleFloatingPanelController, animated: true, completion: nil) + #else + dismiss(animated: true) + #endif + + return + } + + viewModel.barButtonPressed(sender: sender, type: .addToMyDrive) + } + func reloadCollectionViewWith(files: [File]) { let changeSet = StagedChangeset(source: displayedFiles, target: files) collectionView.reload(using: changeSet, @@ -352,6 +417,30 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV } } + private func fileFloatingPanelLayout(files: [File]) -> FloatingPanelLayout { + guard driveFileManager.isPublicShare else { + return FileFloatingPanelLayout( + initialState: .half, + hideTip: true, + backdropAlpha: 0.2 + ) + } + + if files.first?.isDirectory == true { + 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 @@ -363,11 +452,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 = fileFloatingPanelLayout(files: files) if let file = files.first { fileInformationsViewController.setFile(file, driveFileManager: driveFileManager) @@ -385,7 +470,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] @@ -411,6 +496,10 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV presentingParent: self ) + if downloadOnly { + selectViewController.actions = [.download] + } + floatingPanelViewController = AdaptiveDriveFloatingPanelController() floatingPanelViewController.set(contentViewController: selectViewController) (floatingPanelViewController as? AdaptiveDriveFloatingPanelController)? @@ -469,7 +558,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() { @@ -752,7 +841,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/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift index 6201676ef..d89a9d9e3 100644 --- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift +++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift @@ -34,12 +34,15 @@ enum FileListBarButtonType { case searchFilters case photoSort case addFolder + case downloadAll + case downloadingAll + case addToMyDrive } enum FileListQuickActionType { case file case trash - case multipleSelection + case multipleSelection(onlyDownload: Bool) } enum ControllerPresentationType { @@ -137,6 +140,8 @@ class FileListViewModel: SelectDelegate { } } + var onDismissViewController: (() -> Void)? + var sortTypeObservation: AnyCancellable? var listStyleObservation: AnyCancellable? var bindStore = Set() @@ -160,8 +165,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 { @@ -195,6 +198,13 @@ class FileListViewModel: SelectDelegate { currentDirectory: self.currentDirectory ) } + + loadButtonsConfiguration() + } + + func loadButtonsConfiguration() { + currentLeftBarButtons = configuration.leftBarButtons + currentRightBarButtons = configuration.rightBarButtons } func updateRealmObservation() { @@ -279,7 +289,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/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift index ea8257905..7d9223835 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: .move, name: KDriveResourcesStrings.Localizable.buttonMove, icon: KDriveResourcesAsset.folderSelect ) static let delete = MultipleSelectionAction( - id: 1, + id: .delete, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) static let more = MultipleSelectionAction( - id: 2, + id: .more, name: KDriveResourcesStrings.Localizable.buttonMenu, icon: KDriveResourcesAsset.menu ) static let deletePermanently = MultipleSelectionAction( - id: 3, + id: .deletePermanently, name: KDriveResourcesStrings.Localizable.buttonDelete, icon: KDriveResourcesAsset.delete ) + static let download = MultipleSelectionAction( + id: .download, + name: KDriveResourcesStrings.Localizable.buttonDownload, + icon: KDriveResourcesAsset.menu + ) } @MainActor @@ -67,6 +80,8 @@ class MultipleSelectionFileListViewModel { leftBarButtons = [.cancel] if configuration.selectAllSupported { rightBarButtons = [.selectAll] + } else { + rightBarButtons = [] } } else { leftBarButtons = nil @@ -107,7 +122,13 @@ 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 self.configuration = configuration @@ -161,7 +182,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 } @@ -201,7 +224,16 @@ 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, + fileId: publicShareProxy.fileId) + } else { + directoryCount = try await driveFileManager.apiFetcher.count(of: proxyCurrentDirectory) + } + selectedCount = directoryCount.count rightBarButtons = [.deselectAll] } catch { diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift index e6833993e..85b7ea577 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: @@ -58,6 +65,15 @@ extension FileActionsFloatingPanelViewController { } private func setupActions() { + guard !driveFileManager.isPublicShare else { + if file.isDirectory { + actions = FloatingPanelAction.publicShareFolderActions + } else { + actions = FloatingPanelAction.publicShareActions + } + return + } + actions = (file.isDirectory ? FloatingPanelAction.folderListActions : FloatingPanelAction.listActions).filter { action in switch action { case .openWith: @@ -149,6 +165,8 @@ extension FileActionsFloatingPanelViewController { leaveShareAction() case .cancelImport: cancelImportAction() + case .addToMyDrive: + addToMyDrive() default: break } @@ -179,7 +197,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) } } @@ -503,4 +522,35 @@ extension FileActionsFloatingPanelViewController { } } } + + private func addToMyDrive() { + guard accountManager.currentAccount != nil else { + dismiss(animated: true) { + self.router.showUpsaleFloatingPanel() + } + return + } + + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager, + let publicShareProxy = driveFileManager.publicShareProxy else { + return + } + + PublicShareAction().addToMyDrive( + publicShareProxy: publicShareProxy, + currentUserDriveFileManager: currentUserDriveFileManager, + selectedItemsIds: [file.id], + exceptItemIds: [], + 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/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift index 585eb0f13..8238f687d 100644 --- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift @@ -130,7 +130,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) @@ -178,7 +178,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) @@ -195,7 +197,15 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController { } } } - 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) + } } func copyShareLinkToPasteboard(from indexPath: IndexPath, link: String) { @@ -267,7 +277,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/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 0d398e815..aa934c43f 100644 --- a/kDrive/UI/Controller/Files/FilePresenter.swift +++ b/kDrive/UI/Controller/Files/FilePresenter.swift @@ -145,15 +145,36 @@ final class FilePresenter { let viewModel: FileListViewModel if driveFileManager.drive.sharedWithMe { viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file) + } else if let publicShareProxy = driveFileManager.publicShareProxy { + let configuration = FileListViewModel.Configuration(selectAllSupported: true, + rootTitle: nil, + emptyViewType: .emptyFolder, + supportsDrop: false, + rightBarButtons: [.downloadAll], + matomoViewPath: [ + MatomoUtils.Views.menu.displayName, + "publicShare" + ]) + + viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy, + sortType: .nameAZ, + driveFileManager: driveFileManager, + currentDirectory: file, + apiFetcher: PublicShareApiFetcher(), + configuration: configuration) } else if file.isTrashed || file.deletedAt != nil { viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } else { viewModel = ConcreteFileListViewModel(driveFileManager: driveFileManager, currentDirectory: file) } - let nextVC = FileListViewController(viewModel: viewModel) + 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 } @@ -173,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/FloatingPanelAction.swift b/kDrive/UI/Controller/Files/FloatingPanelAction.swift index 8268ee822..94e001245 100644 --- a/kDrive/UI/Controller/Files/FloatingPanelAction.swift +++ b/kDrive/UI/Controller/Files/FloatingPanelAction.swift @@ -52,6 +52,7 @@ public class FloatingPanelAction: Equatable { case shareAndRights case shareLink case upsaleColor + case addToMyDrive } init( @@ -188,6 +189,11 @@ public class FloatingPanelAction: Equatable { name: KDriveResourcesStrings.Localizable.buttonChangeFolderColor, image: KDriveResourcesAsset.colorBucket.image ) + static let addToMyDrive = FloatingPanelAction( + id: .addToMyDrive, + name: KDriveResourcesStrings.Localizable.buttonAddToKDrive, + image: KDriveResourcesAsset.drive.image + ) static var listActions: [FloatingPanelAction] { return [ @@ -225,6 +231,18 @@ public class FloatingPanelAction: Equatable { ].map { $0.reset() } } + static var publicShareActions: [FloatingPanelAction] { + return [openWith, sendCopy, download, addToMyDrive].map { $0.reset() } + } + + static var publicShareFolderActions: [FloatingPanelAction] { + return [download].map { $0.reset() } + } + + static var multipleSelectionPublicShareActions: [FloatingPanelAction] { + return [download].map { $0.reset() } + } + static var quickActions: [FloatingPanelAction] { return [informations, sendCopy, shareAndRights, shareLink].map { $0.reset() } } diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift index 951c30288..c79d1bf20 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController+Actions.swift @@ -100,53 +100,88 @@ 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 + } + + 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) } - DownloadQueue.instance.addToQueue(file: file, userId: accountManager.currentUserId) + } else { + 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, + 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() downloadArchivedFiles(downloadCellPath: indexPath) { 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 16bda77b8..3395892a0 100644 --- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift +++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift @@ -84,7 +84,9 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } func setupContent() { - if sharedWithMe { + if driveFileManager.isPublicShare { + actions = FloatingPanelAction.multipleSelectionPublicShareActions + } else if sharedWithMe { actions = FloatingPanelAction.multipleSelectionSharedWithMeActions } else if allItemsSelected { actions = FloatingPanelAction.selectAllActions @@ -140,7 +142,9 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } } - func downloadArchivedFiles(downloadCellPath: IndexPath, completion: @escaping (Result) -> Void) { + func downloadPublicShareArchivedFiles(downloadCellPath: IndexPath, + publicShareProxy: PublicShareProxy, + completion: @escaping (Result) -> Void) { Task { [proxyFiles = files.map { $0.proxify() }, currentProxyDirectory = currentDirectory.proxify()] in do { let archiveBody: ArchiveBody @@ -149,7 +153,44 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro } else { archiveBody = .init(files: proxyFiles) } - let response = try await driveFileManager.apiFetcher.buildArchive( + + let response = try await PublicShareApiFetcher().buildPublicShareArchive( + driveId: publicShareProxy.driveId, + linkUuid: publicShareProxy.shareLinkUid, + 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)) + } + } + DownloadQueue.instance.addPublicShareArchiveToQueue(archiveId: response.uuid, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + + self.collectionView.reloadItems(at: [downloadCellPath]) + } catch { + completion(.failure(error as? DriveError ?? .unknownError)) + } + } + } + + 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 ) @@ -163,9 +204,17 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro completion(.failure(error ?? .unknownError)) } } - DownloadQueue.instance.addToQueue(archiveId: response.uuid, - driveId: self.driveFileManager.drive.id, - userId: accountManager.currentUserId) + + 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)) @@ -210,6 +259,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/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 6064036a5..3b4f440ed 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -52,7 +52,11 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } } - private var currentDownloadOperation: DownloadOperation? + private var editButtonHidden: Bool { + driveFileManager.isPublicShare + } + + private var currentDownloadOperation: DownloadAuthenticatedOperation? private let pdfPageLabel = UILabel(frame: .zero) private var titleWidthConstraint: NSLayoutConstraint? private var titleHeightConstraint: 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) @@ -542,7 +547,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 +572,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?.task?.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, @@ -601,8 +631,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 @@ -642,7 +672,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) + } } } @@ -718,7 +752,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) } @@ -733,7 +767,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/kDrive/UI/Controller/Files/PublicShareAction.swift b/kDrive/UI/Controller/Files/PublicShareAction.swift new file mode 100644 index 000000000..2abc49ebf --- /dev/null +++ b/kDrive/UI/Controller/Files/PublicShareAction.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 kDriveCore +import UIKit + +struct PublicShareAction { + @MainActor func addToMyDrive( + publicShareProxy: PublicShareProxy, + currentUserDriveFileManager: DriveFileManager, + selectedItemsIds: [Int], + exceptItemIds: [Int], + onPresentViewController: (UIViewController, Bool) -> Void, + onSave: (() -> Void)?, + onDismissViewController: (() -> Void)? + ) { + let saveNavigationViewController = SaveFileViewController.instantiateInNavigationController( + driveFileManager: currentUserDriveFileManager, + publicShareProxy: publicShareProxy, + publicShareFileIds: selectedItemsIds, + publicShareExceptIds: exceptItemIds, + onSave: onSave, + onDismissViewController: onDismissViewController + ) + + onPresentViewController(saveNavigationViewController, true) + } +} 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..26f5c504f 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) @@ -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 22de58218..47f458c6f 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+FooterButtonDelegate.swift @@ -25,27 +25,63 @@ import UIKit extension SaveFileViewController: FooterButtonDelegate { @objc func didClickOnButton(_ sender: AnyObject) { - guard let drive = selectedDriveFileManager?.drive, + guard let selectedDriveFileManager, let directory = selectedDirectory else { return } - - // Making sure the user cannot spam the button on tasks that may take a while + let drive = selectedDriveFileManager.drive let button = sender as? IKLargeButton button?.setLoading(true) - let items = items - guard !items.isEmpty else { - dismiss(animated: true) + guard let publicShareProxy else { + guard !items.isEmpty else { + dismissViewController() + return + } + + Task { + await saveAndDismiss(files: items, directory: directory, drive: drive) + } return } Task { - await presentSnackBarSaveAndDismiss(files: items, directory: directory, drive: drive) + defer { + onSave?() + dismissViewController() + } + + try await savePublicShareToDrive(sourceDriveId: publicShareProxy.driveId, + destinationDriveId: drive.id, + destinationFolderId: directory.id, + fileIds: publicShareFileIds, + exceptIds: publicShareExceptIds, + sharelinkUuid: publicShareProxy.shareLinkUid, + driveFileManager: selectedDriveFileManager) } } - private func presentSnackBarSaveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { + private func savePublicShareToDrive(sourceDriveId: Int, + destinationDriveId: Int, + destinationFolderId: Int, + fileIds: [Int], + 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) + } + + private func dismissViewController() { + onDismissViewController?() + dismiss(animated: true) + } + + private func saveAndDismiss(files: [ImportedFile], directory: File, drive: Drive) async { let message: String do { try await processForUpload(files: files, directory: directory, drive: drive) @@ -57,6 +93,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+SelectFolderDelegate.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift index d776c0ce6..b76bd3872 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+SelectFolderDelegate.swift @@ -16,8 +16,8 @@ along with this program. If not, see . */ +import Foundation import kDriveCore -import UIKit extension SaveFileViewController: SelectFolderDelegate { func didSelectFolder(_ folder: File) { diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift index 284734cea..b5fd81129 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController+UITableViewDataSource.swift @@ -86,7 +86,9 @@ extension SaveFileViewController: UITableViewDataSource { fatalError("Not supported by this datasource") } } +} +extension SaveFileViewController { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { switch sections[section] { case .fileName: diff --git a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift index 7db6efa73..27443f711 100644 --- a/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift +++ b/kDrive/UI/Controller/Files/Save File/SaveFileViewController.swift @@ -71,6 +71,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 { @@ -110,9 +117,14 @@ class SaveFileViewController: UIViewController { } } + @MainActor var onDismissViewController: (() -> Void)? + @MainActor var onSave: (() -> Void)? + @IBOutlet var tableView: UITableView! @IBOutlet var closeBarButtonItem: UIBarButtonItem! + // MARK: View lifecycle + override func viewDidLoad() { super.viewDidLoad() @@ -138,7 +150,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() @@ -158,22 +170,24 @@ class SaveFileViewController: UIViewController { ) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - MatomoUtils.track(view: [MatomoUtils.Views.save.displayName, "SaveFile"]) - } - 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 @@ -199,6 +213,48 @@ class SaveFileViewController: UIViewController { } } + // MARK: Helpers + + func getBestDirectory() -> File? { + if lastSelectedDirectory?.driveId == selectedDriveFileManager?.drive.id { + return lastSelectedDirectory + } + + guard let selectedDriveFileManager else { return nil } + + let myFilesDirectory = selectedDriveFileManager.database.fetchResults(ofType: File.self) { lazyFiles in + lazyFiles.filter("rawVisibility = %@", FileVisibility.isPrivateSpace.rawValue) + }.first + + if let myFilesDirectory { + return myFilesDirectory.freezeIfNeeded() + } + + guard selectedDriveFileManager.drive.sharedWithMe else { return nil } + + let firstAvailableSharedDriveDirectory = selectedDriveFileManager.database.fetchResults(ofType: File.self) { lazyFiles in + lazyFiles.filter( + "rawVisibility = %@ AND driveId == %d", + FileVisibility.isInSharedSpace.rawValue, + selectedDriveFileManager.drive.id + ) + }.first + return firstAvailableSharedDriveDirectory?.freezeIfNeeded() + } + + func dismiss(animated: Bool, clean: Bool = true, completion: (() -> Void)? = nil) { + Task { + // Cleanup file that were duplicated to appGroup on extension mode + if appContextService.isExtension && clean { + await items.concurrentForEach { item in + try? FileManager.default.removeItem(at: item.path) + } + } + + navigationController?.dismiss(animated: animated, completion: completion) + } + } + func setAssetIdentifiers() { guard let assetIdentifiers else { return } sections = [.importing] @@ -237,48 +293,22 @@ class SaveFileViewController: UIViewController { } func updateButton() { - enableButton = selectedDirectory != nil && items.allSatisfy { !$0.name.isEmpty } && !items.isEmpty && !importInProgress - } - - func getBestDirectory() -> File? { - if lastSelectedDirectory?.driveId == selectedDriveFileManager?.drive.id { - return lastSelectedDirectory + guard selectedDirectory != nil, !importInProgress else { + enableButton = false + return } - guard let selectedDriveFileManager else { return nil } - - let myFilesDirectory = selectedDriveFileManager.database.fetchResults(ofType: File.self) { lazyFiles in - lazyFiles.filter("rawVisibility = %@", FileVisibility.isPrivateSpace.rawValue) - }.first - - if let myFilesDirectory { - return myFilesDirectory.freezeIfNeeded() + guard !isPublicShareFiles else { + enableButton = true + return } - // 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 - lazyFiles.filter( - "rawVisibility = %@ AND driveId == %d", - FileVisibility.isInSharedSpace.rawValue, - selectedDriveFileManager.drive.id - ) - }.first - return firstAvailableSharedDriveDirectory?.freezeIfNeeded() - } - - func dismiss(animated: Bool, clean: Bool = true, completion: (() -> Void)? = nil) { - Task { - // Cleanup file that were duplicated to appGroup on extension mode - if appContextService.isExtension && clean { - await items.concurrentForEach { item in - try? FileManager.default.removeItem(at: item.path) - } - } - - navigationController?.dismiss(animated: animated, completion: completion) + guard !items.isEmpty, + items.allSatisfy({ !$0.name.isEmpty }) else { + enableButton = false + return } + enableButton = true } private func updateTableViewAfterImport() { @@ -311,6 +341,8 @@ class SaveFileViewController: UIViewController { } } + // MARK: Class methods + class func instantiate(driveFileManager: DriveFileManager?) -> SaveFileViewController { let viewController = Storyboard.saveFile .instantiateViewController(withIdentifier: "SaveFileViewController") as! SaveFileViewController @@ -318,13 +350,36 @@ class SaveFileViewController: UIViewController { return viewController } + class func instantiateInNavigationController(driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + publicShareFileIds: [Int], + publicShareExceptIds: [Int], + onSave: (() -> Void)?, + onDismissViewController: (() -> Void)?) + -> TitleSizeAdjustingNavigationController { + let saveViewController = instantiate(driveFileManager: driveFileManager) + + saveViewController.publicShareFileIds = publicShareFileIds + saveViewController.publicShareExceptIds = publicShareExceptIds + saveViewController.publicShareProxy = publicShareProxy + saveViewController.onSave = onSave + saveViewController.onDismissViewController = onDismissViewController + + return wrapInNavigationController(saveViewController) + } + class func instantiateInNavigationController(driveFileManager: DriveFileManager?, files: [ImportedFile]? = nil) -> TitleSizeAdjustingNavigationController { let saveViewController = instantiate(driveFileManager: driveFileManager) if let files { saveViewController.items = files } - let navigationController = TitleSizeAdjustingNavigationController(rootViewController: saveViewController) + + return wrapInNavigationController(saveViewController) + } + + private class func wrapInNavigationController(_ viewController: UIViewController) -> TitleSizeAdjustingNavigationController { + let navigationController = TitleSizeAdjustingNavigationController(rootViewController: viewController) navigationController.navigationBar.prefersLargeTitles = true return navigationController } diff --git a/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift b/kDrive/UI/Controller/Files/Save File/SelectFolderViewController.swift index cec483a03..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.listFloatingButtonPaddingBottom, 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/Files/Search/SearchFilesViewModel.swift b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift index 7b1a77ccf..32d1acab3 100644 --- a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift +++ b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift @@ -155,7 +155,7 @@ class SearchFilesViewModel: FileListViewModel { startObservation() } - override func barButtonPressed(type: FileListBarButtonType) { + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { if type == .searchFilters { let navigationController = SearchFiltersViewController .instantiateInNavigationController(driveFileManager: driveFileManager) @@ -164,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/Home/HomeViewController.swift b/kDrive/UI/Controller/Home/HomeViewController.swift index c8cd24c9a..686e579f9 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() @@ -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), diff --git a/kDrive/UI/Controller/LoginDelegateHandler.swift b/kDrive/UI/Controller/LoginDelegateHandler.swift index 99e39567d..44f820e68 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,7 @@ public final class LoginDelegateHandler: InfomaniakLoginDelegate { UserDefaults.shared.legacyIsFirstLaunch = false UserDefaults.shared.numberOfConnections = 1 _ = router.showMainViewController(driveFileManager: driveFileManager, selectedIndex: nil) + deeplinkService.processDeeplinksPostAuthentication() } private func didCompleteLoginWithError(_ error: Error, 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/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 new file mode 100644 index 000000000..38bd20f91 --- /dev/null +++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift @@ -0,0 +1,204 @@ +/* + 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 InfomaniakCore +import InfomaniakDI +import kDriveCore +import RealmSwift +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 + @LazyInjectService private var deeplinkService: DeeplinkServiceable + + private var downloadObserver: ObservationToken? + + var publicShareProxy: PublicShareProxy? + let rootProxy: ProxyFile + var publicShareApiFetcher: PublicShareApiFetcher? + + override init(configuration: Configuration, driveFileManager: DriveFileManager, currentDirectory: File) { + rootProxy = currentDirectory.proxify() + super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) + observedFiles = AnyRealmCollection(currentDirectory.children) + } + + convenience init( + publicShareProxy: PublicShareProxy, + sortType: SortType, + driveFileManager: DriveFileManager, + currentDirectory: File, + apiFetcher: PublicShareApiFetcher, + configuration: Configuration + ) { + self.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: currentDirectory) + + self.publicShareProxy = publicShareProxy + self.sortType = sortType + publicShareApiFetcher = apiFetcher + } + + required init(driveFileManager: DriveFileManager, currentDirectory: File?) { + fatalError("Use init(publicShareProxy:… ) instead") + } + + override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws { + 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, + cursor: cursor, + publicShareApiFetcher: publicShareApiFetcher) + endRefreshing() + if let nextCursor { + try await loadFiles(cursor: nextCursor) + } + } + + override func barButtonPressed(sender: Any?, type: FileListBarButtonType) { + 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) { + deeplinkService.clearLastPublicShare() + onDismissViewController?() + } else { + super.barButtonPressed(sender: sender, type: type) + } + } + + 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 = [.downloadingAll] + 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 { + return + } + + defer { + self.clearDownloadObserver() + } + + guard error == nil else { + UIConstants.showSnackBarIfNeeded(error: DriveError.downloadFailed) + return + } + + // present share sheet + let activityViewController = UIActivityViewController( + activityItems: [self.currentDirectory.localUrl], + applicationActivities: nil + ) + + 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") + } + + self.onPresentViewController?(.modal, activityViewController, true) + } + } + + DownloadQueue.instance.addPublicShareToQueue(file: currentDirectory, + driveFileManager: driveFileManager, + publicShareProxy: publicShareProxy) + } + + private func clearDownloadObserver() { + downloadObserver?.cancel() + downloadObserver = nil + } + + private func addToMyDrive(sender: Any?, publicShareProxy: PublicShareProxy) { + guard accountManager.currentAccount != nil else { + router.showUpsaleFloatingPanel() + return + } + + guard let currentUserDriveFileManager = accountManager.currentDriveFileManager else { + return + } + + 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, + selectedItemsIds: selectedItemsIds, + exceptItemIds: exceptItemIds, + onPresentViewController: { saveNavigationViewController, animated in + onPresentViewController?(.modal, saveNavigationViewController, animated) + }, + onSave: { + MatomoUtils.trackAddBulkToMykDrive() + }, + onDismissViewController: { [weak self] in + guard let self else { return } + self.onDismissViewController?() + self.multipleSelectionViewModel?.isMultipleSelectionEnabled = false + } + ) + } +} diff --git a/kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift b/kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift similarity index 100% rename from kDrive/UI/Controller/Menu/SharedWithMe/SharedWithMeViewModel.swift rename to kDrive/UI/Controller/Menu/Share/SharedWithMeViewModel.swift 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/Controller/Menu/Trash/TrashListViewModel.swift b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift index f836588e3..4bb9a12e0 100644 --- a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift +++ b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift @@ -93,7 +93,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, @@ -104,7 +104,7 @@ class TrashListViewModel: InMemoryFileListViewModel { } onPresentViewController?(.modal, alert, true) } else { - super.barButtonPressed(type: type) + super.barButtonPressed(sender: sender, type: type) } } diff --git a/kDrive/UI/Controller/OnboardingViewController.swift b/kDrive/UI/Controller/OnboardingViewController.swift index 974f7ddc4..3a09a7870 100644 --- a/kDrive/UI/Controller/OnboardingViewController.swift +++ b/kDrive/UI/Controller/OnboardingViewController.swift @@ -26,6 +26,12 @@ import Lottie import UIKit class OnboardingViewController: UIViewController { + @LazyInjectService private var appNavigable: AppNavigable + @LazyInjectService private var accountManager: AccountManageable + @LazyInjectService private var infomaniakLogin: InfomaniakLoginable + + private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid + private lazy var loginDelegateHandler: LoginDelegateHandler = { let loginDelegateHandler = LoginDelegateHandler() loginDelegateHandler.didStartLoginCallback = { [weak self] in @@ -61,15 +67,9 @@ class OnboardingViewController: UIViewController { @IBOutlet var nextButtonHeight: NSLayoutConstraint! @IBOutlet var registerButtonHeight: NSLayoutConstraint! - @LazyInjectService var accountManager: AccountManageable - @LazyInjectService var infomaniakLogin: InfomaniakLoginable - @LazyInjectService var appNavigable: AppNavigable - var addUser = false var slides: [Slide] = [] - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - override func viewDidLoad() { super.viewDidLoad() infomaniakLogin.setupWebviewNavbar(title: "", @@ -146,19 +146,15 @@ class OnboardingViewController: UIViewController { } @IBAction func signInButtonPressed(_ sender: Any) { - MatomoUtils.track(eventWithCategory: .account, name: "openLoginWebview") backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "Login WebView") { [weak self] in SentryDebug.capture(message: "Background task expired while logging in") self?.endBackgroundTask() } - infomaniakLogin.webviewLoginFrom(viewController: self, - hideCreateAccountButton: true, - delegate: loginDelegateHandler) + appNavigable.showLogin(delegate: loginDelegateHandler) } @IBAction func registerButtonPressed(_ sender: Any) { - MatomoUtils.track(eventWithCategory: .account, name: "openCreationWebview") - present(RegisterViewController.instantiateInNavigationController(delegate: loginDelegateHandler), animated: true) + appNavigable.showRegister(delegate: loginDelegateHandler) } @IBAction func closeButtonPressed(_ sender: Any) { diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift index 6ca50d051..1d59c9f45 100644 --- a/kDrive/UI/View/Files/FileCollectionViewCell.swift +++ b/kDrive/UI/View/Files/FileCollectionViewCell.swift @@ -48,6 +48,10 @@ protocol FileCellDelegate: AnyObject { var file: File var selectionMode: Bool var isSelected = false + + /// Public share data if file exists within a public share + let publicShareProxy: PublicShareProxy? + private var downloadProgressObserver: ObservationToken? private var downloadObserver: ObservationToken? var thumbnailDownloadTask: Kingfisher.DownloadTask? @@ -114,6 +118,7 @@ protocol FileCellDelegate: AnyObject { init(driveFileManager: DriveFileManager, file: File, selectionMode: Bool) { self.file = file self.selectionMode = selectionMode + publicShareProxy = driveFileManager.publicShareProxy categories = driveFileManager.drive.categories(for: file) } @@ -139,30 +144,50 @@ 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.cornerRadius = UIConstants.Image.cornerRadius 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 publicShareProxy { + // Fetch public share thumbnail + thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid, + publicDriveId: publicShareProxy.driveId, + publicFileId: file.id) { [ + requestFileId = file.id, + weak self + ] image, _ in + self?.setImage(image, on: imageView, requestFileId: requestFileId) } - if file.id == requestFileId { - imageView.image = image - imageView.backgroundColor = nil + } else { + // Fetch thumbnail + thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in + 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() @@ -303,7 +328,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 @@ -322,7 +347,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. @@ -334,18 +364,20 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell { } func configureForSelection() { - guard viewModel?.selectionMode == true else { return } + guard let viewModel, + viewModel.selectionMode 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..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 { @@ -110,7 +123,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/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift index b3ddb11de..e45b62566 100644 --- a/kDrive/UI/View/Files/FileListBarButton.swift +++ b/kDrive/UI/View/Files/FileListBarButton.swift @@ -49,7 +49,30 @@ 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 + case .downloadingAll: + self.init(title: nil, style: .plain, target: target, action: action) + + let activityView = UIActivityIndicatorView(style: .medium) + activityView.startAnimating() + + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(cancelDownloadPressed)) + activityView.addGestureRecognizer(tapGestureRecognizer) + + customView = activityView + case .addToMyDrive: + let image = KDriveResourcesAsset.drive.image + self.init(image: image, style: .plain, target: target, action: action) + accessibilityLabel = KDriveResourcesStrings.Localizable.buttonAddToKDrive } self.type = type } + + @objc private func cancelDownloadPressed() { + guard let targetObject = target as? NSObject, let action else { return } + targetObject.perform(action, with: self) + } } 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/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..25fa2d028 --- /dev/null +++ b/kDrive/UI/View/Upsale/NoDriveUpsaleViewController.swift @@ -0,0 +1,46 @@ +/* + 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 { + var onDismissViewController: (() -> Void)? + + 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) + onDismissViewController?() + } +} 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..cc3e5458a --- /dev/null +++ b/kDrive/UI/View/Upsale/UpsaleViewController.swift @@ -0,0 +1,258 @@ +/* + 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 InfomaniakDI +import kDriveCore +import kDriveResources +import UIKit + +public class UpsaleViewController: UIViewController { + var onLoginCompleted: (() -> Void)? + var onFreeTrialCompleted: (() -> Void)? + + 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() + + MatomoUtils.trackUpsalePresented() + } + + 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() { + dismiss(animated: true, completion: nil) + onFreeTrialCompleted?() + } + + @objc public func login() { + dismiss(animated: true, completion: nil) + onLoginCompleted?() + } + + public static func instantiateInFloatingPanel(rootViewController: UIViewController) -> UIViewController { + let upsaleViewController = UpsaleViewController() + + upsaleViewController.onFreeTrialCompleted = { [weak rootViewController] in + guard let rootViewController else { return } + rootViewController.dismiss(animated: true) { + let loginDelegateHandler = LoginDelegateHandler() + @InjectService var router: AppNavigable + router.showRegister(delegate: loginDelegateHandler) + } + } + + upsaleViewController.onLoginCompleted = { [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) + } +} 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 deleted file mode 100644 index 50b68adf5..000000000 --- a/kDrive/Utils/UniversalLinksHelper.swift +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 CocoaLumberjackSwift -import Foundation -import InfomaniakDI -import kDriveCore -import kDriveResources -import SwiftRegex -import UIKit - -#if !ISEXTENSION -enum UniversalLinksHelper { - private struct Link { - let regex: Regex - let displayMode: DisplayMode - - /// Matches a private share link - static let privateShareLink = Link( - regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-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] - } - - private enum DisplayMode { - case office, file - } - - static func handlePath(_ path: String) -> Bool { - DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") - - for link in Link.all { - let matches = link.regex.matches(in: path) - if processRegex(matches: matches, displayMode: link.displayMode) { - return true - } - } - - DDLogWarn("[UniversalLinksHelper] Unable to process link with path: \(path)") - return false - } - - private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool { - @InjectService var accountManager: AccountManageable - - guard let firstMatch = matches.first, - firstMatch.count > 2, - let driveId = Int(firstMatch[1]), - let last = firstMatch.last, - let uploadFileId = Int(last), - let driveFileManager = accountManager.getDriveFileManager(for: driveId, - userId: accountManager.currentUserId) - else { return false } - - openFile(id: uploadFileId, driveFileManager: driveFileManager, office: displayMode == .office) - - return true - } - - private static func openFile(id: Int, driveFileManager: DriveFileManager, office: Bool) { - Task { - do { - let file = try await driveFileManager.file(id: id) - @InjectService var appNavigable: AppNavigable - await appNavigable.present(file: file, driveFileManager: driveFileManager, office: office) - } catch { - DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") - await UIConstants.showSnackBarIfNeeded(error: error) - } - } - } -} -#endif 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/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift index e0bd01b47..ef4c17838 100644 --- a/kDriveCore/Data/Api/DriveApiFetcher.swift +++ b/kDriveCore/Data/Api/DriveApiFetcher.swift @@ -52,6 +52,54 @@ public class AuthenticatedImageRequestModifier: ImageDownloadRequestModifier { } } +public struct PublicShareMetadata: Decodable { + public let url: URL + public let fileId: Int + public let right: String + + 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 + case right + 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) + + 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)") + } + } +} + public class DriveApiFetcher: ApiFetcher { @LazyInjectService var accountManager: AccountManageable @LazyInjectService var tokenable: InfomaniakNetworkLoginable @@ -501,6 +549,48 @@ 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, + 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 = [ + PublicShareAPIParameters.sourceDriveId: sourceDriveId, + PublicShareAPIParameters.destinationFolderId: destinationFolderId, + PublicShareAPIParameters.sharelinkUuid: sharelinkUuid + ] + + if let fileIds, !fileIds.isEmpty { + requestParameters[PublicShareAPIParameters.fileIds] = fileIds + } else if let exceptIds, !exceptIds.isEmpty { + requestParameters[PublicShareAPIParameters.exceptFileIds] = exceptIds + } + + if let password { + requestParameters[PublicShareAPIParameters.password] = password + } + + let result: ValidServerResponse = try await perform(request: authenticatedRequest( + importShareLinkFiles, + method: .post, + parameters: requestParameters + )) + return result + } +} + +enum PublicShareAPIParameters { + static let sourceDriveId = "source_drive_id" + 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 { diff --git a/kDriveCore/Data/Api/Endpoint+Files.swift b/kDriveCore/Data/Api/Endpoint+Files.swift new file mode 100644 index 000000000..e7bd35552 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Files.swift @@ -0,0 +1,314 @@ +/* + 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, + publicShareProxy: PublicShareProxy? = nil, + as asType: String? = nil) -> Endpoint { + let queryItems: [URLQueryItem]? + if let asType { + queryItems = [URLQueryItem(name: "as", value: asType)] + } else { + queryItems = nil + } + 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 { + 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..a801db5fd --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Share.swift @@ -0,0 +1,109 @@ +/* + 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 { + /// 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 + static func shareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + shareUrlV3.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + static func shareLinkFileV2(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)") + } + + /// Share link file children + 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/\(fileId)/files") + return fileChildrenEndpoint.appending(path: "", queryItems: shareLinkQueryItems) + } + + /// Share link file thumbnail + static func shareLinkFileThumbnail(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint { + return shareLinkFileV2(driveId: driveId, linkUuid: linkUuid, fileId: fileId).appending(path: "/thumbnail") + } + + /// 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 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") + } + + /// 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") + } + + /// Count files of a public share folder + 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 { + return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)") + } + + static func importShareLinkFiles(destinationDrive: AbstractDrive) -> Endpoint { + return Endpoint.driveInfoV2(drive: destinationDrive).appending(path: "/imports/sharelink") + } +} diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift new file mode 100644 index 000000000..17e778a22 --- /dev/null +++ b/kDriveCore/Data/Api/Endpoint+Trash.swift @@ -0,0 +1,74 @@ +/* + 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 { + private static let trashPath = "/trash" + + private static let countPath = "/count" + + static func trash(drive: AbstractDrive) -> Endpoint { + return .driveInfo(drive: drive).appending(path: trashPath, queryItems: [FileWith.fileMinimal.toQueryItem()]) + } + + static func trashV2(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: trashPath) + } + + static func emptyTrash(drive: AbstractDrive) -> Endpoint { + return .driveInfoV2(drive: drive).appending(path: trashPath) + } + + static func trashCount(drive: AbstractDrive) -> Endpoint { + return .trash(drive: drive).appending(path: countPath) + } + + 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: countPath) + } +} diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift index dd93b038a..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,63 +345,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 { - 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 diff --git a/kDriveCore/Data/Api/PublicShareApiFetcher.swift b/kDriveCore/Data/Api/PublicShareApiFetcher.swift new file mode 100644 index 000000000..b97c52e47 --- /dev/null +++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift @@ -0,0 +1,112 @@ +/* + 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 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 = "link_is_not_valid" +} + +public class PublicShareApiFetcher: ApiFetcher { + override public init() { + super.init() + } + + /// 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(200 ... 500) + + 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) + + do { + let metadata: PublicShareMetadata = try await perform(request: request) + return metadata + } catch InfomaniakError.apiError(let apiError) { + throw apiError + } + } + + 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 + ] + let request = Session.default.request(shareLinkFileUrl, parameters: requestParameters) + let shareLinkFile: File = try await perform(request: request) + return shareLinkFile + } + + /// Query a specific page + 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: rootFolderId, + 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 + } + + 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 + } + + 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 + } +} diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift index 01d0e6367..dd78d4f83 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) } @@ -55,6 +68,9 @@ 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, driveId: Int, rootFileId: Int) -> DriveFileManager? func getApiFetcher(for userId: Int, token: ApiToken) -> DriveApiFetcher func getTokenForUserId(_ id: Int) -> ApiToken? func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) @@ -82,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" @@ -185,6 +202,29 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { } } + public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager? { + if let inMemoryDriveFileManager = driveFileManagers[publicShareId] { + return inMemoryDriveFileManager + } + + // 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 { + DDLogError("Failed to store public share drive in base, \(error)") + return nil + } + + let frozenPublicShareDrive = publicShareDrive.freeze() + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId) + let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy) + + return DriveFileManager(drive: frozenPublicShareDrive, apiFetcher: DriveApiFetcher(), context: context) + } + public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager { let userDrives = driveInfosManager.getDrives(for: userId) @@ -507,6 +547,8 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable { public func logoutCurrentAccountAndSwitchToNextIfPossible() { Task { @MainActor in + deeplinkService.clearLastPublicShare() + if let currentAccount { removeTokenAndAccount(account: currentAccount) } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift index 8133f6d0b..bf1be04df 100644 --- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift +++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager+Transactionable.swift @@ -29,7 +29,10 @@ public enum DriveFileManagerContext { /// Dedicated dataset to store shared with me case sharedWithMe - func realmURL(driveId: Int, driveUserId: Int) -> URL { + /// Dedicated in memory dataset for a public share link + case publicShare(shareProxy: PublicShareProxy) + + func realmURL(driveId: Int, driveUserId: Int) -> URL? { switch self { case .drive: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId).realm") @@ -37,6 +40,9 @@ public enum DriveFileManagerContext { return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-shared.realm") case .fileProvider: return DriveFileManager.constants.realmRootURL.appendingPathComponent("\(driveUserId)-\(driveId)-fp.realm") + case .publicShare: + // Public share are stored in memory only + return nil } } } diff --git a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManager.swift index 0c773087a..e8aee3859 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 { @@ -85,11 +100,23 @@ 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) + + let inMemoryIdentifier: String? + if case .publicShare(let 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 @@ -197,9 +224,28 @@ public final class DriveFileManager { ) } + public var isPublicShare: Bool { + switch context { + case .publicShare: + return true + default: + return false + } + } + + public var publicShareProxy: PublicShareProxy? { + switch context { + case .publicShare(let shareProxy): + return shareProxy + 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) @@ -389,6 +435,29 @@ 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( + rootFolderId: rootProxy.id, + publicShareProxy: publicShareProxy, + sortType: sortType, + cursor: cursor + ) + 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/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 { 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/DownloadQueue/BackgroundDownloadSessionManager.swift b/kDriveCore/Data/DownloadQueue/BackgroundDownloadSessionManager.swift index fbaaee339..19cdbf1c1 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)? @@ -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() @@ -167,7 +167,10 @@ 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/DownloadArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift similarity index 71% rename from kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift rename to kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift index a49585b40..bce52ad19 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadArchiveOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadArchiveOperation.swift @@ -16,60 +16,30 @@ along with this program. If not, see . */ +import Alamofire import CocoaLumberjackSwift import FileProvider import Foundation import InfomaniakCore import InfomaniakDI -public class DownloadArchiveOperation: Operation { +public class DownloadArchiveOperation: DownloadOperation, @unchecked Sendable { // MARK: - Attributes - @LazyInjectService var accountManager: AccountManageable - @LazyInjectService var appContextService: AppContextServiceable - - private let archiveId: String private let driveFileManager: DriveFileManager - private let urlSession: FileDownloadSession - private var progressObservation: NSKeyValueObservation? - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - - 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 - } + let archiveId: String + let shareDrive: AbstractDrive + let urlSession: FileDownloadSession - override public var isAsynchronous: Bool { - return true - } + public var archiveUrl: URL? - public init(archiveId: String, driveFileManager: DriveFileManager, urlSession: FileDownloadSession) { + public init(archiveId: String, + shareDrive: AbstractDrive, + driveFileManager: DriveFileManager, + urlSession: FileDownloadSession) { self.archiveId = archiveId + self.shareDrive = shareDrive self.driveFileManager = driveFileManager self.urlSession = urlSession } @@ -98,11 +68,15 @@ public class DownloadArchiveOperation: Operation { } // If the operation is not canceled, begin executing the task - _executing = true + operationExecuting = true main() } override public func main() { + authenticatedDownload() + } + + func authenticatedDownload() { DDLogInfo("[DownloadOperation] Downloading Archive of files \(archiveId) with session \(urlSession.identifier)") let url = Endpoint.getArchive(drive: driveFileManager.drive, uuid: archiveId).url @@ -112,14 +86,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) @@ -131,6 +98,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 @@ -174,20 +152,8 @@ public class DownloadArchiveOperation: Operation { 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.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift similarity index 75% rename from kDriveCore/Data/DownloadQueue/DownloadOperation.swift rename to kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift index 47f9a226e..ccb4e6a1b 100644 --- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadAuthenticatedOperation.swift @@ -24,69 +24,32 @@ 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 DownloadOperation: Operation, DownloadOperationable { +public class DownloadAuthenticatedOperation: DownloadOperation, DownloadFileOperationable, @unchecked Sendable { // MARK: - Attributes private let fileManager = FileManager.default - private let driveFileManager: DriveFileManager - private let urlSession: FileDownloadSession private let itemIdentifier: NSFileProviderItemIdentifier? - private var progressObservation: NSKeyValueObservation? - private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid - @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) private var uploadsDatabase: Transactionable - - @LazyInjectService var accountManager: AccountManageable + @LazyInjectService(customTypeIdentifier: kDriveDBID.uploads) var uploadsDatabase: Transactionable @LazyInjectService var driveInfosManager: DriveInfosManager @LazyInjectService var downloadManager: BackgroundDownloadSessionManager - @LazyInjectService var appContextService: AppContextServiceable + + let urlSession: FileDownloadSession + let driveFileManager: DriveFileManager 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( @@ -101,12 +64,15 @@ public class DownloadOperation: Operation, DownloadOperationable { 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 itemIdentifier = nil + super.init(task: task) } override public func start() { @@ -150,7 +116,7 @@ public class DownloadOperation: Operation, DownloadOperationable { } // If the operation is not canceled, begin executing the task - _executing = true + operationExecuting = true main() } @@ -170,6 +136,12 @@ public class DownloadOperation: Operation, DownloadOperationable { } override public func main() { + DDLogInfo("[DownloadOperation] Start for \(file.id) with session \(urlSession.identifier)") + + downloadFile() + } + + private func downloadFile() { DDLogInfo("[DownloadOperation] Downloading \(file.id) with session \(urlSession.identifier)") let url = Endpoint.download(file: file).url @@ -191,31 +163,29 @@ 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 = .localError // Other error? + error = .unknownToken // Other error? end(sessionUrl: url) } } - override public func cancel() { - DDLogInfo("[DownloadOperation] Download of \(file.id) canceled") - super.cancel() - task?.cancel() + 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() } // MARK: - methods @@ -275,12 +245,7 @@ public class DownloadOperation: Operation, DownloadOperationable { 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 @@ -288,7 +253,10 @@ 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:\(String(describing: error))" + ) try? uploadsDatabase.writeTransaction { writableRealm in guard let task = writableRealm.objects(DownloadTask.self) diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift new file mode 100644 index 000000000..12e17760d --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadOperation.swift @@ -0,0 +1,84 @@ +/* + 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? + + public var progress: Progress? { + task?.progress + } + + init(task: URLSessionDownloadTask? = nil) { + self.task = task + } + + var operationExecuting = false { + willSet { + willChangeValue(forKey: "isExecuting") + } + didSet { + didChangeValue(forKey: "isExecuting") + } + } + + var operationFinished = false { + willSet { + willChangeValue(forKey: "isFinished") + } + didSet { + didChangeValue(forKey: "isFinished") + } + } + + override public var isExecuting: Bool { + return operationExecuting + } + + override public var isFinished: Bool { + return operationFinished + } + + 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) + } + + operationExecuting = false + operationFinished = true + } +} diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.swift new file mode 100644 index 000000000..b280adf58 --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareArchiveOperation.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 CocoaLumberjackSwift +import FileProvider +import Foundation +import InfomaniakCore +import InfomaniakDI + +public final class DownloadPublicShareArchiveOperation: DownloadArchiveOperation, @unchecked Sendable { + 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) + downloadRequest(request) + } +} diff --git a/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift new file mode 100644 index 000000000..f058176e7 --- /dev/null +++ b/kDriveCore/Data/DownloadQueue/DownloadOperation/DownloadPublicShareOperation.swift @@ -0,0 +1,81 @@ +/* + 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: DownloadAuthenticatedOperation, @unchecked Sendable { + 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) + downloadRequest(request) + } +} diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift index 647cc5685..57047d578 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() @@ -112,6 +112,44 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { // MARK: - Public methods + public func addPublicShareToQueue(file: File, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy, + itemIdentifier: NSFileProviderItemIdentifier? = nil, + onOperationCreated: ((DownloadPublicShareOperation?) -> Void)? = nil, + completion: ((DriveError?) -> Void)? = 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 = DownloadPublicShareOperation( + file: file, + driveFileManager: driveFileManager, + urlSession: self.bestSession, + publicShareProxy: publicShareProxy, + itemIdentifier: itemIdentifier + ) + operation.completionBlock = { + self.dispatchQueue.async { + 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) + } + } + public func addToQueue(file: File, userId: Int, itemIdentifier: NSFileProviderItemIdentifier? = nil) { @@ -136,7 +174,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { OperationQueueHelper.disableIdleTimer(true) - let operation = DownloadOperation( + let operation = DownloadAuthenticatedOperation( file: file, driveFileManager: driveFileManager, urlSession: self.bestSession, @@ -144,13 +182,41 @@ 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.fileOperationsInQueue[file.id] = operation + } + } + + public func addPublicShareArchiveToQueue(archiveId: String, + driveFileManager: DriveFileManager, + publicShareProxy: PublicShareProxy) { + Log.downloadQueue("addPublicShareArchiveToQueue archiveId:\(archiveId)") + dispatchQueue.async { + OperationQueueHelper.disableIdleTimer(true) + + let operation = DownloadPublicShareArchiveOperation( + archiveId: archiveId, + shareDrive: publicShareProxy.proxyDrive, + 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.fileOperationsInQueue.isEmpty) } } + self.operationQueue.addOperation(operation) - self.operationsInQueue[file.id] = operation + self.archiveOperationsInQueue[archiveId] = operation } } @@ -166,6 +232,7 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { let operation = DownloadArchiveOperation( archiveId: archiveId, + shareDrive: drive, driveFileManager: driveFileManager, urlSession: self.bestSession ) @@ -173,7 +240,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) @@ -183,7 +250,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) { [ @@ -200,16 +267,20 @@ 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) - 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) } } @@ -229,12 +300,28 @@ public final class DownloadQueue: ParallelismHeuristicDelegate { operationQueue.cancelAllOperations() } - /// Check if a file is been uploaded - /// - /// Thread safe - /// Lookup O(1) as Dictionary backed - public func operation(for fileId: Int) -> DownloadOperation? { - return operationsInQueue[fileId] + 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 { diff --git a/kDriveCore/Data/Models/Drive/Drive.swift b/kDriveCore/Data/Models/Drive/Drive.swift index 5a67e138b..468788fa9 100644 --- a/kDriveCore/Data/Models/Drive/Drive.swift +++ b/kDriveCore/Data/Models/Drive/Drive.swift @@ -190,8 +190,6 @@ public final class Drive: Object, Codable { fileCategoriesIds = file.categories.sorted { $0.addedAt.compare($1.addedAt) == .orderedAscending }.map(\.categoryId) } let filteredCategories = categories.filter("id IN %@", fileCategoriesIds) - - // Sort the categories return fileCategoriesIds.compactMap { id in filteredCategories.first { $0.id == id } } } diff --git a/kDriveCore/Data/Models/File+Image.swift b/kDriveCore/Data/Models/File+Image.swift index 87fab6c8e..e7a07e627 100644 --- a/kDriveCore/Data/Models/File+Image.swift +++ b/kDriveCore/Data/Models/File+Image.swift @@ -16,10 +16,41 @@ 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) 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 { @@ -40,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) + } + } } } diff --git a/kDriveCore/Data/Models/File.swift b/kDriveCore/Data/Models/File.swift index 2f6357623..ecf725049 100644 --- a/kDriveCore/Data/Models/File.swift +++ b/kDriveCore/Data/Models/File.swift @@ -183,6 +183,23 @@ public enum ConvertedType: String, CaseIterable { public static let documentTypes: Set = [.presentation, .spreadsheet, .text] } +/// Minimal data needed to query a PublicShare +public struct PublicShareProxy { + public let driveId: Int + public let fileId: Int + public let shareLinkUid: String + + public init(driveId: Int, fileId: Int, shareLinkUid: String) { + self.driveId = driveId + self.fileId = fileId + self.shareLinkUid = shareLinkUid + } + + public var proxyDrive: ProxyDrive { + ProxyDrive(id: driveId) + } +} + public enum SortType: String { case nameAZ case nameZA @@ -541,14 +558,30 @@ public final class File: Object, Codable { public var isDownloaded: Bool { let localPath = localUrl.path - guard fileManager.fileExists(atPath: localPath) else { + let temporaryPath = temporaryUrl.path + + 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 { diff --git a/kDriveCore/Data/Models/Rights.swift b/kDriveCore/Data/Models/Rights.swift index 0f5271b76..3992bffa0 100644 --- a/kDriveCore/Data/Models/Rights.swift +++ b/kDriveCore/Data/Models/Rights.swift @@ -46,7 +46,8 @@ public class Rights: EmbeddedObject, Codable { /// Right to color a folder @Persisted public var canColor: Bool - // Directory capabilities + // MARK: Directory capabilities + /// Right to add new child directory @Persisted public var canCreateDirectory: Bool /// Right to add new child file @@ -58,6 +59,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 @@ -75,27 +91,39 @@ public class Rights: EmbeddedObject, Codable { case canUpload case canMoveInto case canBecomeDropbox + case canEdit + case canSeeStats + case canSeeInfo + case canDownload + case canComment + case canRequestAccess case canColor = "colorable" } 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 canColor = try container.decodeIfPresent(Bool.self, forKey: .canColor) ?? false } 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..30ff3a315 100644 --- a/kDriveCore/UI/UIConstants.swift +++ b/kDriveCore/UI/UIConstants.swift @@ -25,6 +25,40 @@ 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 publicSharePaddingBottom = 90.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 +66,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 +91,7 @@ public enum UIConstants { @discardableResult @MainActor - public static func showCancelableSnackBar( + static func showCancelableSnackBar( message: String, cancelSuccessMessage: String, duration: SnackBar.Duration = .lengthLong, @@ -94,7 +120,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 +133,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 +147,7 @@ public enum UIConstants { #endif } - public static func presentLinkPreviewForFile( + static func presentLinkPreviewForFile( _ file: File, link: String, from viewController: UIViewController, diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift index 72804e5b0..1fbc024a9 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 @@ -36,6 +37,8 @@ public protocol RouterAppNavigable { @MainActor func showLaunchFloatingPanel() + @MainActor func showUpsaleFloatingPanel() + @MainActor func showUpdateRequired() @MainActor func showPhotoSyncSettings() @@ -45,6 +48,10 @@ public protocol RouterAppNavigable { driveFileManager: DriveFileManager, files: [ImportedFile] ) + + @MainActor func showLogin(delegate: InfomaniakLoginDelegate) + + @MainActor func showRegister(delegate: InfomaniakLoginDelegate) } /// Routing methods available from both the AppExtension mode and App @@ -68,6 +75,20 @@ 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(_ destinationURL: URL) + + /// 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, + publicShareProxy: PublicShareProxy, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher + ) + /// Present a list of files from a folder /// - Parameters: /// - frozenFolder: Folder to display diff --git a/kDriveCore/Utils/DeeplinkParser.swift b/kDriveCore/Utils/DeeplinkParser.swift index 5fa85e381..cbdd3ed64 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 { @@ -64,6 +66,7 @@ public struct DeeplinkParser: DeeplinkParsable { return false } await router.navigate(to: .saveFiles(files: files)) + MatomoUtils.trackDeeplink(name: DeeplinkPath.file.rawValue) return true } diff --git a/kDriveCore/Utils/Deeplinks/DeeplinkService.swift b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift new file mode 100644 index 000000000..9c0c89d31 --- /dev/null +++ b/kDriveCore/Utils/Deeplinks/DeeplinkService.swift @@ -0,0 +1,48 @@ +/* + 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 + +public protocol DeeplinkServiceable: AnyObject { + func setLastPublicShare(_ link: PublicShareLink) + func clearLastPublicShare() + func processDeeplinksPostAuthentication() +} + +public class DeeplinkService: DeeplinkServiceable { + private var lastPublicShareLink: PublicShareLink? + + public func setLastPublicShare(_ link: PublicShareLink) { + lastPublicShareLink = link + } + + public func clearLastPublicShare() { + lastPublicShareLink = nil + } + + public func processDeeplinksPostAuthentication() { + guard let lastPublicShareLink else { + return + } + + Task { @MainActor in + await UniversalLinksHelper.processPublicShareLink(lastPublicShareLink) + clearLastPublicShare() + } + } +} diff --git a/kDriveCore/Utils/Deeplinks/PublicShareLink.swift b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift new file mode 100644 index 000000000..556c48237 --- /dev/null +++ b/kDriveCore/Utils/Deeplinks/PublicShareLink.swift @@ -0,0 +1,50 @@ +/* + 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: 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/Deeplinks/UniversalLinksHelper.swift b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift new file mode 100644 index 000000000..7379601ce --- /dev/null +++ b/kDriveCore/Utils/Deeplinks/UniversalLinksHelper.swift @@ -0,0 +1,218 @@ +/* + 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 CocoaLumberjackSwift +import Foundation +import InfomaniakCore +import InfomaniakDI +import kDriveResources +import SwiftRegex +import UIKit + +public enum UniversalLinksHelper { + private struct Link { + let regex: Regex + let displayMode: DisplayMode + + /// Matches a private share link + static let privateShareLink = Link( + regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-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] + } + + private enum DisplayMode { + case office, file + } + + @discardableResult + 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 + } + + let path = components.path + DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)") + + if let publicShare = await PublicShareLink(publicShareURL: url), + await processPublicShareLink(publicShare) { + return true + } + + // Common regex + for link in Link.all { + let matches = link.regex.matches(in: path) + if processRegex(matches: matches, displayMode: link.displayMode) { + return true + } + } + + DDLogWarn("[UniversalLinksHelper] Unable to process link with path: \(path)") + return false + } + + @discardableResult + public static func processPublicShareLink(_ link: PublicShareLink) async -> Bool { + @InjectService var deeplinkService: DeeplinkServiceable + deeplinkService.setLastPublicShare(link) + + let apiFetcher = PublicShareApiFetcher() + do { + let metadata = try await apiFetcher.getMetadata(driveId: link.driveId, shareLinkUid: link.shareLinkUid) + + return await processPublicShareMetadata( + metadata, + driveId: link.driveId, + shareLinkUid: link.shareLinkUid, + apiFetcher: apiFetcher + ) + } catch { + guard let apiError = error as? ApiError else { + return false + } + + guard let limitation = PublicShareLimitation(rawValue: apiError.code) else { + return false + } + + return await processPublicShareMetadataLimitation(limitation, publicShareURL: link.publicShareURL) + } + } + + 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(publicShareURL) + case .expired: + MatomoUtils.trackDeeplink(name: "publicShareExpired") + await appNavigable.presentPublicShareExpired() + } + + return true + } + + private static func processPublicShareMetadata(_ metadata: PublicShareMetadata, + driveId: Int, + shareLinkUid: String, + apiFetcher: PublicShareApiFetcher) async -> Bool { + @InjectService var accountManager: AccountManageable + + MatomoUtils.trackDeeplink(name: "publicShare") + + guard let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager( + for: shareLinkUid, + driveId: driveId, + rootFileId: metadata.fileId + ) else { + return false + } + + openPublicShare(driveId: driveId, + linkUuid: shareLinkUid, + fileId: metadata.fileId, + driveFileManager: publicShareDriveFileManager, + apiFetcher: apiFetcher) + + return true + } + + private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool { + @InjectService var accountManager: AccountManageable + + guard let firstMatch = matches.first, + firstMatch.count > 2, + let driveId = Int(firstMatch[1]), + let last = firstMatch.last, + let uploadFileId = Int(last), + let driveFileManager = accountManager.getDriveFileManager(for: driveId, + userId: accountManager.currentUserId) + else { return false } + + openFile(id: uploadFileId, driveFileManager: driveFileManager, office: displayMode == .office) + + return true + } + + private static func openPublicShare(driveId: Int, + linkUuid: String, + fileId: Int, + driveFileManager: DriveFileManager, + apiFetcher: PublicShareApiFetcher) { + Task { + do { + 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, update: .modified) + } + + let frozenRootFolder = rootFolder.freeze() + + @InjectService var appNavigable: AppNavigable + let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: fileId, shareLinkUid: linkUuid) + await appNavigable.presentPublicShare( + frozenRootFolder: frozenRootFolder, + publicShareProxy: publicShareProxy, + driveFileManager: driveFileManager, + apiFetcher: apiFetcher + ) + } catch { + DDLogError( + "[UniversalLinksHelper] Failed to get public folder [driveId:\(driveId) linkUuid:\(linkUuid) fileId:\(fileId)]: \(error)" + ) + await UIConstants.showSnackBarIfNeeded(error: error) + } + } + } + + private static func openFile(id: Int, driveFileManager: DriveFileManager, office: Bool) { + Task { + do { + let file = try await driveFileManager.file(id: id) + @InjectService var appNavigable: AppNavigable + await appNavigable.present(file: file, driveFileManager: driveFileManager, office: office) + } catch { + DDLogError("[UniversalLinksHelper] Failed to get file [\(driveFileManager.drive.id) - \(id)]: \(error)") + await UIConstants.showSnackBarIfNeeded(error: error) + } + } + } +} diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift index 770082f80..44d89ad06 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 { @@ -127,4 +127,28 @@ 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 trackAddToMyDrive() { + track(eventWithCategory: .publicShareAction, name: "saveToKDrive") + } + + public static func trackAddBulkToMykDrive() { + track(eventWithCategory: .publicShareAction, name: "bulkSaveToKDrive") + } + + public static func trackPublicSharePasswordAction() { + track(eventWithCategory: .publicSharePasswordAction, name: "openInBrowser") + } + + public static func trackUpsalePresented() { + track(eventWithCategory: .publicShareAction, name: "adBottomSheet") + } } diff --git a/kDriveCore/VideoPlayer/VideoPlayer.swift b/kDriveCore/VideoPlayer/VideoPlayer.swift index 20c0cccb1..215f6beaa 100644 --- a/kDriveCore/VideoPlayer/VideoPlayer.swift +++ b/kDriveCore/VideoPlayer/VideoPlayer.swift @@ -98,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 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 - 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 remoteAsset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) + self.setupStreamingAsset(remoteAsset, 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, diff --git a/kDriveTestShared/MCKRouter.swift b/kDriveTestShared/MCKRouter.swift index 1d24fda4f..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() } @@ -148,4 +161,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..e475dd97f 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") + } }