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")
+ }
}