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/AppDelegate.swift b/kDrive/AppDelegate.swift
index b61288e25..3b9c5ef2d 100644
--- a/kDrive/AppDelegate.swift
+++ b/kDrive/AppDelegate.swift
@@ -103,6 +103,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
application.registerForRemoteNotifications()
+ // swiftlint:disable force_try
+ Task {
+ try! await Task.sleep(nanoseconds:5_000_000_000)
+ print("coucou")
+ let somePublicShare = URL(string: "https://kdrive.infomaniak.com/app/share/140946/01953831-16d3-4df6-8b48-33c8001c7981")
+ //await UIApplication.shared.open(somePublicShare!) // opens safari
+
+ let components = URLComponents(url: somePublicShare!, resolvingAgainstBaseURL: true)
+ await UniversalLinksHelper.handlePath(components!.path)
+ }
+
return true
}
diff --git a/kDrive/AppRouter.swift b/kDrive/AppRouter.swift
index 3b2f05caf..5bef6867c 100644
--- a/kDrive/AppRouter.swift
+++ b/kDrive/AppRouter.swift
@@ -284,7 +284,8 @@ public struct AppRouter: AppNavigable {
let fileIds = sceneUserInfo[SceneRestorationValues.Carousel.filesIds.rawValue] as? [Int],
let currentIndex = sceneUserInfo[SceneRestorationValues.Carousel.currentIndex.rawValue] as? Int,
let normalFolderHierarchy = sceneUserInfo[SceneRestorationValues.Carousel.normalFolderHierarchy.rawValue] as? Bool,
- let presentationOrigin = sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else {
+ let presentationOrigin =
+ sceneUserInfo[SceneRestorationValues.Carousel.presentationOrigin.rawValue] as? PresentationOrigin else {
Log.sceneDelegate("metadata issue for PreviewController :\(sceneUserInfo)", level: .error)
return
}
@@ -584,6 +585,49 @@ public struct AppRouter: AppNavigable {
// MARK: RouterFileNavigable
+ @MainActor public func presentPublicShare(
+ frozenRootFolder: File,
+ publicShareProxy: PublicShareProxy,
+ driveFileManager: DriveFileManager,
+ apiFetcher: PublicShareApiFetcher
+ ) {
+ guard let window,
+ let rootViewController = window.rootViewController else {
+ fatalError("TODO: lazy load a rootViewController")
+ }
+
+ guard let rootViewController = window.rootViewController as? MainTabViewController else {
+ fatalError("Root is not a MainTabViewController")
+ return
+ }
+
+ // TODO: Fix access right
+ guard !frozenRootFolder.isDisabled else {
+ fatalError("isDisabled")
+ return
+ }
+
+ rootViewController.dismiss(animated: false) {
+ rootViewController.selectedIndex = MainTabBarIndex.files.rawValue
+
+ guard let navigationController = rootViewController.selectedViewController as? UINavigationController else {
+ return
+ }
+
+ let viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy,
+ sortType: .nameAZ,
+ driveFileManager: driveFileManager,
+ currentDirectory: frozenRootFolder,
+ apiFetcher: apiFetcher)
+ let viewController = FileListViewController(viewModel: viewModel)
+ let publicShareNavigationController = UINavigationController(rootViewController: viewController)
+ publicShareNavigationController.modalPresentationStyle = .fullScreen
+ publicShareNavigationController.modalTransitionStyle = .coverVertical
+
+ navigationController.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/SceneDelegate.swift b/kDrive/SceneDelegate.swift
index 6f23a8f33..25a69a2c9 100644
--- a/kDrive/SceneDelegate.swift
+++ b/kDrive/SceneDelegate.swift
@@ -230,13 +230,16 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDel
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
Log.sceneDelegate("scene continue userActivity")
- guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
- let incomingURL = userActivity.webpageURL,
- let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
- return
- }
+ Task {
+ guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
+ let incomingURL = userActivity.webpageURL,
+ let components = URLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else {
+ Log.sceneDelegate("scene continue userActivity - unable", level: .error)
+ return
+ }
- UniversalLinksHelper.handlePath(components.path)
+ await UniversalLinksHelper.handlePath(components.path)
+ }
}
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..1e1b5f92a
--- /dev/null
+++ b/kDrive/UI/Controller/Create File/FloatingPanelLayouts.swift
@@ -0,0 +1,141 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import FloatingPanel
+import kDriveCore
+import kDriveResources
+import UIKit
+
+/// Layout used for a folder within a public share
+class PublicShareFolderFloatingPanelLayout: FloatingPanelLayout {
+ var position: FloatingPanelPosition = .bottom
+ var initialState: FloatingPanelState = .tip
+ var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring]
+ private var backdropAlpha: CGFloat
+
+ init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) {
+ self.initialState = initialState
+ self.backdropAlpha = backdropAlpha
+ let extendedAnchor = FloatingPanelLayoutAnchor(
+ absoluteInset: 140.0 + safeAreaInset,
+ edge: .bottom,
+ referenceGuide: .superview
+ )
+ anchors = [
+ .full: extendedAnchor,
+ .half: extendedAnchor,
+ .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview)
+ ]
+ }
+
+ func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
+ return backdropAlpha
+ }
+}
+
+/// Layout used for a file within a public share
+class PublicShareFileFloatingPanelLayout: FloatingPanelLayout {
+ var position: FloatingPanelPosition = .bottom
+ var initialState: FloatingPanelState = .tip
+ var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring]
+ private var backdropAlpha: CGFloat
+
+ init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) {
+ self.initialState = initialState
+ self.backdropAlpha = backdropAlpha
+ let extendedAnchor = FloatingPanelLayoutAnchor(
+ absoluteInset: 248.0 + safeAreaInset,
+ edge: .bottom,
+ referenceGuide: .superview
+ )
+ anchors = [
+ .full: extendedAnchor,
+ .half: extendedAnchor,
+ .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview)
+ ]
+ }
+
+ func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
+ return backdropAlpha
+ }
+}
+
+class FileFloatingPanelLayout: FloatingPanelLayout {
+ var position: FloatingPanelPosition = .bottom
+ var initialState: FloatingPanelState = .tip
+ var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring]
+ private var backdropAlpha: CGFloat
+
+ init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) {
+ self.initialState = initialState
+ self.backdropAlpha = backdropAlpha
+ if hideTip {
+ anchors = [
+ .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
+ .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea)
+ ]
+ } else {
+ anchors = [
+ .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
+ .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
+ .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview)
+ ]
+ }
+ }
+
+ func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
+ return backdropAlpha
+ }
+}
+
+class PlusButtonFloatingPanelLayout: FloatingPanelLayout {
+ var position: FloatingPanelPosition = .bottom
+ var height: CGFloat = 16
+
+ init(height: CGFloat) {
+ self.height = height
+ }
+
+ var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
+ return [
+ .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea)
+ ]
+ }
+
+ var initialState: FloatingPanelState = .full
+
+ func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
+ return 0.2
+ }
+}
+
+class InformationViewFloatingPanelLayout: FloatingPanelLayout {
+ var position: FloatingPanelPosition = .bottom
+
+ var initialState: FloatingPanelState = .full
+
+ var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
+ return [
+ .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea)
+ ]
+ }
+
+ func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
+ return 0.3
+ }
+}
diff --git a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift
index 793edb132..e5ca7bc58 100644
--- a/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift
+++ b/kDrive/UI/Controller/Create File/FloatingPanelUtils.swift
@@ -73,68 +73,3 @@ class AdaptiveDriveFloatingPanelController: DriveFloatingPanelController {
track(scrollView: scrollView)
}
}
-
-class FileFloatingPanelLayout: FloatingPanelLayout {
- var position: FloatingPanelPosition = .bottom
- var initialState: FloatingPanelState = .tip
- var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring]
- private var backdropAlpha: CGFloat
-
- init(initialState: FloatingPanelState = .tip, hideTip: Bool = false, safeAreaInset: CGFloat = 0, backdropAlpha: CGFloat = 0) {
- self.initialState = initialState
- self.backdropAlpha = backdropAlpha
- if hideTip {
- anchors = [
- .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
- .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea)
- ]
- } else {
- anchors = [
- .full: FloatingPanelLayoutAnchor(absoluteInset: 16.0, edge: .top, referenceGuide: .safeArea),
- .half: FloatingPanelLayoutAnchor(fractionalInset: 0.5, edge: .bottom, referenceGuide: .safeArea),
- .tip: FloatingPanelLayoutAnchor(absoluteInset: 86.0 + safeAreaInset, edge: .bottom, referenceGuide: .superview)
- ]
- }
- }
-
- func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
- return backdropAlpha
- }
-}
-
-class PlusButtonFloatingPanelLayout: FloatingPanelLayout {
- var position: FloatingPanelPosition = .bottom
- var height: CGFloat = 16
-
- init(height: CGFloat) {
- self.height = height
- }
-
- var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
- return [
- .full: FloatingPanelLayoutAnchor(absoluteInset: height, edge: .bottom, referenceGuide: .safeArea)
- ]
- }
-
- var initialState: FloatingPanelState = .full
-
- func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
- return 0.2
- }
-}
-
-class InformationViewFloatingPanelLayout: FloatingPanelLayout {
- var position: FloatingPanelPosition = .bottom
-
- var initialState: FloatingPanelState = .full
-
- var anchors: [FloatingPanelState: FloatingPanelLayoutAnchoring] {
- return [
- .full: FloatingPanelIntrinsicLayoutAnchor(absoluteOffset: 0, referenceGuide: .safeArea)
- ]
- }
-
- func backdropAlpha(for state: FloatingPanelState) -> CGFloat {
- return 0.3
- }
-}
diff --git a/kDrive/UI/Controller/Files/File List/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 5ca4e7167..c9c8aca7a 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
@@ -141,6 +142,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV
)
setupViewModel()
+ setupFooterIfNeeded()
}
override func viewWillAppear(_ animated: Bool) {
@@ -250,6 +252,46 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV
}
}
+ func setupFooterIfNeeded() {
+ guard driveFileManager.isPublicShare else {
+ return
+ }
+
+ let addToKDriveButton = IKButton(type: .custom)
+ addToKDriveButton.setTitle("Add to My kDrive", for: .normal)
+ addToKDriveButton.addTarget(self, action: #selector(addToMyDriveButtonTapped), for: .touchUpInside)
+ addToKDriveButton.setBackgroundColors(normal: .systemBlue, highlighted: .darkGray)
+ addToKDriveButton.translatesAutoresizingMaskIntoConstraints = false
+ addToKDriveButton.cornerRadius = 8.0
+ addToKDriveButton.clipsToBounds = true
+
+ view.addSubview(addToKDriveButton)
+ view.bringSubviewToFront(addToKDriveButton)
+
+ let leadingConstraint = addToKDriveButton.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor,
+ constant: 16)
+ leadingConstraint.priority = .defaultHigh
+ let trailingConstraint = addToKDriveButton.trailingAnchor.constraint(
+ greaterThanOrEqualTo: view.trailingAnchor,
+ constant: -16
+ )
+ trailingConstraint.priority = .defaultHigh
+ let widthConstraint = addToKDriveButton.widthAnchor.constraint(lessThanOrEqualToConstant: 360)
+
+ NSLayoutConstraint.activate([
+ addToKDriveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ leadingConstraint,
+ trailingConstraint,
+ addToKDriveButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
+ addToKDriveButton.heightAnchor.constraint(equalToConstant: 60),
+ widthConstraint
+ ])
+ }
+
+ @objc func addToMyDriveButtonTapped() {
+ print("TODO: addToMyDriveButtonTapped")
+ }
+
func reloadCollectionViewWith(files: [File]) {
let changeSet = StagedChangeset(source: displayedFiles, target: files)
collectionView.reload(using: changeSet,
@@ -348,6 +390,30 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV
}
}
+ private func fileLayout(files: [File]) -> FloatingPanelLayout {
+ guard driveFileManager.isPublicShare else {
+ return FileFloatingPanelLayout(
+ initialState: .half,
+ hideTip: true,
+ backdropAlpha: 0.2
+ )
+ }
+
+ if files.first?.isDirectory ?? false {
+ return PublicShareFolderFloatingPanelLayout(
+ initialState: .half,
+ hideTip: true,
+ backdropAlpha: 0.2
+ )
+ } else {
+ return PublicShareFileFloatingPanelLayout(
+ initialState: .half,
+ hideTip: true,
+ backdropAlpha: 0.2
+ )
+ }
+ }
+
private func showQuickActionsPanel(files: [File], actionType: FileListQuickActionType) {
#if !ISEXTENSION
var floatingPanelViewController: DriveFloatingPanelController
@@ -359,11 +425,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV
fileInformationsViewController.presentingParent = self
fileInformationsViewController.normalFolderHierarchy = viewModel.configuration.normalFolderHierarchy
- floatingPanelViewController.layout = FileFloatingPanelLayout(
- initialState: .half,
- hideTip: true,
- backdropAlpha: 0.2
- )
+ floatingPanelViewController.layout = fileLayout(files: files)
if let file = files.first {
fileInformationsViewController.setFile(file, driveFileManager: driveFileManager)
@@ -465,7 +527,7 @@ class FileListViewController: UICollectionViewController, SwipeActionCollectionV
}
@objc func barButtonPressed(_ sender: FileListBarButton) {
- viewModel.barButtonPressed(type: sender.type)
+ viewModel.barButtonPressed(sender: sender, type: sender.type)
}
@objc func forceRefresh() {
@@ -896,3 +958,38 @@ extension FileListViewController: UICollectionViewDropDelegate {
}
}
}
+
+// Move to CoreUIKit or use something else ?
+extension UIImage {
+ convenience init?(color: UIColor) {
+ let size = CGSize(width: 1, height: 1)
+ UIGraphicsBeginImageContext(size)
+ guard let context = UIGraphicsGetCurrentContext() else {
+ return nil
+ }
+
+ context.setFillColor(color.cgColor)
+ context.fill(CGRect(origin: .zero, size: size))
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()!
+ UIGraphicsEndImageContext()
+ guard let cgImage = image.cgImage else {
+ return nil
+ }
+
+ self.init(cgImage: cgImage)
+ }
+}
+
+// Move to CoreUIKit or use something else ?
+extension IKButton {
+ func setBackgroundColors(normal normalColor: UIColor, highlighted highlightedColor: UIColor) {
+ if let normalImage = UIImage(color: normalColor) {
+ setBackgroundImage(normalImage, for: .normal)
+ }
+
+ if let highlightedImage = UIImage(color: highlightedColor) {
+ setBackgroundImage(highlightedImage, for: .highlighted)
+ }
+ }
+}
diff --git a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift
index 6201676ef..cf1db00fc 100644
--- a/kDrive/UI/Controller/Files/File List/FileListViewModel.swift
+++ b/kDrive/UI/Controller/Files/File List/FileListViewModel.swift
@@ -34,6 +34,7 @@ enum FileListBarButtonType {
case searchFilters
case photoSort
case addFolder
+ case downloadAll
}
enum FileListQuickActionType {
@@ -279,7 +280,7 @@ class FileListViewModel: SelectDelegate {
}.store(in: &bindStore)
}
- func barButtonPressed(type: FileListBarButtonType) {
+ func barButtonPressed(sender: Any? = nil, type: FileListBarButtonType) {
if multipleSelectionViewModel?.isMultipleSelectionEnabled == true {
multipleSelectionViewModel?.barButtonPressed(type: type)
}
@@ -342,7 +343,10 @@ class FileListViewModel: SelectDelegate {
}
func didSelectFile(at indexPath: IndexPath) {
- guard let file: File = getFile(at: indexPath) else { return }
+ guard let file: File = getFile(at: indexPath) else {
+ return
+ }
+
if ReachabilityListener.instance.currentStatus == .offline && !file.isDirectory && !file.isAvailableOffline {
return
}
diff --git a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift
index ea8257905..e8e854b7c 100644
--- a/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift
+++ b/kDrive/UI/Controller/Files/File List/MultipleSelectionFileListViewModel.swift
@@ -107,8 +107,15 @@ class MultipleSelectionFileListViewModel {
init(configuration: FileListViewModel.Configuration, driveFileManager: DriveFileManager, currentDirectory: File) {
isMultipleSelectionEnabled = false
selectedCount = 0
- multipleSelectionActions = [.move, .delete, .more]
+
self.driveFileManager = driveFileManager
+
+ if driveFileManager.isPublicShare {
+ multipleSelectionActions = []
+ } else {
+ multipleSelectionActions = [.move, .delete, .more]
+ }
+
self.currentDirectory = currentDirectory
self.configuration = configuration
}
diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift
index bbcf13c7c..4407c521c 100644
--- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift
+++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController+Actions.swift
@@ -36,7 +36,14 @@ extension FileActionsFloatingPanelViewController {
private func setupQuickActions() {
let offline = ReachabilityListener.instance.currentStatus == .offline
- quickActions = file.isDirectory ? FloatingPanelAction.folderQuickActions : FloatingPanelAction.quickActions
+ if driveFileManager.isPublicShare {
+ quickActions = []
+ } else if file.isDirectory {
+ quickActions = FloatingPanelAction.folderQuickActions
+ } else {
+ quickActions = FloatingPanelAction.quickActions
+ }
+
for action in quickActions {
switch action {
case .shareAndRights:
@@ -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:
@@ -175,7 +191,8 @@ extension FileActionsFloatingPanelViewController {
if file.isMostRecentDownloaded {
presentShareSheet(from: indexPath)
} else {
- downloadFile(action: action, indexPath: indexPath) { [weak self] in
+ downloadFile(action: action,
+ indexPath: indexPath) { [weak self] in
self?.presentShareSheet(from: indexPath)
}
}
diff --git a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift
index 4e1acdbb5..d462c2d3d 100644
--- a/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift
+++ b/kDrive/UI/Controller/Files/FileActionsFloatingPanelViewController.swift
@@ -205,6 +205,14 @@ public class FloatingPanelAction: Equatable {
return [informations, add, shareAndRights, shareLink].map { $0.reset() }
}
+ static var publicShareActions: [FloatingPanelAction] {
+ return [openWith, sendCopy, download].map { $0.reset() }
+ }
+
+ static var publicShareFolderActions: [FloatingPanelAction] {
+ return [download].map { $0.reset() }
+ }
+
static var multipleSelectionActions: [FloatingPanelAction] {
return [manageCategories, favorite, offline, download, move, duplicate].map { $0.reset() }
}
@@ -388,7 +396,9 @@ final class FileActionsFloatingPanelViewController: UICollectionViewController {
present(activityViewController, animated: true)
}
- func downloadFile(action: FloatingPanelAction, indexPath: IndexPath, completion: @escaping () -> Void) {
+ func downloadFile(action: FloatingPanelAction,
+ indexPath: IndexPath,
+ completion: @escaping () -> Void) {
guard let observerViewController = UIApplication.shared.windows.first?.rootViewController else { return }
downloadAction = action
setLoading(true, action: action, at: indexPath)
@@ -405,7 +415,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) {
@@ -477,7 +495,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/FilePresenter.swift b/kDrive/UI/Controller/Files/FilePresenter.swift
index 0d398e815..be01bcbeb 100644
--- a/kDrive/UI/Controller/Files/FilePresenter.swift
+++ b/kDrive/UI/Controller/Files/FilePresenter.swift
@@ -145,6 +145,12 @@ final class FilePresenter {
let viewModel: FileListViewModel
if driveFileManager.drive.sharedWithMe {
viewModel = SharedWithMeViewModel(driveFileManager: driveFileManager, currentDirectory: file)
+ } else if let publicShareProxy = driveFileManager.publicShareProxy {
+ viewModel = PublicShareViewModel(publicShareProxy: publicShareProxy,
+ sortType: .nameAZ,
+ driveFileManager: driveFileManager,
+ currentDirectory: file,
+ apiFetcher: PublicShareApiFetcher())
} else if file.isTrashed || file.deletedAt != nil {
viewModel = TrashListViewModel(driveFileManager: driveFileManager, currentDirectory: file)
} else {
diff --git a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift
index a22f6035a..a17b0aa76 100644
--- a/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift
+++ b/kDrive/UI/Controller/Files/MultipleSelectionFloatingPanelViewController.swift
@@ -209,6 +209,16 @@ final class MultipleSelectionFloatingPanelViewController: UICollectionViewContro
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let action = actions[indexPath.item]
handleAction(action, at: indexPath)
- MatomoUtils.trackBuklAction(action: action, files: files, fromPhotoList: presentingParent is PhotoListViewController)
+
+ let eventCategory: MatomoUtils.EventCategory
+ if presentingParent is PhotoListViewController {
+ eventCategory = .picturesFileAction
+ } else if driveFileManager.isPublicShare {
+ eventCategory = .publicShareAction
+ } else {
+ eventCategory = .fileListFileAction
+ }
+
+ MatomoUtils.trackBuklAction(action: action, files: files, category: eventCategory)
}
}
diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
index 19c0c6477..20b35ffa5 100644
--- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
+++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift
@@ -650,7 +650,11 @@ extension PreviewViewController: UICollectionViewDataSource {
) {
let file = previewFiles[indexPath.row]
if let cell = cell as? DownloadingPreviewCollectionViewCell {
- cell.progressiveLoadingForFile(file)
+ if let publicShareProxy = driveFileManager.publicShareProxy {
+ cell.progressiveLoadingForPublicShareFile(file, publicShareProxy: publicShareProxy)
+ } else {
+ cell.progressiveLoadingForFile(file)
+ }
}
}
diff --git a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift
index 99f975988..32d1acab3 100644
--- a/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift
+++ b/kDrive/UI/Controller/Files/Search/SearchFilesViewModel.swift
@@ -84,7 +84,8 @@ class SearchFilesViewModel: FileListViewModel {
filters = Filters()
let searchFakeRoot = driveFileManager.getManagedFile(from: DriveFileManager.searchFilesRootFile)
super.init(configuration: configuration, driveFileManager: driveFileManager, currentDirectory: searchFakeRoot)
- observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children).sorted(by: [sortType.value.sortDescriptor]))
+ observedFiles = AnyRealmCollection(AnyRealmCollection(searchFakeRoot.children)
+ .sorted(by: [sortType.value.sortDescriptor]))
}
override func startObservation() {
@@ -145,16 +146,16 @@ class SearchFilesViewModel: FileListViewModel {
private func searchOffline() {
observedFiles = AnyRealmCollection(driveFileManager.searchOffline(query: currentSearchText,
- date: filters.date?.dateInterval,
- fileType: filters.fileType,
- categories: Array(filters.categories),
- fileExtensions: filters.fileExtensions,
- belongToAllCategories: filters.belongToAllCategories,
- sortType: sortType))
+ date: filters.date?.dateInterval,
+ fileType: filters.fileType,
+ categories: Array(filters.categories),
+ fileExtensions: filters.fileExtensions,
+ belongToAllCategories: filters.belongToAllCategories,
+ sortType: sortType))
startObservation()
}
- override func barButtonPressed(type: FileListBarButtonType) {
+ override func barButtonPressed(sender: Any?, type: FileListBarButtonType) {
if type == .searchFilters {
let navigationController = SearchFiltersViewController
.instantiateInNavigationController(driveFileManager: driveFileManager)
@@ -163,7 +164,7 @@ class SearchFilesViewModel: FileListViewModel {
searchFiltersViewController?.delegate = self
onPresentViewController?(.modal, navigationController, true)
} else {
- super.barButtonPressed(type: type)
+ super.barButtonPressed(sender: sender, type: type)
}
}
diff --git a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift
index 72d46e53e..d6378a65e 100644
--- a/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift
+++ b/kDrive/UI/Controller/Menu/PhotoList/PhotoListViewModel.swift
@@ -144,7 +144,7 @@ class PhotoListViewModel: FileListViewModel {
self.nextCursor = nextCursor
}
- override func barButtonPressed(type: FileListBarButtonType) {
+ override func barButtonPressed(sender: Any?, type: FileListBarButtonType) {
if type == .search {
let viewModel = SearchFilesViewModel(driveFileManager: driveFileManager, filters: Filters(fileType: .image))
let searchViewController = SearchViewController.instantiateInNavigationController(viewModel: viewModel)
@@ -156,7 +156,7 @@ class PhotoListViewModel: FileListViewModel {
delegate: self)
onPresentViewController?(.modal, floatingPanelViewController, true)
} else {
- super.barButtonPressed(type: type)
+ super.barButtonPressed(sender: sender, type: type)
}
}
diff --git a/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift
new file mode 100644
index 000000000..69273f79f
--- /dev/null
+++ b/kDrive/UI/Controller/Menu/Share/PublicShareViewModel.swift
@@ -0,0 +1,141 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import InfomaniakCore
+import InfomaniakDI
+import kDriveCore
+import RealmSwift
+import UIKit
+
+/// Public share view model, loading content from memory realm
+final class PublicShareViewModel: InMemoryFileListViewModel {
+ private var downloadObserver: ObservationToken?
+
+ var publicShareProxy: PublicShareProxy?
+ let rootProxy: ProxyFile
+ var publicShareApiFetcher: PublicShareApiFetcher?
+
+ required init(driveFileManager: DriveFileManager, currentDirectory: File? = nil) {
+ guard let currentDirectory else {
+ fatalError("PublicShareViewModel requires a currentDirectory to work")
+ }
+
+ // TODO: i18n
+ let configuration = Configuration(selectAllSupported: false,
+ rootTitle: "public share",
+ emptyViewType: .emptyFolder,
+ supportsDrop: false,
+ rightBarButtons: [.downloadAll],
+ matomoViewPath: [MatomoUtils.Views.menu.displayName, "publicShare"])
+
+ 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
+ ) {
+ self.init(driveFileManager: driveFileManager, currentDirectory: currentDirectory)
+ self.publicShareProxy = publicShareProxy
+ self.sortType = sortType
+ publicShareApiFetcher = apiFetcher
+ }
+
+ override func loadFiles(cursor: String? = nil, forceRefresh: Bool = false) async throws {
+ guard !isLoading || cursor != nil,
+ let publicShareProxy,
+ let publicShareApiFetcher else {
+ return
+ }
+
+ // Only show loading indicator if we have nothing in cache
+ if !currentDirectory.canLoadChildrenFromCache {
+ startRefreshing(cursor: cursor)
+ }
+ defer {
+ endRefreshing()
+ }
+
+ let (_, nextCursor) = try await driveFileManager.publicShareFiles(rootProxy: rootProxy,
+ publicShareProxy: publicShareProxy,
+ publicShareApiFetcher: publicShareApiFetcher)
+ endRefreshing()
+ if let nextCursor {
+ try await loadFiles(cursor: nextCursor)
+ }
+ }
+
+ // TODO: Move away from view model
+ override func barButtonPressed(sender: Any?, type: FileListBarButtonType) {
+ guard downloadObserver == nil else {
+ return
+ }
+
+ guard type == .downloadAll,
+ let publicShareProxy = publicShareProxy else {
+ return
+ }
+
+ // TODO: Abstract sheet presentation
+ @InjectService var appNavigable: AppNavigable
+ guard let topMostViewController = appNavigable.topMostViewController else {
+ return
+ }
+
+ downloadObserver = DownloadQueue.instance
+ .observeFileDownloaded(self, fileId: currentDirectory.id) { [weak self] _, error in
+ Task { @MainActor in
+ guard let self = self else {
+ return
+ }
+
+ defer {
+ self.downloadObserver?.cancel()
+ self.downloadObserver = nil
+ }
+
+ guard let senderItem = sender as? UIBarButtonItem else {
+ return
+ }
+
+ guard error == nil else {
+ UIConstants.showSnackBarIfNeeded(error: DriveError.downloadFailed)
+ return
+ }
+
+ // present share sheet
+ let activityViewController = UIActivityViewController(
+ activityItems: [self.currentDirectory.localUrl],
+ applicationActivities: nil
+ )
+
+ activityViewController.popoverPresentationController?.barButtonItem = senderItem
+ topMostViewController.present(activityViewController, animated: true)
+ }
+ }
+
+ DownloadQueue.instance.addPublicShareToQueue(file: currentDirectory,
+ driveFileManager: driveFileManager,
+ publicShareProxy: publicShareProxy)
+ }
+}
diff --git a/kDrive/UI/Controller/Menu/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/Trash/TrashListViewModel.swift b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift
index 67ab3d778..e77094413 100644
--- a/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift
+++ b/kDrive/UI/Controller/Menu/Trash/TrashListViewModel.swift
@@ -92,7 +92,7 @@ class TrashListViewModel: InMemoryFileListViewModel {
forceRefresh()
}
- override func barButtonPressed(type: FileListBarButtonType) {
+ override func barButtonPressed(sender: Any?, type: FileListBarButtonType) {
if type == .emptyTrash {
let alert = AlertTextViewController(title: KDriveResourcesStrings.Localizable.modalEmptyTrashTitle,
message: KDriveResourcesStrings.Localizable.modalEmptyTrashDescription,
@@ -103,7 +103,7 @@ class TrashListViewModel: InMemoryFileListViewModel {
}
onPresentViewController?(.modal, alert, true)
} else {
- super.barButtonPressed(type: type)
+ super.barButtonPressed(sender: sender, type: type)
}
}
diff --git a/kDrive/UI/View/Files/FileCollectionViewCell.swift b/kDrive/UI/View/Files/FileCollectionViewCell.swift
index 23901692f..09e0b416b 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)
}
@@ -138,26 +143,52 @@ protocol FileCellDelegate: AnyObject {
}
func setThumbnail(on imageView: UIImageView) {
+ // check if public share / use specific endpoint
guard !file.isInvalidated,
- (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail)
- else { return }
+ (file.convertedType == .image || file.convertedType == .video) && file.supportedBy.contains(.thumbnail) else {
+ return
+ }
+
// Configure placeholder
imageView.image = nil
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = UIConstants.imageCornerRadius
imageView.layer.masksToBounds = true
imageView.backgroundColor = KDriveResourcesAsset.loaderDefaultColor.color
- // Fetch thumbnail
- thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in
- guard let self,
- !self.file.isInvalidated,
- !self.isSelected else {
- return
+
+ if let publicShareProxy = publicShareProxy {
+ // Fetch public share thumbnail
+ thumbnailDownloadTask = file.getPublicShareThumbnail(publicShareId: publicShareProxy.shareLinkUid,
+ publicDriveId: publicShareProxy.driveId,
+ publicFileId: file.id) { [
+ requestFileId = file.id,
+ weak self
+ ] image, _ in
+ guard let self,
+ !self.file.isInvalidated,
+ !self.isSelected else {
+ return
+ }
+
+ if file.id == requestFileId {
+ imageView.image = image
+ imageView.backgroundColor = nil
+ }
}
- if file.id == requestFileId {
- imageView.image = image
- imageView.backgroundColor = nil
+ } else {
+ // Fetch thumbnail
+ thumbnailDownloadTask = file.getThumbnail { [requestFileId = file.id, weak self] image, _ in
+ guard let self,
+ !self.file.isInvalidated,
+ !self.isSelected else {
+ return
+ }
+
+ if file.id == requestFileId {
+ imageView.image = image
+ imageView.backgroundColor = nil
+ }
}
}
}
@@ -302,7 +333,7 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
func configure(with viewModel: FileViewModel) {
self.viewModel = viewModel
- configureLogoImage()
+ configureLogoImage(viewModel: viewModel)
titleLabel.text = viewModel.title
detailLabel?.text = viewModel.subtitle
favoriteImageView?.isHidden = !viewModel.isFavorite
@@ -321,7 +352,12 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
}
func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) {
- configure(with: FileViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode))
+ let fileViewModel = FileViewModel(
+ driveFileManager: driveFileManager,
+ file: file,
+ selectionMode: selectionMode
+ )
+ configure(with: fileViewModel)
}
/// Update the cell selection mode.
@@ -333,18 +369,20 @@ class FileCollectionViewCell: UICollectionViewCell, SwipableCell {
}
func configureForSelection() {
- guard viewModel?.selectionMode == true else { return }
+ guard let viewModel,
+ viewModel.selectionMode == true else {
+ return
+ }
if isSelected {
configureCheckmarkImage()
configureImport(shouldDisplay: false)
} else {
- configureLogoImage()
+ configureLogoImage(viewModel: viewModel)
}
}
- private func configureLogoImage() {
- guard let viewModel else { return }
+ private func configureLogoImage(viewModel: FileViewModel) {
logoImage.isAccessibilityElement = true
logoImage.accessibilityLabel = viewModel.iconAccessibilityLabel
logoImage.image = viewModel.icon
diff --git a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift
index 4ba0608a7..58b6e7ec2 100644
--- a/kDrive/UI/View/Files/FileGridCollectionViewCell.swift
+++ b/kDrive/UI/View/Files/FileGridCollectionViewCell.swift
@@ -110,7 +110,12 @@ class FileGridCollectionViewCell: FileCollectionViewCell {
}
override func configureWith(driveFileManager: DriveFileManager, file: File, selectionMode: Bool = false) {
- configure(with: FileGridViewModel(driveFileManager: driveFileManager, file: file, selectionMode: selectionMode))
+ let viewModel = FileGridViewModel(
+ driveFileManager: driveFileManager,
+ file: file,
+ selectionMode: selectionMode
+ )
+ configure(with: viewModel)
}
override func configureLoading() {
diff --git a/kDrive/UI/View/Files/FileListBarButton.swift b/kDrive/UI/View/Files/FileListBarButton.swift
index c55363306..62984460f 100644
--- a/kDrive/UI/View/Files/FileListBarButton.swift
+++ b/kDrive/UI/View/Files/FileListBarButton.swift
@@ -17,8 +17,8 @@
*/
import Foundation
-import UIKit
import kDriveResources
+import UIKit
final class FileListBarButton: UIBarButtonItem {
private(set) var type: FileListBarButtonType = .cancel
@@ -49,6 +49,10 @@ final class FileListBarButton: UIBarButtonItem {
case .addFolder:
self.init(image: KDriveResourcesAsset.folderAdd.image, style: .plain, target: target, action: action)
accessibilityLabel = KDriveResourcesStrings.Localizable.createFolderTitle
+ case .downloadAll:
+ let image = KDriveResourcesAsset.download.image
+ self.init(image: image, style: .plain, target: target, action: action)
+ accessibilityLabel = KDriveResourcesStrings.Localizable.buttonDownload
}
self.type = type
}
diff --git a/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/Utils/MatomoUtils+UI.swift b/kDrive/Utils/MatomoUtils+UI.swift
index e2a828f5c..88082c8ee 100644
--- a/kDrive/Utils/MatomoUtils+UI.swift
+++ b/kDrive/Utils/MatomoUtils+UI.swift
@@ -45,8 +45,7 @@ extension MatomoUtils {
#if !ISEXTENSION
- static func trackFileAction(action: FloatingPanelAction, file: File, fromPhotoList: Bool) {
- let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction
+ static func trackFileAction(action: FloatingPanelAction, file: File, category: EventCategory) {
switch action {
// Quick Actions
case .sendCopy:
@@ -77,9 +76,8 @@ extension MatomoUtils {
}
}
- static func trackBuklAction(action: FloatingPanelAction, files: [File], fromPhotoList: Bool) {
+ static func trackBuklAction(action: FloatingPanelAction, files: [File], category: EventCategory) {
let numberOfFiles = files.count
- let category: EventCategory = fromPhotoList ? .picturesFileAction : .fileListFileAction
switch action {
// Quick Actions
case .duplicate:
diff --git a/kDrive/Utils/UniversalLinksHelper.swift b/kDrive/Utils/UniversalLinksHelper.swift
index 50b68adf5..10718c026 100644
--- a/kDrive/Utils/UniversalLinksHelper.swift
+++ b/kDrive/Utils/UniversalLinksHelper.swift
@@ -35,26 +35,44 @@ enum UniversalLinksHelper {
regex: Regex(pattern: #"^/app/drive/([0-9]+)/redirect/([0-9]+)$"#)!,
displayMode: .file
)
+
+ /// Matches a public share link
+ static let publicShareLink = Link(
+ regex: Regex(pattern: #"^/app/share/([0-9]+)/([a-z0-9-]+)$"#)!,
+ displayMode: .file
+ )
+
/// Matches a directory list link
static let directoryLink = Link(regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+)$"#)!, displayMode: .file)
+
/// Matches a file preview link
static let filePreview = Link(
regex: Regex(pattern: #"^/app/drive/([0-9]+)/files/([0-9]+/)?preview/[a-z]+/([0-9]+)$"#)!,
displayMode: .file
)
+
/// Matches an office file link
static let officeLink = Link(regex: Regex(pattern: #"^/app/office/([0-9]+)/([0-9]+)$"#)!, displayMode: .office)
- static let all = [privateShareLink, directoryLink, filePreview, officeLink]
+ static let all = [privateShareLink, publicShareLink, directoryLink, filePreview, officeLink]
}
private enum DisplayMode {
case office, file
}
- static func handlePath(_ path: String) -> Bool {
+ @discardableResult
+ static func handlePath(_ path: String) async -> Bool {
DDLogInfo("[UniversalLinksHelper] Trying to open link with path: \(path)")
+ // Public share link regex
+ let shareLink = Link.publicShareLink
+ let matches = shareLink.regex.matches(in: path)
+ if await processPublicShareLink(matches: matches, displayMode: shareLink.displayMode) {
+ return true
+ }
+
+ // Common regex
for link in Link.all {
let matches = link.regex.matches(in: path)
if processRegex(matches: matches, displayMode: link.displayMode) {
@@ -66,6 +84,47 @@ enum UniversalLinksHelper {
return false
}
+ private static func processPublicShareLink(matches: [[String]], displayMode: DisplayMode) async -> Bool {
+ @InjectService var accountManager: AccountManageable
+
+ guard let firstMatch = matches.first,
+ let driveId = firstMatch[safe: 1],
+ let driveIdInt = Int(driveId),
+ let shareLinkUid = firstMatch[safe: 2] else {
+ return false
+ }
+
+ // request metadata
+ let apiFetcher = PublicShareApiFetcher()
+ guard let metadata = try? await apiFetcher.getMetadata(driveId: driveIdInt, shareLinkUid: shareLinkUid)
+ else {
+ return false
+ }
+
+ let trackerName: String
+ if metadata.isPasswordNeeded {
+ trackerName = "publicShareWithPassword"
+ } else if metadata.isExpired {
+ trackerName = "publicShareExpired"
+ } else {
+ trackerName = "publicShare"
+ }
+ MatomoUtils.trackDeeplink(name: trackerName)
+
+ // get file ID from metadata
+ let publicShareDriveFileManager = accountManager.getInMemoryDriveFileManager(
+ for: shareLinkUid,
+ driveId: driveIdInt,
+ rootFileId: metadata.fileId
+ )
+ openPublicShare(driveId: driveIdInt,
+ linkUuid: shareLinkUid,
+ fileId: metadata.fileId,
+ driveFileManager: publicShareDriveFileManager,
+ apiFetcher: apiFetcher)
+ return true
+ }
+
private static func processRegex(matches: [[String]], displayMode: DisplayMode) -> Bool {
@InjectService var accountManager: AccountManageable
@@ -83,6 +142,40 @@ enum UniversalLinksHelper {
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)
+ }
+
+ 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 {
diff --git a/kDriveCore/Data/Api/DriveApiFetcher.swift b/kDriveCore/Data/Api/DriveApiFetcher.swift
index 0d75d0e84..ed27a7d51 100644
--- a/kDriveCore/Data/Api/DriveApiFetcher.swift
+++ b/kDriveCore/Data/Api/DriveApiFetcher.swift
@@ -52,6 +52,58 @@ 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
+
+ // TODO: Test parsing
+ public let isPasswordNeeded: Bool = false
+ public let isExpired: Bool = false
+
+ 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: InfomaniakTokenable
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..2d3d01edd
--- /dev/null
+++ b/kDriveCore/Data/Api/Endpoint+Share.swift
@@ -0,0 +1,95 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import 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)")
+ }
+
+ /// Some legacy calls like thumbnails require a V2 call
+ static func shareLinkFileV2(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint {
+ shareUrlV2.appending(path: "/\(driveId)/share/\(linkUuid)/files/\(fileId)")
+ }
+
+ /// Share link file children
+ 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")
+ }
+
+ func showOfficeShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) -> Endpoint {
+ return Self.shareUrlV1.appending(path: "/share/\(driveId)/\(linkUuid)/preview/text/\(fileId)")
+ }
+
+ func importShareLinkFiles(driveId: Int) -> Endpoint {
+ return Self.shareUrlV2.appending(path: "/\(driveId)/imports/sharelink")
+ }
+}
diff --git a/kDriveCore/Data/Api/Endpoint+Trash.swift b/kDriveCore/Data/Api/Endpoint+Trash.swift
new file mode 100644
index 000000000..d90a1a6d6
--- /dev/null
+++ b/kDriveCore/Data/Api/Endpoint+Trash.swift
@@ -0,0 +1,70 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCore
+import RealmSwift
+
+// MARK: - Trash
+
+public extension Endpoint {
+ static func trash(drive: AbstractDrive) -> Endpoint {
+ return .driveInfo(drive: drive).appending(path: "/trash", queryItems: [FileWith.fileMinimal.toQueryItem()])
+ }
+
+ static func trashV2(drive: AbstractDrive) -> Endpoint {
+ return .driveInfoV2(drive: drive).appending(path: "/trash")
+ }
+
+ static func emptyTrash(drive: AbstractDrive) -> Endpoint {
+ return .driveInfoV2(drive: drive).appending(path: "/trash")
+ }
+
+ static func trashCount(drive: AbstractDrive) -> Endpoint {
+ return .trash(drive: drive).appending(path: "/count")
+ }
+
+ static func trashedInfo(file: AbstractFile) -> Endpoint {
+ return .trash(drive: ProxyDrive(id: file.driveId)).appending(
+ path: "/\(file.id)",
+ queryItems: [FileWith.fileExtra.toQueryItem(), noAvatarDefault()]
+ )
+ }
+
+ static func trashedInfoV2(file: AbstractFile) -> Endpoint {
+ return .trashV2(drive: ProxyDrive(id: file.driveId)).appending(path: "/\(file.id)")
+ }
+
+ static func trashedFiles(of directory: AbstractFile) -> Endpoint {
+ return .trashedInfo(file: directory).appending(path: "/files", queryItems: [FileWith.fileMinimal.toQueryItem()])
+ }
+
+ static func restore(file: AbstractFile) -> Endpoint {
+ return .trashedInfoV2(file: file).appending(path: "/restore")
+ }
+
+ static func trashThumbnail(file: AbstractFile, at date: Date) -> Endpoint {
+ return .trashedInfoV2(file: file).appending(path: "/thumbnail", queryItems: [
+ URLQueryItem(name: "t", value: "\(Int(date.timeIntervalSince1970))")
+ ])
+ }
+
+ static func trashCount(of directory: AbstractFile) -> Endpoint {
+ return .trashedInfo(file: directory).appending(path: "/count")
+ }
+}
diff --git a/kDriveCore/Data/Api/Endpoint.swift b/kDriveCore/Data/Api/Endpoint.swift
index 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..341b22741
--- /dev/null
+++ b/kDriveCore/Data/Api/PublicShareApiFetcher.swift
@@ -0,0 +1,67 @@
+/*
+ Infomaniak kDrive - iOS App
+ Copyright (C) 2024 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Alamofire
+import InfomaniakCore
+import InfomaniakDI
+import InfomaniakLogin
+import Kingfisher
+
+public class PublicShareApiFetcher: ApiFetcher {
+ override public init() {
+ super.init()
+ }
+
+ public func getMetadata(driveId: Int, shareLinkUid: String) async throws -> PublicShareMetadata {
+ let shareLinkInfoUrl = Endpoint.shareLinkInfo(driveId: driveId, shareLinkUid: shareLinkUid).url
+ // TODO: Use authenticated token if availlable
+ let request = Session.default.request(shareLinkInfoUrl)
+ let metadata: PublicShareMetadata = try await perform(request: request)
+ return metadata
+ }
+
+ public func getShareLinkFile(driveId: Int, linkUuid: String, fileId: Int) async throws -> File {
+ let shareLinkFileUrl = Endpoint.shareLinkFile(driveId: driveId, linkUuid: linkUuid, fileId: fileId).url
+ let 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
+ public func shareLinkFileChildren(rootFolderId: Int,
+ publicShareProxy: PublicShareProxy,
+ sortType: SortType,
+ cursor: String? = nil) async throws -> ValidServerResponse<[File]> {
+ let shareLinkFileChildren = Endpoint.shareLinkFileChildren(
+ driveId: publicShareProxy.driveId,
+ linkUuid: publicShareProxy.shareLinkUid,
+ fileId: 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
+ }
+}
diff --git a/kDriveCore/Data/Cache/AccountManager.swift b/kDriveCore/Data/Cache/AccountManager.swift
index 5e780e683..53ee93823 100644
--- a/kDriveCore/Data/Cache/AccountManager.swift
+++ b/kDriveCore/Data/Cache/AccountManager.swift
@@ -24,6 +24,19 @@ import InfomaniakLogin
import RealmSwift
import Sentry
+// TODO: Delete
+public class SomeRefreshTokenDelegate: RefreshTokenDelegate {
+ public init() {}
+
+ public func didUpdateToken(newToken: ApiToken, oldToken: ApiToken) {
+ print("noop")
+ }
+
+ public func didFailRefreshToken(_ token: ApiToken) {
+ print("noop")
+ }
+}
+
public protocol UpdateAccountDelegate: AnyObject {
@MainActor func didUpdateCurrentAccountInformations(_ currentAccount: Account)
}
@@ -63,6 +76,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)
@@ -194,6 +210,34 @@ public class AccountManager: RefreshTokenDelegate, AccountManageable {
}
}
+ public func getInMemoryDriveFileManager(for publicShareId: String, driveId: Int, rootFileId: Int) -> DriveFileManager {
+ if let inMemoryDriveFileManager = driveFileManagers[publicShareId] {
+ return inMemoryDriveFileManager
+ }
+
+ // TODO: Big hack, refactor to allow for non authenticated requests
+ guard let someToken = apiFetchers.values.first?.currentToken else {
+ fatalError("probably no account available")
+ }
+
+ // FileViewModel K.O. without a valid drive in Realm, therefore add one
+ let publicShareDrive = Drive()
+ publicShareDrive.objectId = publicShareId
+ @LazyInjectService var driveInfosManager: DriveInfosManager
+ do {
+ try driveInfosManager.storePublicShareDrive(drive: publicShareDrive)
+ } catch {
+ fatalError("unable to update public share drive in base, \(error)")
+ }
+ let forzenPublicShareDrive = publicShareDrive.freeze()
+
+ let apiFetcher = DriveApiFetcher(token: someToken, delegate: SomeRefreshTokenDelegate())
+ let publicShareProxy = PublicShareProxy(driveId: driveId, fileId: rootFileId, shareLinkUid: publicShareId)
+ let context = DriveFileManagerContext.publicShare(shareProxy: publicShareProxy)
+
+ return DriveFileManager(drive: forzenPublicShareDrive, apiFetcher: apiFetcher, context: context)
+ }
+
public func getFirstAvailableDriveFileManager(for userId: Int) throws -> DriveFileManager {
let userDrives = driveInfosManager.getDrives(for: userId)
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 c7a25ee92..c924e5704 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 {
@@ -84,11 +99,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
@@ -191,9 +218,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)
@@ -383,6 +429,28 @@ 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
+ )
+ 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 9ce515899..b65c3f6d2 100644
--- a/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift
+++ b/kDriveCore/Data/Cache/DriveFileManager/DriveFileManagerConstants.swift
@@ -28,7 +28,7 @@ public enum RealmSchemaVersion {
static let upload: UInt64 = 21
/// Current version of the Drive Realm
- static let drive: UInt64 = 11
+ static let drive: UInt64 = 12
}
public class DriveFileManagerConstants {
diff --git a/kDriveCore/Data/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/DownloadOperation.swift b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
index 47f9a226e..2002c47cc 100644
--- a/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
+++ b/kDriveCore/Data/DownloadQueue/DownloadOperation.swift
@@ -38,6 +38,7 @@ public class DownloadOperation: Operation, DownloadOperationable {
private let fileManager = FileManager.default
private let driveFileManager: DriveFileManager
private let urlSession: FileDownloadSession
+ private let publicShareProxy: PublicShareProxy?
private let itemIdentifier: NSFileProviderItemIdentifier?
private var progressObservation: NSKeyValueObservation?
private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid
@@ -93,19 +94,25 @@ public class DownloadOperation: Operation, DownloadOperationable {
file: File,
driveFileManager: DriveFileManager,
urlSession: FileDownloadSession,
+ publicShareProxy: PublicShareProxy? = nil,
itemIdentifier: NSFileProviderItemIdentifier? = nil
) {
self.file = File(value: file)
self.driveFileManager = driveFileManager
self.urlSession = urlSession
+ self.publicShareProxy = publicShareProxy
self.itemIdentifier = itemIdentifier
}
- public init(file: File, driveFileManager: DriveFileManager, task: URLSessionDownloadTask, urlSession: FileDownloadSession) {
+ public init(file: File,
+ driveFileManager: DriveFileManager,
+ task: URLSessionDownloadTask,
+ urlSession: FileDownloadSession) {
self.file = file
self.driveFileManager = driveFileManager
self.urlSession = urlSession
self.task = task
+ publicShareProxy = nil
itemIdentifier = nil
}
@@ -170,6 +177,53 @@ public class DownloadOperation: Operation, DownloadOperationable {
}
override public func main() {
+ DDLogInfo("[DownloadOperation] Start for \(file.id) with session \(urlSession.identifier)")
+
+ if let publicShareProxy {
+ downloadPublicShareFile(publicShareProxy: publicShareProxy)
+ } else {
+ downloadFile()
+ }
+ }
+
+ private func downloadPublicShareFile(publicShareProxy: PublicShareProxy) {
+ DDLogInfo("[DownloadOperation] Downloading publicShare \(file.id) with session \(urlSession.identifier)")
+
+ let url = Endpoint.download(file: file, publicShareProxy: publicShareProxy).url
+
+ // Add download task to Realm
+ let downloadTask = DownloadTask(
+ fileId: file.id,
+ isDirectory: file.isDirectory,
+ driveId: file.driveId,
+ userId: driveFileManager.drive.userId,
+ sessionId: urlSession.identifier,
+ sessionUrl: url.absoluteString
+ )
+
+ try? uploadsDatabase.writeTransaction { writableRealm in
+ writableRealm.add(downloadTask, update: .modified)
+ }
+
+ let request = URLRequest(url: url)
+ task = urlSession.downloadTask(with: request, completionHandler: downloadCompletion)
+ progressObservation = task?.progress.observe(\.fractionCompleted, options: .new) { [fileId = file.id] _, value in
+ guard let newValue = value.newValue else {
+ return
+ }
+ DownloadQueue.instance.publishProgress(newValue, for: fileId)
+ }
+ if let itemIdentifier {
+ driveInfosManager.getFileProviderManager(for: driveFileManager.drive) { manager in
+ manager.register(self.task!, forItemWithIdentifier: itemIdentifier) { _ in
+ // META: keep SonarCloud happy
+ }
+ }
+ }
+ task?.resume()
+ }
+
+ private func downloadFile() {
DDLogInfo("[DownloadOperation] Downloading \(file.id) with session \(urlSession.identifier)")
let url = Endpoint.download(file: file).url
@@ -207,7 +261,7 @@ public class DownloadOperation: Operation, DownloadOperationable {
}
task?.resume()
} else {
- error = .localError // Other error?
+ error = .unknownToken // Other error?
end(sessionUrl: url)
}
}
@@ -288,7 +342,7 @@ public class DownloadOperation: Operation, DownloadOperationable {
return
}
- assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation")
+ assert(file.isDownloaded, "Expecting to be downloaded at the end of the downloadOperation error:\(error)")
try? uploadsDatabase.writeTransaction { writableRealm in
guard let task = writableRealm.objects(DownloadTask.self)
diff --git a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
index a0f7450b6..db2eaf452 100644
--- a/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
+++ b/kDriveCore/Data/DownloadQueue/DownloadQueue.swift
@@ -112,6 +112,40 @@ public final class DownloadQueue: ParallelismHeuristicDelegate {
// MARK: - Public methods
+ public func addPublicShareToQueue(file: File,
+ driveFileManager: DriveFileManager,
+ publicShareProxy: PublicShareProxy,
+ itemIdentifier: NSFileProviderItemIdentifier? = nil) {
+ Log.downloadQueue("addPublicShareToQueue file:\(file.id)")
+ let file = file.freezeIfNeeded()
+
+ dispatchQueue.async {
+ guard !self.hasOperation(for: file.id) else {
+ Log.downloadQueue("Already in download queue, skipping \(file.id)", level: .error)
+ return
+ }
+
+ OperationQueueHelper.disableIdleTimer(true)
+
+ let operation = DownloadOperation(
+ file: file,
+ driveFileManager: driveFileManager,
+ urlSession: self.bestSession,
+ publicShareProxy: publicShareProxy,
+ itemIdentifier: itemIdentifier
+ )
+ operation.completionBlock = {
+ self.dispatchQueue.async {
+ self.operationsInQueue.removeValue(forKey: file.id)
+ self.publishFileDownloaded(fileId: file.id, error: operation.error)
+ OperationQueueHelper.disableIdleTimer(false, hasOperationsInQueue: !self.operationsInQueue.isEmpty)
+ }
+ }
+ self.operationQueue.addOperation(operation)
+ self.operationsInQueue[file.id] = operation
+ }
+ }
+
public func addToQueue(file: File,
userId: Int,
itemIdentifier: NSFileProviderItemIdentifier? = nil) {
diff --git a/kDriveCore/Data/Models/Drive/Drive.swift b/kDriveCore/Data/Models/Drive/Drive.swift
index c6ff0c515..d6d8bbef9 100644
--- a/kDriveCore/Data/Models/Drive/Drive.swift
+++ b/kDriveCore/Data/Models/Drive/Drive.swift
@@ -190,8 +190,9 @@ public final class Drive: Object, Codable {
// File is not managed by Realm: cannot use the `.sorted(by:)` method :(
fileCategoriesIds = file.categories.sorted { $0.addedAt.compare($1.addedAt) == .orderedAscending }.map(\.categoryId)
}
- let filteredCategories = categories.filter(NSPredicate(format: "id IN %@", fileCategoriesIds))
+
// Sort the categories
+ let filteredCategories = categories.filter("id IN %@", fileCategoriesIds)
return fileCategoriesIds.compactMap { id in filteredCategories.first { $0.id == id } }
}
diff --git a/kDriveCore/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 ffdc71d8c..79c36649d 100644
--- a/kDriveCore/Data/Models/File.swift
+++ b/kDriveCore/Data/Models/File.swift
@@ -182,6 +182,19 @@ public enum ConvertedType: String, CaseIterable {
public static let ignoreThumbnailTypes = downloadableTypes
}
+/// Minimal data needed to query a PublicShare
+public struct PublicShareProxy {
+ 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 enum SortType: String {
case nameAZ
case nameZA
@@ -541,7 +554,8 @@ public final class File: Object, Codable {
public var isDownloaded: Bool {
let localPath = localUrl.path
- guard fileManager.fileExists(atPath: localPath) else {
+ let temporaryPath = temporaryUrl.path
+ guard fileManager.fileExists(atPath: localPath) || fileManager.fileExists(atPath: temporaryPath) else {
DDLogError("[File] no local copy to read from")
return false
}
diff --git a/kDriveCore/Data/Models/Rights.swift b/kDriveCore/Data/Models/Rights.swift
index 6d599a289..322db9750 100644
--- a/kDriveCore/Data/Models/Rights.swift
+++ b/kDriveCore/Data/Models/Rights.swift
@@ -44,7 +44,8 @@ public class Rights: EmbeddedObject, Codable {
/// Right to use and give team access
@Persisted public var canUseTeam: Bool
- // Directory capabilities
+ // MARK: Directory capabilities
+
/// Right to add new child directory
@Persisted public var canCreateDirectory: Bool
/// Right to add new child file
@@ -56,6 +57,21 @@ public class Rights: EmbeddedObject, Codable {
/// Right to use convert directory into dropbox
@Persisted public var canBecomeDropbox: Bool
+ // MARK: Public share
+
+ /// Can edit
+ @Persisted public var canEdit: Bool
+ /// Can see stats
+ @Persisted public var canSeeStats: Bool
+ /// Can see info
+ @Persisted public var canSeeInfo: Bool
+ /// Can download
+ @Persisted public var canDownload: Bool
+ /// Can comment
+ @Persisted public var canComment: Bool
+ /// Can request access
+ @Persisted public var canRequestAccess: Bool
+
enum CodingKeys: String, CodingKey {
case canShow
case canRead
@@ -73,26 +89,38 @@ public class Rights: EmbeddedObject, Codable {
case canUpload
case canMoveInto
case canBecomeDropbox
+ case canEdit
+ case canSeeStats
+ case canSeeInfo
+ case canDownload
+ case canComment
+ case canRequestAccess
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
- canShow = try container.decode(Bool.self, forKey: .canShow)
- canRead = try container.decode(Bool.self, forKey: .canRead)
- canWrite = try container.decode(Bool.self, forKey: .canWrite)
- canShare = try container.decode(Bool.self, forKey: .canShare)
- canLeave = try container.decode(Bool.self, forKey: .canLeave)
- canDelete = try container.decode(Bool.self, forKey: .canDelete)
- canRename = try container.decode(Bool.self, forKey: .canRename)
- canMove = try container.decode(Bool.self, forKey: .canMove)
- canBecomeSharelink = try container.decode(Bool.self, forKey: .canBecomeSharelink)
- canUseFavorite = try container.decode(Bool.self, forKey: .canUseFavorite)
- canUseTeam = try container.decode(Bool.self, forKey: .canUseTeam)
+ canShow = try container.decodeIfPresent(Bool.self, forKey: .canShow) ?? true
+ canRead = try container.decodeIfPresent(Bool.self, forKey: .canRead) ?? true
+ canWrite = try container.decodeIfPresent(Bool.self, forKey: .canWrite) ?? false
+ canShare = try container.decodeIfPresent(Bool.self, forKey: .canShare) ?? false
+ canLeave = try container.decodeIfPresent(Bool.self, forKey: .canLeave) ?? false
+ canDelete = try container.decodeIfPresent(Bool.self, forKey: .canDelete) ?? false
+ canRename = try container.decodeIfPresent(Bool.self, forKey: .canRename) ?? false
+ canMove = try container.decodeIfPresent(Bool.self, forKey: .canMove) ?? false
+ canBecomeSharelink = try container.decodeIfPresent(Bool.self, forKey: .canBecomeSharelink) ?? false
+ canUseFavorite = try container.decodeIfPresent(Bool.self, forKey: .canUseFavorite) ?? false
+ canUseTeam = try container.decodeIfPresent(Bool.self, forKey: .canUseTeam) ?? false
canCreateDirectory = try container.decodeIfPresent(Bool.self, forKey: .canCreateDirectory) ?? false
canCreateFile = try container.decodeIfPresent(Bool.self, forKey: .canCreateFile) ?? false
canUpload = try container.decodeIfPresent(Bool.self, forKey: .canUpload) ?? false
canMoveInto = try container.decodeIfPresent(Bool.self, forKey: .canMoveInto) ?? false
canBecomeDropbox = try container.decodeIfPresent(Bool.self, forKey: .canBecomeDropbox) ?? false
+ canEdit = try container.decodeIfPresent(Bool.self, forKey: .canEdit) ?? false
+ canSeeStats = try container.decodeIfPresent(Bool.self, forKey: .canSeeStats) ?? false
+ canSeeInfo = try container.decodeIfPresent(Bool.self, forKey: .canSeeInfo) ?? false
+ canDownload = try container.decodeIfPresent(Bool.self, forKey: .canDownload) ?? false
+ canComment = try container.decodeIfPresent(Bool.self, forKey: .canComment) ?? false
+ canRequestAccess = try container.decodeIfPresent(Bool.self, forKey: .canRequestAccess) ?? false
}
override public init() {
diff --git a/kDriveCore/Utils/AppNavigable.swift b/kDriveCore/Utils/AppNavigable.swift
index dc54ce51a..bcd52b59e 100644
--- a/kDriveCore/Utils/AppNavigable.swift
+++ b/kDriveCore/Utils/AppNavigable.swift
@@ -64,6 +64,14 @@ public protocol RouterFileNavigable {
/// - office: Open in only office
@MainActor func present(file: File, driveFileManager: DriveFileManager, office: Bool)
+ /// Present a file list for a public share, 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 1af20238b..80ab6d180 100644
--- a/kDriveCore/Utils/DeeplinkParser.swift
+++ b/kDriveCore/Utils/DeeplinkParser.swift
@@ -17,6 +17,7 @@
*/
import InfomaniakDI
+import MatomoTracker
import SwiftUI
/// Deeplink entrypoint
@@ -50,6 +51,7 @@ public struct DeeplinkParser: DeeplinkParsable {
let driveId = params.first(where: { $0.name == "driveId" })?.value,
let driveIdInt = Int(driveId), let userIdInt = Int(userId) {
await router.navigate(to: .store(driveId: driveIdInt, userId: userIdInt))
+ MatomoUtils.trackDeeplink(name: DeeplinkPath.store.rawValue)
return true
} else if components.host == DeeplinkPath.file.rawValue,
@@ -57,6 +59,7 @@ public struct DeeplinkParser: DeeplinkParsable {
let fileUrl = URL(fileURLWithPath: filePath)
let file = ImportedFile(name: fileUrl.lastPathComponent, path: fileUrl, uti: fileUrl.uti ?? .data)
await router.navigate(to: .saveFile(file: file))
+ MatomoUtils.trackDeeplink(name: DeeplinkPath.file.rawValue)
return true
}
diff --git a/kDriveCore/Utils/MatomoUtils.swift b/kDriveCore/Utils/MatomoUtils.swift
index c63b72b29..1d0c46478 100644
--- a/kDriveCore/Utils/MatomoUtils.swift
+++ b/kDriveCore/Utils/MatomoUtils.swift
@@ -44,7 +44,7 @@ public enum MatomoUtils {
public enum EventCategory: String {
case newElement, fileListFileAction, picturesFileAction, fileInfo, shareAndRights, colorFolder, categories, search,
fileList, comment, drive, account, settings, photoSync, home, displayList, inApp, trash,
- dropbox, preview, mediaPlayer, shortcuts, appReview
+ dropbox, preview, mediaPlayer, shortcuts, appReview, deeplink, publicShareAction, publicSharePasswordAction
}
public enum UserAction: String {
@@ -64,7 +64,12 @@ public enum MatomoUtils {
shared.track(view: view)
}
- public static func track(eventWithCategory category: EventCategory, action: UserAction = .click, name: String, value: Float? = nil) {
+ public static func track(
+ eventWithCategory category: EventCategory,
+ action: UserAction = .click,
+ name: String,
+ value: Float? = nil
+ ) {
shared.track(eventWithCategory: category.rawValue, action: action.rawValue, name: name, value: value)
}
@@ -122,4 +127,24 @@ public enum MatomoUtils {
public static func trackMediaPlayer(leaveAt percentage: Double?) {
track(eventWithCategory: .mediaPlayer, name: "duration", value: Float(percentage ?? 0))
}
+
+ // MARK: - Deeplink
+
+ public static func trackDeeplink(name: String) {
+ track(eventWithCategory: .deeplink, name: name)
+ }
+
+ // MARK: - Public Share
+
+ public static func trackAddToMykDrive() {
+ track(eventWithCategory: .publicShareAction, name: "saveToKDrive")
+ }
+
+ public static func trackAddBulkToMykDrive() {
+ track(eventWithCategory: .publicShareAction, name: "bulkSaveToKDrive")
+ }
+
+ public static func trackPublicSharePasswordAction() {
+ track(eventWithCategory: .publicSharePasswordAction, name: "openInBrowser")
+ }
}