diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index 68c13890b2..97d8cad64d 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -8,7 +8,10 @@ import RealmSwift import SwiftUI class NCFiles: NCCollectionViewCommon { + internal var fileNameBlink: String? + internal var fileNameOpen: String? + internal var lastOffsetY: CGFloat = 0 internal var lastScrollTime: TimeInterval = 0 internal var accumulatedScrollDown: CGFloat = 0 @@ -41,10 +44,7 @@ class NCFiles: NCCollectionViewCommon { } NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { _ in - Task { - await self.stopSyncMetadata() - await self.searchOperationHandle.cancel() - } + self.stopSyncMetadata() } if self.serverUrl.isEmpty { @@ -107,15 +107,23 @@ class NCFiles: NCCollectionViewCommon { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") + + // Preload hidden NCMedia so it's ready without being shown +// NCMediaPreloader.shared.preloadIfNeeded() + if !self.dataSource.isEmpty() { - blinkCell(fileName: self.fileNameBlink) - fileNameBlink = nil + self.blinkCell(fileName: self.fileNameBlink) + self.openFile(fileName: self.fileNameOpen) + self.fileNameBlink = nil + self.fileNameOpen = nil } Task { // Plus Menu reload - await self.mainNavigationController?.menuPlus?.create(session: session) - + let capabilities = await database.getCapabilities(account: self.session.account) ?? NKCapabilities.Capabilities() + await mainNavigationController?.createPlusMenu(session: self.session, capabilities: capabilities) // Server data if !isSearchingMode { await getServerData() @@ -126,8 +134,8 @@ class NCFiles: NCCollectionViewCommon { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + stopSyncMetadata() Task { - await stopSyncMetadata() await NCNetworking.shared.networkingTasks.cancel(identifier: "\(self.serverUrl)_NCFiles") } } @@ -136,6 +144,7 @@ class NCFiles: NCCollectionViewCommon { super.viewDidDisappear(animated) fileNameBlink = nil + fileNameOpen = nil } // MARK: - DataSource @@ -183,11 +192,27 @@ class NCFiles: NCCollectionViewCommon { startSyncMetadata(metadatas: self.dataSource.getMetadatas()) } - await networking.networkingTasks.cancel(identifier: "\(self.serverUrl)_NCFiles") + Task { + await networking.networkingTasks.cancel(identifier: "\(self.serverUrl)_NCFiles") + } guard !isSearchingMode else { - await self.search() - return + return networkSearch() + } + + func downloadMetadata(_ metadata: tableMetadata) async -> Bool { + let fileSize = utilityFileSystem.fileProviderStorageSize(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + guard fileSize > 0 else { return false } + + if let tblLocalFile = await database.getTableLocalFileAsync(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { + if tblLocalFile.etag != metadata.etag { + return true + } + } + return false } let resultsReadFolder = await networkReadFolderAsync(serverUrl: self.serverUrl, forced: forced) @@ -198,7 +223,7 @@ class NCFiles: NCCollectionViewCommon { let metadatasForDownload: [tableMetadata] = resultsReadFolder.metadatas ?? self.dataSource.getMetadatas() Task.detached(priority: .utility) { for metadata in metadatasForDownload where !metadata.directory { - if await self.downloadMetadata(metadata) { + if await downloadMetadata(metadata) { if let metadata = await self.database.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, session: NCNetworking.shared.sessionDownload, selector: NCGlobal.shared.selectorDownloadFile, @@ -212,27 +237,9 @@ class NCFiles: NCCollectionViewCommon { await self.reloadDataSource() } - private func downloadMetadata(_ metadata: tableMetadata) async -> Bool { - let fileSize = utilityFileSystem.fileProviderStorageSize(metadata.ocId, - fileName: metadata.fileNameView, - userId: metadata.userId, - urlBase: metadata.urlBase) - guard fileSize > 0 else { - return false - } - - if let tblLocalFile = await database.getTableLocalFileAsync(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { - if tblLocalFile.etag != metadata.etag { - return true - } - } - return false - } - private func networkReadFolderAsync(serverUrl: String, forced: Bool) async -> (metadatas: [tableMetadata]?, error: NKError, reloadRequired: Bool) { var reloadRequired: Bool = false - let account = session.account - let resultsReadFile = await NCNetworking.shared.readFileAsync(serverUrlFileName: serverUrl, account: account) { task in + let resultsReadFile = await NCNetworking.shared.readFileAsync(serverUrlFileName: serverUrl, account: session.account) { task in Task { await NCNetworking.shared.networkingTasks.track(identifier: "\(self.serverUrl)_NCFiles", task: task) } @@ -247,12 +254,12 @@ class NCFiles: NCCollectionViewCommon { let e2eEncrypted = metadata.e2eEncrypted let ocId = metadata.ocId - await self.database.updateDirectoryRichWorkspaceAsync(metadata.richWorkspace, account: account, serverUrl: serverUrl) + await self.database.updateDirectoryRichWorkspaceAsync(metadata.richWorkspace, account: resultsReadFile.account, serverUrl: serverUrl) let tableDirectory = await self.database.getTableDirectoryAsync(ocId: metadata.ocId) // Verify LivePhoto // - reloadRequired = await networking.setLivePhoto(account: account) + reloadRequired = await networking.setLivePhoto(account: resultsReadFile.account) await NCManageDatabase.shared.deleteLivePhotoError() let shouldSkipUpdate: Bool = ( @@ -269,11 +276,9 @@ class NCFiles: NCCollectionViewCommon { startGUIGetServerData() let options = NKRequestOptions(timeout: 180) - let resultsReadFolder = await NCNetworking.shared.readFolderAsync( - serverUrl: serverUrl, - account: account, - options: options - ) { task in + let (account, metadataFolder, metadatas, error) = await NCNetworking.shared.readFolderAsync(serverUrl: serverUrl, + account: session.account, + options: options) { task in Task { await NCNetworking.shared.networkingTasks.track(identifier: "\(self.serverUrl)_NCFiles", task: task) } @@ -282,8 +287,8 @@ class NCFiles: NCCollectionViewCommon { } } - guard resultsReadFolder.error == .success else { - return(nil, resultsReadFolder.error, reloadRequired) + guard error == .success else { + return(nil, error, reloadRequired) } reloadRequired = true @@ -292,64 +297,81 @@ class NCFiles: NCCollectionViewCommon { self.richWorkspaceText = metadataFolder.richWorkspace } + // + // E2EE section + // + guard e2eEncrypted, - let metadatas = resultsReadFolder.metadatas, + let metadatas, + !metadatas.isEmpty, NCPreferences().isEndToEndEnabled(account: account), await !NCNetworkingE2EE().isInUpload(account: account, serverUrl: serverUrl) else { - return(resultsReadFolder.metadatas, resultsReadFolder.error, reloadRequired) + return(metadatas, error, reloadRequired) } - // - // E2EE section - // - let lock = await self.database.getE2ETokenLockAsync(account: account, serverUrl: serverUrl) - let resultsE2eeGetMetadata = await NCNetworkingE2EE().getMetadata(fileId: ocId, e2eToken: lock?.e2eToken, account: account) + if let e2eToken = lock?.e2eToken { + nkLog(tag: self.global.logTagE2EE, message: "Tocken: \(e2eToken)", minimumLogLevel: .verbose) + } - guard resultsE2eeGetMetadata.error == .success, - let e2eMetadata = resultsE2eeGetMetadata.e2eMetadata, - let version = resultsE2eeGetMetadata.version else { - if resultsE2eeGetMetadata.error.errorCode == NCGlobal.shared.errorResourceNotFound { + let results = await NCNetworkingE2EE().getMetadata(fileId: ocId, e2eToken: lock?.e2eToken, account: account) + + nkLog(tag: self.global.logTagE2EE, message: "Get metadata with error: \(results.error.errorCode)") + nkLog(tag: self.global.logTagE2EE, message: "Get metadata with metadata: \(results.e2eMetadata ?? ""), signature: \(results.signature ?? ""), version \(results.version ?? "")", minimumLogLevel: .verbose) + + guard results.error == .success, + let e2eMetadata = results.e2eMetadata, + let version = results.version else { + + // No metadata fount, re-send it + if results.error.errorCode == NCGlobal.shared.errorResourceNotFound { + NCContentPresenter().showInfo(description: "Metadata not found") let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, account: account) if error != .success { - await showErrorBanner(windowScene: windowScene, - text: error.errorDescription, + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, errorCode: error.errorCode) } } else { - await showErrorBanner(windowScene: windowScene, - text: resultsE2eeGetMetadata.error.errorDescription, - errorCode: resultsE2eeGetMetadata.error.errorCode) + // show error + Task {@MainActor in + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } } - return(metadatas, resultsE2eeGetMetadata.error, reloadRequired) + + return(metadatas, error, reloadRequired) } - var error = await NCEndToEndMetadata().decodeMetadata(e2eMetadata, - signature: resultsE2eeGetMetadata.signature, - serverUrl: serverUrl, session: self.session) + let errorDecodeMetadata = await NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: results.signature, serverUrl: serverUrl, session: self.session) + nkLog(debug: "Decode e2ee metadata with error: \(errorDecodeMetadata.errorCode)") - if error == .success { + if errorDecodeMetadata == .success { let capabilities = await NKCapabilities.shared.getCapabilities(for: self.session.account) - if version == "v1", capabilities.e2EEApiVersion.hasPrefix("2.") { - await showInfoBanner(windowScene: windowScene, text: "Conversion metadata v1 to v2 required, please wait...") + if version == "v1", capabilities.e2EEApiVersion == NCGlobal.shared.e2eeVersionV20 { + NCContentPresenter().showInfo(description: "Conversion metadata v1 to v2 required, please wait...") nkLog(tag: self.global.logTagE2EE, message: "Conversion v1 to v2") NCActivityIndicator.shared.start() - error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, updateVersionV1V2: true, account: account) + let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, updateVersionV1V2: true, account: account) if error != .success { - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) + Task {@MainActor in + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } } NCActivityIndicator.shared.stop() } } else { // Client Diagnostic await self.database.addDiagnosticAsync(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors) - await showErrorBanner(windowScene: windowScene, text: error.errorDescription, errorCode: error.errorCode) - } - - // Error: Go back - if error != .success { - navigationController?.popViewController(animated: false) + Task {@MainActor in + await showErrorBanner(controller: self.controller, + errorDescription: error.errorDescription, + errorCode: error.errorCode) + } } return (metadatas, error, reloadRequired) @@ -375,11 +397,15 @@ class NCFiles: NCCollectionViewCommon { } } - func open(metadata: tableMetadata?) async { - guard let metadata else { - return + func openFile(fileName: String?) { + if let fileName = fileName, let metadata = database.getMetadata(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", session.account, self.serverUrl, fileName)) { + let indexPath = self.dataSource.getIndexPathMetadata(ocId: metadata.ocId) + if let indexPath = indexPath { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.collectionView(self.collectionView, didSelectItemAt: indexPath) + } + } } - await didSelectMetadata(metadata, withOcIds: false) } // MARK: - NCAccountSettingsModelDelegate @@ -411,3 +437,4 @@ class NCFiles: NCCollectionViewCommon { } } } + diff --git a/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift new file mode 100644 index 0000000000..b861d54f00 --- /dev/null +++ b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift @@ -0,0 +1,136 @@ +// +// NCCellProtocol.swift +// Nextcloud +// +// Created by Philippe Weidmann on 05.06.20. +// Copyright © 2020 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 UIKit + +protocol NCCellProtocol { + var fileAvatarImageView: UIImageView? { get } + var fileAccount: String? { get set } + var fileOcId: String? { get set } + var fileOcIdTransfer: String? { get set } + var filePreviewImageView: UIImageView? { get set } + var fileUser: String? { get set } + var fileTitleLabel: UILabel? { get set } + var fileInfoLabel: UILabel? { get set } + var fileSubinfoLabel: UILabel? { get set } + var fileStatusImage: UIImageView? { get set } + var fileLocalImage: UIImageView? { get set } + var fileFavoriteImage: UIImageView? { get set } + var fileSharedImage: UIImageView? { get set } + var fileMoreImage: UIImageView? { get set } + var cellSeparatorView: UIView? { get set } + + func titleInfoTrailingDefault() + func titleInfoTrailingFull() + func writeInfoDateSize(date: NSDate, size: Int64) + func setButtonMore(image: UIImage) + func hideImageItem(_ status: Bool) + func hideImageFavorite(_ status: Bool) + func hideImageStatus(_ status: Bool) + func hideImageLocal(_ status: Bool) + func hideLabelTitle(_ status: Bool) + func hideLabelInfo(_ status: Bool) + func hideLabelSubinfo(_ status: Bool) + func hideButtonShare(_ status: Bool) + func hideButtonMore(_ status: Bool) + func selected(_ status: Bool, isEditMode: Bool) + func setAccessibility(label: String, value: String) + func setTags(tags: [String]) + func setIconOutlines() +} + +extension NCCellProtocol { + var fileAvatarImageView: UIImageView? { + return nil + } + var fileAccount: String? { + get { return nil } + set {} + } + var fileOcId: String? { + get { return nil } + set {} + } + var fileOcIdTransfer: String? { + get { return nil } + set {} + } + var filePreviewImageView: UIImageView? { + get { return nil } + set {} + } + var fileTitleLabel: UILabel? { + get { return nil } + set {} + } + var fileInfoLabel: UILabel? { + get { return nil } + set { } + } + var fileSubinfoLabel: UILabel? { + get { return nil } + set { } + } + var fileStatusImage: UIImageView? { + get { return nil } + set {} + } + var fileLocalImage: UIImageView? { + get { return nil } + set {} + } + var fileFavoriteImage: UIImageView? { + get { return nil } + set {} + } + var fileSharedImage: UIImageView? { + get { return nil } + set {} + } + var fileMoreImage: UIImageView? { + get { return nil } + set {} + } + var cellSeparatorView: UIView? { + get { return nil } + set {} + } + + func titleInfoTrailingDefault() {} + func titleInfoTrailingFull() {} + func writeInfoDateSize(date: NSDate, size: Int64) {} + func setButtonMore(image: UIImage) {} + func hideImageItem(_ status: Bool) {} + func hideImageFavorite(_ status: Bool) {} + func hideImageStatus(_ status: Bool) {} + func hideImageLocal(_ status: Bool) {} + func hideLabelTitle(_ status: Bool) {} + func hideLabelInfo(_ status: Bool) {} + func hideLabelSubinfo(_ status: Bool) {} + func hideButtonShare(_ status: Bool) {} + func hideButtonMore(_ status: Bool) {} + func selected(_ status: Bool, isEditMode: Bool) {} + func setAccessibility(label: String, value: String) {} + func setTags(tags: [String]) {} + func setIconOutlines() {} +} diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 57831aa19b..c4996f0c97 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -163,6 +163,12 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP } func selected(_ status: Bool, isEditMode: Bool, color: UIColor) { + // E2EE - remove encrypt folder selection + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(self.metadata?.ocId), metadata.e2eEncrypted { + imageSelect.isHidden = true + } else { + imageSelect.isHidden = isEditMode ? false : true + } if isEditMode { buttonMore.isHidden = true accessibilityCustomActions = nil @@ -173,6 +179,14 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP imageVisualEffect.alpha = status ? 1 : 0 imageSelect.alpha = status ? 1 : 0 imageSelect.image = NCImageCache.shared.getImageCheckedYes(color: color) +// if status { +// imageSelect.image = NCImageCache.shared.getImageCheckedYes() +// imageVisualEffect.isHidden = false +// } else { +// imageSelect.image = NCImageCache.shared.getImageCheckedNo() +// backgroundView = nil +// imageVisualEffect.isHidden = true +// } } func writeInfoDateSize(date: NSDate, size: Int64) { @@ -189,6 +203,14 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMainP accessibilityLabel = label accessibilityValue = value } + + func setIconOutlines() { + if imageStatus.image != nil { + imageStatus.makeCircularBackground(withColor: .systemBackground) + } else { + imageStatus.backgroundColor = .clear + } + } } // MARK: - Grid Layout diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift index 2e29d9690a..6d5a681752 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift @@ -45,10 +45,45 @@ class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellMain return nil } + @objc private func handleTapObserver(_ g: UITapGestureRecognizer) { + let location = g.location(in: contentView) + + if buttonMore.frame.contains(location) { + delegate?.onMenuIntent(with: metadata) + } + } + + func setButtonMore(image: UIImage) { + buttonMore.setImage(image, for: .normal) + } + + func hideButtonMore(_ status: Bool) { + // buttonMore.isHidden = status NO MORE USED + } + + func hideImageStatus(_ status: Bool) { + imageStatus.isHidden = status + } + func selected(_ status: Bool, isEditMode: Bool, color: UIColor) { imageVisualEffect.alpha = status ? 1 : 0 imageSelect.alpha = status ? 1 : 0 imageSelect.image = NCImageCache.shared.getImageCheckedYes(color: color) + // E2EE - remove encrypt folder selection + if let metadata = NCManageDatabase.shared.getMetadataFromOcId(self.metadata?.ocId), metadata.e2eEncrypted { + imageSelect.isHidden = true + } else { + imageSelect.isHidden = isEditMode ? false : true + } + if status { +// imageSelect.isHidden = false + imageVisualEffect.isHidden = false + imageSelect.image = NCImageCache.shared.getImageCheckedYes() + } else { +// imageSelect.isHidden = true + imageVisualEffect.isHidden = true + imageSelect.image = NCImageCache.shared.getImageCheckedNo() + } } func setAccessibility(label: String, value: String) { diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 4a57032ba3..d89eef4a4e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -143,10 +143,15 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { } if self.isEditMode { - if let index = self.fileSelect.firstIndex(of: metadata.ocId) { - self.fileSelect.remove(at: index) - } else { - self.fileSelect.append(metadata.ocId) + if !metadata.e2eEncrypted { + if let index = self.fileSelect.firstIndex(of: metadata.ocId) { + self.fileSelect.remove(at: index) + } else { + self.fileSelect.append(metadata.ocId) + } + self.collectionView.reloadItems(at: [indexPath]) + self.tabBarSelect?.update(fileSelect: self.fileSelect, metadatas: self.getSelectedMetadatas(), userId: metadata.userId) + // self.collectionView.reloadSections(IndexSet(integer: indexPath.section)) } self.collectionView.reloadItems(at: [indexPath]) self.tabBarSelect?.update(fileSelect: self.fileSelect, metadatas: self.getSelectedMetadatas(), userId: metadata.userId) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift new file mode 100644 index 0000000000..07dfe7c91f --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBar.swift @@ -0,0 +1,381 @@ +// +// NCCollectionViewCommon+SelectTabBar.swift +// Nextcloud +// +// Created by Milen on 01.03.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 UIKit +import Foundation +import NextcloudKit + +extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate, NCSelectableNavigationView { + + func selectAll() { + fileSelect = selectableDataSource.compactMap({ $0.primaryKeyValue }) + tabBarSelect?.update(fileSelect: fileSelect, metadatas: getSelectedMetadatas(), userId: session.userId) + DispatchQueue.main.async { + self.collectionView.reloadData() + self.setNavigationRightItems(enableMenu: false) + } + } + + func delete() { + var alertStyle = UIAlertController.Style.actionSheet + if UIDevice.current.userInterfaceIdiom == .pad { alertStyle = .alert } + let alertController = UIAlertController(title: NSLocalizedString("_confirm_delete_selected_", comment: ""), message: nil, preferredStyle: alertStyle) + let metadatas = getSelectedMetadatas() + let canDeleteServer = metadatas.allSatisfy { !$0.lock } + + if canDeleteServer { +// alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .destructive) { [self] _ in +// NCNetworking.shared.deleteMetadatas(metadatas, sceneIdentifier: self.controller?.sceneIdentifier) +// NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) +// toggleSelect() + alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .destructive) { _ in + self.networking.setStatusWaitDelete(metadatas: metadatas, sceneIdentifier: self.controller?.sceneIdentifier) + self.setEditMode(false) + Task { + await self.reloadDataSource() + } + }) + } + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_remove_local_file_", comment: ""), style: .destructive) { [self] (_: UIAlertAction) in + let copyMetadatas = metadatas + + Task { + var error = NKError() + var ocId: [String] = [] + for metadata in copyMetadatas where error == .success { + error = await NCNetworking.shared.deleteCache(metadata, sceneIdentifier: self.controller?.sceneIdentifier) + if error == .success { + ocId.append(metadata.ocId) + } + error = await self.networking.deleteCache(metadata, sceneIdentifier: self.controller?.sceneIdentifier) + } + NotificationCenter.default.postOnMainThread(name: self.global.notificationCenterDeleteFile, userInfo: ["ocId": ocId, "error": error]) + } + toggleSelect() + }) + + alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { (_: UIAlertAction) in }) + self.present(alertController, animated: true, completion: nil) + } + + func move() { + let metadatas = getSelectedMetadatas() + + NCActionCenter.shared.openSelectView(items: metadatas, controller: self.controller) + toggleSelect() + NCDownloadAction.shared.openSelectView(items: metadatas, controller: self.controller) + setEditMode(false) +// NCActionCenter.shared.openSelectView(items: metadatas, controller: self.controller) +// toggleSelect() + } + + func share() { + let metadatas = getSelectedMetadatas() + NCActionCenter.shared.openActivityViewController(selectedMetadata: metadatas, controller: self.controller) + toggleSelect() + NCDownloadAction.shared.openActivityViewController(selectedMetadata: metadatas, controller: self.controller, sender: nil) + setEditMode(false) +// NCActionCenter.shared.openActivityViewController(selectedMetadata: metadatas, controller: self.controller) +// toggleSelect() + } + + func saveAsAvailableOffline(isAnyOffline: Bool) { + let metadatas = getSelectedMetadatas() + if !isAnyOffline, metadatas.count > 3 { + let alert = UIAlertController( + title: NSLocalizedString("_set_available_offline_", comment: ""), + message: NSLocalizedString("_select_offline_warning_", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("_continue_", comment: ""), style: .default, handler: { _ in + Task { + for metadata in metadatas { + await NCDownloadAction.shared.setMetadataAvalableOffline(metadata, isOffline: isAnyOffline) + } + } + self.setEditMode(false) + + })) + alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel)) + self.present(alert, animated: true) + } else { +// metadatas.forEach { NCActionCenter.shared.setMetadataAvalableOffline($0, isOffline: isAnyOffline) } +// toggleSelect() + Task { + for metadata in metadatas { + await NCDownloadAction.shared.setMetadataAvalableOffline(metadata, isOffline: isAnyOffline) + } + } + setEditMode(false) + + } + } + + func lock(isAnyLocked: Bool) { + let metadatas = getSelectedMetadatas() + for metadata in metadatas where metadata.lock == isAnyLocked { + self.networking.lockUnlockFile(metadata, shoulLock: !isAnyLocked) + } + toggleSelect() + } + + func getSelectedMetadatas() -> [tableMetadata] { + var selectedMetadatas: [tableMetadata] = [] + for ocId in fileSelect { + guard let metadata = database.getMetadataFromOcId(ocId) else { continue } + selectedMetadatas.append(metadata) + } + return selectedMetadatas + } + + func setEditMode(_ editMode: Bool) { + isEditMode = editMode + fileSelect.removeAll() + + navigationItem.hidesBackButton = editMode + navigationController?.interactivePopGestureRecognizer?.isEnabled = !editMode + searchController(enabled: !editMode) + + if editMode { + navigationItem.leftBarButtonItems = nil + } else { + ///Magentacloud branding changes hide user account button on left navigation bar + setNavigationLeftItems() + } + + navigationController?.interactivePopGestureRecognizer?.isEnabled = !editMode + navigationItem.hidesBackButton = editMode + searchController(enabled: !editMode) + self.setNavigationRightItems(enableMenu: true) + self.collectionView.reloadData() + } + + func convertLivePhoto(metadataFirst: tableMetadata?, metadataLast: tableMetadata?) { + if let metadataFirst, let metadataLast { + Task { + await self.networking.setLivePhoto(metadataFirst: metadataFirst, metadataLast: metadataLast) + } + } + setEditMode(false) + } + + /// If explicit `isOn` is not set, it will invert `isEditMode` + func toggleSelect(isOn: Bool? = nil) { + DispatchQueue.main.async { + self.isEditMode = isOn ?? !self.isEditMode + self.selectOcId.removeAll() + self.setNavigationLeftItems() + self.setNavigationRightItems() + self.collectionView.reloadData() + self.setEditMode(self.isEditMode) + } + } + + func createMenuActions() -> [NCMenuAction] { + var actions = [NCMenuAction]() + + actions.append(.cancelAction { + self.toggleSelect() + }) +// if selectOcId.count != dataSource.getMetadataSourceForAllSections().count { +// actions.append(.selectAllAction(action: selectAll)) +// } +// +// guard !selectOcId.isEmpty else { return actions } + if fileSelect.count != selectableDataSource.count { + actions.append(.selectAllAction(action: selectAll)) + } + + guard !fileSelect.isEmpty else { return actions } + + actions.append(.seperator(order: 0)) + + var selectedMetadatas: [tableMetadata] = [] + var selectedMediaMetadatas: [tableMetadata] = [] + var isAnyOffline = false + var isAnyFolder = false + var isAnyLocked = false + var canUnlock = true + var canOpenIn = false + var isDirectoryE2EE = false + +// for ocId in selectOcId { +// guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { continue } +// if metadata.e2eEncrypted { +// selectOcId.removeAll(where: {$0 == metadata.ocId}) + for ocId in fileSelect { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { continue } + if metadata.e2eEncrypted { + fileSelect.removeAll(where: {$0 == metadata.ocId}) + } else { + selectedMetadatas.append(metadata) + } + + if [NKCommon.TypeClassFile.image.rawValue, NKCommon.TypeClassFile.video.rawValue].contains(metadata.classFile) { + selectedMediaMetadatas.append(metadata) + } + if metadata.directory { isAnyFolder = true } + if metadata.lock { + isAnyLocked = true + if metadata.lockOwner != session.userId { + canUnlock = false + } + } + + guard !isAnyOffline else { continue } + if metadata.directory, + let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, metadata.serverUrl + "/" + metadata.fileName)) { + isAnyOffline = directory.offline + } else if let localFile = NCManageDatabase.shared.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { + isAnyOffline = localFile.offline + } // else: file is not offline, continue + + if !metadata.directory { + canOpenIn = true + } + + if metadata.isDirectoryE2EE { + isDirectoryE2EE = true + } + } + + if canOpenIn { + actions.append(.share(selectedMetadatas: selectedMetadatas, viewController: self, completion: { self.toggleSelect() })) + } + + if !isAnyFolder, canUnlock, !NCGlobal.shared.capabilityFilesLockVersion.isEmpty { + actions.append(.openInAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: { self.toggleSelect() })) + } + + if !isAnyFolder, canUnlock, !NCCapabilities.shared.getCapabilities(account: controller?.account).capabilityFilesLockVersion.isEmpty { + actions.append(.lockUnlockFiles(shouldLock: !isAnyLocked, metadatas: selectedMetadatas, completion: { self.toggleSelect() })) + } + + if !selectedMediaMetadatas.isEmpty { + var title: String = NSLocalizedString("_save_selected_files_", comment: "") + var icon = NCUtility().loadImage(named: "save_files",colors: [NCBrandColor.shared.iconImageColor]) + if selectedMediaMetadatas.allSatisfy({ NCManageDatabase.shared.getMetadataLivePhoto(metadata: $0) != nil }) { + title = NSLocalizedString("_livephoto_save_", comment: "") + icon = NCUtility().loadImage(named: "livephoto") + } + + actions.append(NCMenuAction( + title: title, + icon: icon, + order: 0, + action: { _ in + for metadata in selectedMediaMetadatas { + if let metadataMOV = NCManageDatabase.shared.getMetadataLivePhoto(metadata: metadata) { + NCNetworking.shared.saveLivePhotoQueue.addOperation(NCOperationSaveLivePhoto(metadata: metadata, metadataMOV: metadataMOV, hudView: self.view)) + } else { + if NCUtilityFileSystem().fileProviderStorageExists(metadata) { + NCActionCenter.shared.saveAlbum(metadata: metadata, controller: self.tabBarController as? NCMainTabBarController) + } else { + if NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadata, selector: NCGlobal.shared.selectorSaveAlbum)) + } + } + } + } + self.toggleSelect() + } + ) + ) + actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMediaMetadatas, controller: self.controller, completion: { self.toggleSelect() })) + } + actions.append(.setAvailableOfflineAction(selectedMetadatas: selectedMetadatas, isAnyOffline: isAnyOffline, viewController: self, completion: { + self.reloadDataSource() + self.toggleSelect() + })) + + if !isDirectoryE2EE { + actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, viewController: self, indexPath: [], completion: { self.toggleSelect() })) + actions.append(.copyAction(selectOcId: selectOcId, viewController: self, completion: { self.toggleSelect() })) + } + actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, indexPaths: [], viewController: self, completion: { self.toggleSelect() })) + return actions + } + + func setNavigationRightItems(enableMenu: Bool = false) { + DispatchQueue.main.async { + if self.isEditMode { + let more = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), style: .plain) { self.presentMenu(with: self.createMenuActions())} + self.navigationItem.rightBarButtonItems = [more] + } else { + let select = UIBarButtonItem(title: NSLocalizedString("_select_", comment: ""), style: UIBarButtonItem.Style.plain) { self.toggleSelect() } + let notification = UIBarButtonItem(image: UIImage(systemName: "bell"), style: .plain, action: self.tapNotification) + if self.layoutKey == NCGlobal.shared.layoutViewFiles { + self.navigationItem.rightBarButtonItems = [select, notification] + } else { + self.navigationItem.rightBarButtonItems = [select] + } + let transfer = UIBarButtonItem(image: UIImage(systemName: "arrow.left.arrow.right.circle.fill"), style: .plain, action: self.tapTransfer) + let resultsCount = self.database.getResultsMetadatas(predicate: NSPredicate(format: "status != %i", NCGlobal.shared.metadataStatusNormal))?.count ?? 0 + + if self.layoutKey == NCGlobal.shared.layoutViewFiles { + self.navigationItem.rightBarButtonItems = resultsCount > 0 ? [select, notification, transfer] : [select, notification] + } else { + self.navigationItem.rightBarButtonItems = [select] + } + } + guard self.layoutKey == NCGlobal.shared.layoutViewFiles else { return } + self.navigationItem.title = self.titleCurrentFolder + } + } + + func onListSelected() { + if layoutForView?.layout == NCGlobal.shared.layoutGrid { + headerMenu?.buttonSwitch.accessibilityLabel = NSLocalizedString("_grid_view_", comment: "") + layoutForView?.layout = NCGlobal.shared.layoutList + NCManageDatabase.shared.setLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl, layout: layoutForView?.layout) + self.groupByField = "name" + if self.dataSource.groupByField != self.groupByField { + self.dataSource.changeGroupByField(self.groupByField) + } + self.saveLayout(layoutForView!) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(self.listLayout, animated: true) {_ in self.isTransitioning = false } + } + } + + func onGridSelected() { + if layoutForView?.layout == NCGlobal.shared.layoutList { + headerMenu?.buttonSwitch.accessibilityLabel = NSLocalizedString("_list_view_", comment: "") + layoutForView?.layout = NCGlobal.shared.layoutGrid + NCManageDatabase.shared.setLayoutForView(account: session.account, key: layoutKey, serverUrl: serverUrl, layout: layoutForView?.layout) + if isSearchingMode { + self.groupByField = "name" + } else { + self.groupByField = "classFile" + } + if self.dataSource.groupByField != self.groupByField { + self.dataSource.changeGroupByField(self.groupByField) + } + self.saveLayout(layoutForView!) + self.collectionView.reloadData() + self.collectionView.collectionViewLayout.invalidateLayout() + self.collectionView.setCollectionViewLayout(self.gridLayout, animated: true) {_ in self.isTransitioning = false } + } + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBarDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBarDelegate.swift index 063f90b611..9460caf1a3 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBarDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SelectTabBarDelegate.swift @@ -1,6 +1,25 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCCollectionViewCommon+SelectTabBar.swift +// Nextcloud +// +// Created by Milen on 01.03.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 UIKit import Foundation @@ -204,10 +223,146 @@ extension NCCollectionViewCommon: NCCollectionViewCommonSelectTabBarDelegate { if editMode { navigationItem.leftBarButtonItems = nil } else { - await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() + ///Magentacloud branding changes hide user account button on left navigation bar +// setNavigationLeftItems() + } + (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() + + self.collectionView.reloadData() + } + + func convertLivePhoto(metadataFirst: tableMetadata?, metadataLast: tableMetadata?) { + if let metadataFirst, let metadataLast { + Task { + await self.networking.setLivePhoto(metadataFirst: metadataFirst, metadataLast: metadataLast) + } + } + setEditMode(false) + } + + /// If explicit `isOn` is not set, it will invert `isEditMode` + func toggleSelect(isOn: Bool? = nil) { + DispatchQueue.main.async { + self.isEditMode = isOn ?? !self.isEditMode + self.selectOcId.removeAll() + self.setNavigationLeftItems() + self.setNavigationRightItems() + self.collectionView.reloadData() } await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() self.collectionView.reloadData() } + + func createMenuActions() -> [NCMenuAction] { + var actions = [NCMenuAction]() + + actions.append(.cancelAction { + self.toggleSelect() + }) + if selectOcId.count != dataSource.getMetadataSourceForAllSections().count { + actions.append(.selectAllAction(action: selectAll)) + } + + guard !selectOcId.isEmpty else { return actions } + + actions.append(.seperator(order: 0)) + + var selectedMetadatas: [tableMetadata] = [] + var selectedMediaMetadatas: [tableMetadata] = [] + var isAnyOffline = false + var isAnyFolder = false + var isAnyLocked = false + var canUnlock = true + var canOpenIn = false + var isDirectoryE2EE = false + + for ocId in selectOcId { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { continue } + if metadata.e2eEncrypted { + selectOcId.removeAll(where: {$0 == metadata.ocId}) + } else { + selectedMetadatas.append(metadata) + } + + if [NKCommon.TypeClassFile.image.rawValue, NKCommon.TypeClassFile.video.rawValue].contains(metadata.classFile) { + selectedMediaMetadatas.append(metadata) + } + if metadata.directory { isAnyFolder = true } + if metadata.lock { + isAnyLocked = true + if metadata.lockOwner != appDelegate.userId { + canUnlock = false + } + } + + guard !isAnyOffline else { continue } + if metadata.directory, + let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", appDelegate.account, metadata.serverUrl + "/" + metadata.fileName)) { + isAnyOffline = directory.offline + } else if let localFile = NCManageDatabase.shared.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { + isAnyOffline = localFile.offline + } // else: file is not offline, continue + + if !metadata.directory { + canOpenIn = true + } + + if metadata.isDirectoryE2EE { + isDirectoryE2EE = true + } + } + + if canOpenIn { + actions.append(.share(selectedMetadatas: selectedMetadatas, viewController: self, completion: { self.toggleSelect() })) + } + + if !isAnyFolder, canUnlock, !NCGlobal.shared.capabilityFilesLockVersion.isEmpty { + actions.append(.lockUnlockFiles(shouldLock: !isAnyLocked, metadatas: selectedMetadatas, completion: { self.toggleSelect() })) + } + + if !selectedMediaMetadatas.isEmpty { + var title: String = NSLocalizedString("_save_selected_files_", comment: "") + var icon = NCUtility().loadImage(named: "save_files",colors: [NCBrandColor.shared.iconImageColor]) + if selectedMediaMetadatas.allSatisfy({ NCManageDatabase.shared.getMetadataLivePhoto(metadata: $0) != nil }) { + title = NSLocalizedString("_livephoto_save_", comment: "") + icon = NCUtility().loadImage(named: "livephoto") + } + + actions.append(NCMenuAction( + title: title, + icon: icon, + order: 0, + action: { _ in + for metadata in selectedMediaMetadatas { + if let metadataMOV = NCManageDatabase.shared.getMetadataLivePhoto(metadata: metadata) { + NCNetworking.shared.saveLivePhotoQueue.addOperation(NCOperationSaveLivePhoto(metadata: metadata, metadataMOV: metadataMOV, hudView: self.view)) + } else { + if NCUtilityFileSystem().fileProviderStorageExists(metadata) { + NCActionCenter.shared.saveAlbum(metadata: metadata, controller: self.tabBarController as? NCMainTabBarController) + } else { + if NCNetworking.shared.downloadQueue.operations.filter({ ($0 as? NCOperationDownload)?.metadata.ocId == metadata.ocId }).isEmpty { + NCNetworking.shared.downloadQueue.addOperation(NCOperationDownload(metadata: metadata, selector: NCGlobal.shared.selectorSaveAlbum)) + } + } + } + } + self.toggleSelect() + } + ) + ) + } + actions.append(.setAvailableOfflineAction(selectedMetadatas: selectedMetadatas, isAnyOffline: isAnyOffline, viewController: self, completion: { + self.reloadDataSource() + self.toggleSelect() + })) + + if !isDirectoryE2EE { + actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, viewController: self, indexPath: [], completion: { self.toggleSelect() })) + actions.append(.copyAction(selectOcId: selectOcId, viewController: self, completion: { self.toggleSelect() })) + } + actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, indexPaths: [], viewController: self, completion: { self.toggleSelect() })) + return actions + } + } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift new file mode 100644 index 0000000000..8a08faca4e --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift @@ -0,0 +1,94 @@ +// +// NCCollectionViewCommon+SwipeCollectionViewCellDelegate.swift +// Nextcloud +// +// Created by Milen on 01.03.24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 SwipeCellKit + +extension NCCollectionViewCommon: SwipeCollectionViewCellDelegate { + func collectionView(_ collectionView: UICollectionView, editActionsForItemAt indexPath: IndexPath, for orientation: SwipeCellKit.SwipeActionsOrientation) -> [SwipeCellKit.SwipeAction]? { + guard orientation == .right, let metadata = self.dataSource.cellForItemAt(indexPath: indexPath) else { return nil } + + let scaleTransition = ScaleTransition(duration: 0.3, initialScale: 0.8, threshold: 0.8) + + // wait a fix for truncate the text .. ? .. + let favoriteAction = SwipeAction(style: .default, title: NSLocalizedString(metadata.favorite ? "_favorite_short_" : "_favorite_short_", comment: "") ) { _, _ in + NCNetworking.shared.favoriteMetadata(metadata) { error in + if error != .success { + NCContentPresenter().showError(error: error) + } + } + } + favoriteAction.backgroundColor = NCBrandColor.shared.yellowFavorite + favoriteAction.image = .init(systemName: "star.fill") + favoriteAction.transitionDelegate = scaleTransition + favoriteAction.hidesWhenSelected = true + + var actions = [favoriteAction] + + let shareAction = SwipeAction(style: .default, title: NSLocalizedString("_share_", comment: "")) { _, _ in + NCActionCenter.shared.openActivityViewController(selectedMetadata: [metadata], controller: self.controller) + } + shareAction.backgroundColor = .blue + shareAction.image = .init(systemName: "square.and.arrow.up") + shareAction.transitionDelegate = scaleTransition + shareAction.hidesWhenSelected = true + + let deleteAction = SwipeAction(style: .destructive, title: NSLocalizedString("_delete_", comment: "")) { _, _ in + let titleDelete: String + + if metadata.directory { + titleDelete = NSLocalizedString("_delete_folder_", comment: "") + } else { + titleDelete = NSLocalizedString("_delete_file_", comment: "") + } + + let message = NSLocalizedString("_want_delete_", comment: "") + "\n - " + metadata.fileNameView + + let alertController = UIAlertController.deleteFileOrFolder(titleString: titleDelete + "?", message: message, canDeleteServer: !metadata.lock, selectedMetadatas: [metadata], sceneIdentifier: self.controller?.sceneIdentifier) { _ in } + + self.viewController.present(alertController, animated: true, completion: nil) + } + deleteAction.image = UIImage.init(systemName: "trash") + deleteAction.style = .destructive + deleteAction.transitionDelegate = scaleTransition + deleteAction.hidesWhenSelected = true + + if !NCManageDatabase.shared.isMetadataShareOrMounted(metadata: metadata, metadataFolder: metadataFolder) { + actions.insert(deleteAction, at: 0) + } + + if metadata.canShare { + actions.append(shareAction) + } + + return actions + } + + func collectionView(_ collectionView: UICollectionView, editActionsOptionsForItemAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions { + var options = SwipeOptions() + options.expansionStyle = .selection + options.transitionStyle = .border + options.backgroundColor = .clear + return options + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift index 5ae9be568e..3ce153429c 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommonSelectTabBar.swift @@ -195,6 +195,18 @@ struct NCCollectionViewCommonSelectTabBarView: View { tabBarSelect.delegate?.lock(isAnyLocked: tabBarSelect.isAnyLocked) }, label: { Label(NSLocalizedString(tabBarSelect.isAnyLocked ? "_unlock_" : "_lock_", comment: ""), systemImage: tabBarSelect.isAnyLocked ? "lock.open" : "lock") + // NMC-5295 - iOS v10.2.2.3 - File Browser: Remove "file lock" feature from menu + // lock menu entry is not available. not supported by magentacloud +// Button(action: { +// tabBarSelect.delegate?.lock(isAnyLocked: tabBarSelect.isAnyLocked) +// }, label: { +// Label(NSLocalizedString(tabBarSelect.isAnyLocked ? "_unlock_" : "_lock_", comment: ""), systemImage: tabBarSelect.isAnyLocked ? "lock.open" : "lock") +// +// if !tabBarSelect.enableLock { +// Text(NSLocalizedString("_lock_no_permissions_selected_", comment: "")) +// } +// }) +// .disabled(!tabBarSelect.enableLock || tabBarSelect.isSelectedEmpty) if !tabBarSelect.enableLock { Text(NSLocalizedString("_lock_no_permissions_selected_", comment: "")) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift index e937164631..d42c184c85 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDataSource.swift @@ -234,10 +234,16 @@ class NCCollectionViewDataSource: NSObject { } func getFooterInformation() -> (directories: Int, files: Int, size: Int64) { - let directories = metadatas.filter({ $0.directory == true}) - let files = metadatas.filter({ $0.directory == false}) + let validMetadatas = metadatas.filter { !$0.isInvalidated } + let directories = validMetadatas.filter({ $0.directory == true}) + let files = validMetadatas.filter({ $0.directory == false}) + var size: Int64 = 0 + directories.forEach { metadata in + size += metadata.size + } + files.forEach { metadata in size += metadata.size } diff --git a/iOSClient/Main/Collection Common/NCSelectableNavigationView.swift b/iOSClient/Main/Collection Common/NCSelectableNavigationView.swift new file mode 100644 index 0000000000..17dbb04c03 --- /dev/null +++ b/iOSClient/Main/Collection Common/NCSelectableNavigationView.swift @@ -0,0 +1,130 @@ +// +// NCSelectableNavigationView.swift +// Nextcloud +// +// Created by Henrik Storch on 27.01.22. +// Copyright © 2022 Henrik Storch. All rights reserved. +// +// Author Marino Faggiana +// Author Henrik Storch +// +// 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 NextcloudKit +import Realm +import UIKit + +extension RealmSwiftObject { + var primaryKeyValue: String? { + guard let primaryKeyName = self.objectSchema.primaryKeyProperty?.name else { return nil } + return value(forKey: primaryKeyName) as? String + } +} + +public protocol NCSelectableViewTabBar { + var tabBarController: UITabBarController? { get } + var hostingController: UIViewController? { get } +} + +protocol NCSelectableNavigationView: AnyObject { + var viewController: UIViewController { get } +// var appDelegate: AppDelegate { get } + var selectableDataSource: [RealmSwiftObject] { get } + var collectionView: UICollectionView! { get set } + var isEditMode: Bool { get set } + var fileSelect: [String] { get set } +// var selectIndexPaths: [IndexPath] { get set } + var appDelegate: AppDelegate { get } + var selectableDataSource: [RealmSwiftObject] { get } + var collectionView: UICollectionView! { get set } + var isEditMode: Bool { get set } + var selectOcId: [String] { get set } + var appDelegate: AppDelegate { get } + var selectIndexPaths: [IndexPath] { get set } + var titleCurrentFolder: String { get } + var navigationItem: UINavigationItem { get } + var navigationController: UINavigationController? { get } + var layoutKey: String { get } + var serverUrl: String { get } +// var tabBarSelect: NCSelectableViewTabBar? { get set } +// var dataSource: NCCollectionViewDataSource { get set } + +// func reloadDataSource(withQueryDB: Bool) + var tabBarSelect: NCSelectableViewTabBar? { get set } + + func reloadDataSource(withQueryDB: Bool) +// func reloadDataSource(withQueryDB: Bool) + func setNavigationLeftItems() + func setNavigationRightItems(enableMenu: Bool) + func createMenuActions() -> [NCMenuAction] + + func toggleSelect(isOn: Bool?) + func onListSelected() + func onGridSelected() +} + +extension NCSelectableNavigationView { + func setNavigationLeftItems() {} + + func saveLayout(_ layoutForView: NCDBLayoutForView) { + NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) + setNavigationRightItems(enableMenu: false) + } + + /// If explicit `isOn` is not set, it will invert `isEditMode` + func toggleSelect(isOn: Bool? = nil) { + DispatchQueue.main.async { + self.isEditMode = isOn ?? !self.isEditMode + self.fileSelect.removeAll() +// self.selectIndexPaths.removeAll() + self.selectOcId.removeAll() + self.selectIndexPaths.removeAll() + self.setNavigationLeftItems() + self.setNavigationRightItems(enableMenu: true) + self.collectionView.reloadData() + } + } + + func collectionViewSelectAll() { + + fileSelect = selectableDataSource.compactMap({ $0.primaryKeyValue }) +// fileSelect = NCCollectionViewDataSource().getMetadataSourceForAllSections().compactMap({ $0.primaryKeyValue }) +// selectOcId = selectableDataSource.compactMap({ $0.primaryKeyValue }) + collectionView.reloadData() + setNavigationRightItems(enableMenu: false) + } + + func tapNotification() { + if let viewController = UIStoryboard(name: "NCNotification", bundle: nil).instantiateInitialViewController() as? NCNotification { + navigationController?.pushViewController(viewController, animated: true) + } + } + + func tapTransfer() { + if let navController = UIStoryboard(name: "NCTransfers", bundle: nil).instantiateInitialViewController() as? UINavigationController, + let viewController = navController.topViewController as? NCTransfers { + viewController.modalPresentationStyle = .pageSheet +// self.present(navigationController, animated: true, completion: nil) + navigationController?.present(navController, animated: true, completion: nil) + } + } +} + +extension NCSelectableNavigationView where Self: UIViewController { + var viewController: UIViewController { + self + } +} diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift index 7272cffaea..3a3f982288 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift @@ -9,6 +9,7 @@ import NextcloudKit protocol NCSectionFirstHeaderDelegate: AnyObject { func tapRichWorkspace(_ sender: Any) func tapRecommendations(with metadata: tableMetadata) + func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) } class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegate { @@ -35,13 +36,6 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat private var viewController: UIViewController? private var sceneIdentifier: String = "" -#if !EXTENSION - @MainActor - internal var controller: NCMainTabBarController? { - viewController?.tabBarController as? NCMainTabBarController - } -#endif - override func awakeFromNib() { super.awakeFromNib() @@ -130,16 +124,7 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat viewSection.isHidden = false } -#if EXTENSION self.collectionViewRecommendations.reloadData() -#else - Task { - let isPause = await (viewController as? NCCollectionViewCommon)?.debouncerReloadDataSource.isPausedNow() ?? false - if !isPause { - self.collectionViewRecommendations.reloadData() - } - } -#endif } // MARK: - RichWorkspace @@ -193,7 +178,7 @@ extension NCSectionFirstHeader: UICollectionViewDataSource { if let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: self.global.previewExt512, userId: metadata.userId, urlBase: metadata.urlBase) { Task { @MainActor in for case let cell as NCRecommendationsCell in self.collectionViewRecommendations.visibleCells { - if cell.metadata?.fileId == recommendedFiles.id { + if cell.id == recommendedFiles.id { cell.image.contentMode = .scaleAspectFill if metadata.classFile == NKTypeClassFile.document.rawValue { cell.setImageCorner(withBorder: true) @@ -217,12 +202,13 @@ extension NCSectionFirstHeader: UICollectionViewDataSource { cell.setImageCorner(withBorder: false) } - cell.setBidiSafeFilename(metadata.fileNameView, isDirectory: metadata.directory, titleLabel: cell.labelFilename, extensionLabel: cell.labelExtensionFilename) + cell.labelFilename.text = metadata.fileNameView cell.labelInfo.text = recommendedFiles.reason cell.delegate = self cell.metadata = metadata cell.recommendedFiles = recommendedFiles + cell.id = recommendedFiles.id } return cell @@ -256,7 +242,7 @@ extension NCSectionFirstHeader: UICollectionViewDelegate { return NCViewerProviderContextMenu(metadata: metadata, image: image, sceneIdentifier: self.sceneIdentifier) }, actionProvider: { _ in let cell = collectionView.cellForItem(at: indexPath) - let contextMenu = NCContextMenuMain(metadata: metadata.detachedCopy(), viewController: viewController, controller: self.controller, sender: cell) + let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: viewController, sceneIdentifier: self.sceneIdentifier, image: image, sender: cell) return contextMenu.viewMenu() }) #endif @@ -272,23 +258,7 @@ extension NCSectionFirstHeader: UICollectionViewDelegateFlowLayout { } extension NCSectionFirstHeader: NCRecommendationsCellDelegate { - func openContextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) { -#if !EXTENSION - Task { - guard let viewController = self.viewController, let metadata else { - return - } - button.menu = NCContextMenuMain(metadata: metadata, viewController: viewController, controller: self.controller, sender: sender).viewMenu() - } -#endif - } - - func onMenuIntent(with metadata: tableMetadata?) { -#if !EXTENSION - Task { - let collectionViewCommon = (self.viewController as? NCCollectionViewCommon) - await collectionViewCommon?.debouncerReloadData.pause() - } -#endif + func touchUpInsideButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { + self.delegate?.tapRecommendationsButtonMenu(with: metadata, image: image, sender: sender) } } diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.swift new file mode 100644 index 0000000000..6cd8ae727d --- /dev/null +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.swift @@ -0,0 +1,329 @@ +// +// NCSectionHeaderFooter.swift +// Nextcloud +// +// Created by Marino Faggiana on 09/10/2018. +// Copyright © 2018 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 UIKit +import MarkdownKit + +class NCSectionHeaderMenu: UICollectionReusableView, UIGestureRecognizerDelegate { + + @IBOutlet weak var buttonSwitch: UIButton! + @IBOutlet weak var buttonOrder: UIButton! + @IBOutlet weak var buttonMore: UIButton! + @IBOutlet weak var buttonTransfer: UIButton! + @IBOutlet weak var imageButtonTransfer: UIImageView! + @IBOutlet weak var labelTransfer: UILabel! + @IBOutlet weak var progressTransfer: UIProgressView! + @IBOutlet weak var transferSeparatorBottom: UIView! + @IBOutlet weak var textViewRichWorkspace: UITextView! + @IBOutlet weak var labelSection: UILabel! + @IBOutlet weak var viewTransfer: UIView! + @IBOutlet weak var viewButtonsView: UIView! + @IBOutlet weak var viewSeparator: UIView! + @IBOutlet weak var viewRichWorkspace: UIView! + @IBOutlet weak var viewSection: UIView! + + @IBOutlet weak var viewTransferHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var viewButtonsViewHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var viewSeparatorHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var viewRichWorkspaceHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var viewSectionHeightConstraint: NSLayoutConstraint! + @IBOutlet weak var transferSeparatorBottomHeightConstraint: NSLayoutConstraint! + + weak var delegate: NCSectionHeaderMenuDelegate? + let utility = NCUtility() + private var markdownParser = MarkdownParser() + private var richWorkspaceText: String? + private var textViewColor: UIColor? + private let gradient: CAGradientLayer = CAGradientLayer() + + override func awakeFromNib() { + super.awakeFromNib() + + backgroundColor = .clear + + buttonSwitch.setImage(UIImage(systemName: "list.bullet")!.image(color: NCBrandColor.shared.iconImageColor, size: 25), for: .normal) + + buttonOrder.setTitle("", for: .normal) + buttonOrder.setTitleColor(NCBrandColor.shared.brand, for: .normal) + buttonMore.setImage(UIImage(named: "more")!.image(color: NCBrandColor.shared.iconImageColor, size: 25), for: .normal) + + // Gradient + gradient.startPoint = CGPoint(x: 0, y: 0.50) + gradient.endPoint = CGPoint(x: 0, y: 1) + viewRichWorkspace.layer.addSublayer(gradient) + + let tap = UITapGestureRecognizer(target: self, action: #selector(touchUpInsideViewRichWorkspace(_:))) + tap.delegate = self + viewRichWorkspace?.addGestureRecognizer(tap) + + viewSeparatorHeightConstraint.constant = 0.5 + viewSeparator.backgroundColor = .separator + + markdownParser = MarkdownParser(font: UIFont.systemFont(ofSize: 15), color: .label) + markdownParser.header.font = UIFont.systemFont(ofSize: 25) + if let richWorkspaceText = richWorkspaceText { + textViewRichWorkspace.attributedText = markdownParser.parse(richWorkspaceText) + } + textViewColor = .label + + labelSection.text = "" + viewSectionHeightConstraint.constant = 0 + + buttonTransfer.backgroundColor = .clear + buttonTransfer.setImage(nil, for: .normal) + buttonTransfer.layer.cornerRadius = 6 + buttonTransfer.layer.masksToBounds = true + imageButtonTransfer.image = UIImage(systemName: "stop.circle") + imageButtonTransfer.tintColor = .white + labelTransfer.text = "" + progressTransfer.progress = 0 + progressTransfer.tintColor = NCBrandColor.shared.brand + progressTransfer.trackTintColor = NCBrandColor.shared.brand.withAlphaComponent(0.2) + transferSeparatorBottom.backgroundColor = .separator + transferSeparatorBottomHeightConstraint.constant = 0.5 + } + + override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) + + gradient.frame = viewRichWorkspace.bounds + setInterfaceColor() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + setInterfaceColor() + } + + // MARK: - View + + func setStatusButtonsView(enable: Bool) { + + buttonSwitch.isEnabled = enable + buttonOrder.isEnabled = enable + buttonMore.isEnabled = enable + } + + func buttonMoreIsHidden(_ isHidden: Bool) { + + buttonMore.isHidden = isHidden + } + + func setImageSwitchList() { + + buttonSwitch.setImage(UIImage(systemName: "list.bullet")!.image(color: NCBrandColor.shared.iconImageColor, width: 20, height: 15), for: .normal) + } + + func setImageSwitchGrid() { + + buttonSwitch.setImage(UIImage(systemName: "square.grid.2x2")!.image(color: NCBrandColor.shared.iconImageColor, size: 20), for: .normal) + } + + func setButtonsView(height: CGFloat) { + + viewButtonsViewHeightConstraint.constant = height + if height == 0 { + viewButtonsView.isHidden = true + } else { + viewButtonsView.isHidden = false + } + } + + func setSortedTitle(_ title: String) { + + let title = NSLocalizedString(title, comment: "") + buttonOrder.setTitle(title, for: .normal) + } + + // MARK: - RichWorkspace + + func setRichWorkspaceHeight(_ size: CGFloat) { + + viewRichWorkspaceHeightConstraint.constant = size + if size == 0 { + viewRichWorkspace.isHidden = true + } else { + viewRichWorkspace.isHidden = false + } + } + + func setInterfaceColor() { + + if traitCollection.userInterfaceStyle == .dark { + gradient.colors = [UIColor(white: 0, alpha: 0).cgColor, UIColor.black.cgColor] + } else { + gradient.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor.white.cgColor] + } + } + + func setRichWorkspaceText(_ text: String?) { + guard let text = text else { return } + + if text != self.richWorkspaceText { + textViewRichWorkspace.attributedText = markdownParser.parse(text) + self.richWorkspaceText = text + } + } + + // MARK: - Transfer + + func setViewTransfer(isHidden: Bool, ocId: String? = nil, text: String? = nil, progress: Float? = nil) { + + labelTransfer.text = text + viewTransfer.isHidden = isHidden + progressTransfer.progress = 0 + + if isHidden { + viewTransferHeightConstraint.constant = 0 + } else { + var image: UIImage? + if let ocId, + let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) { + image = utility.createFilePreviewImage(ocId: metadata.ocId, etag: metadata.etag, fileNameView: metadata.fileNameView, classFile: metadata.classFile, status: metadata.status, createPreviewMedia: true)//?.darken() + if image == nil { + image = UIImage(named: metadata.iconName) + buttonTransfer.backgroundColor = .lightGray + } else { + buttonTransfer.backgroundColor = .clear + } + buttonTransfer.setImage(image, for: .normal) + } + viewTransferHeightConstraint.constant = NCGlobal.shared.heightHeaderTransfer + if let progress { + progressTransfer.progress = progress + } + } + } + + // MARK: - Section + + func setSectionHeight(_ size: CGFloat) { + + viewSectionHeightConstraint.constant = size + if size == 0 { + viewSection.isHidden = true + } else { + viewSection.isHidden = false + } + } + + // MARK: - Action + + @IBAction func touchUpInsideSwitch(_ sender: Any) { + delegate?.tapButtonSwitch(sender) + } + + @IBAction func touchUpInsideOrder(_ sender: Any) { + delegate?.tapButtonOrder(sender) + } + + @IBAction func touchUpInsideMore(_ sender: Any) { + delegate?.tapButtonMore(sender) + } + + @IBAction func touchUpTransfer(_ sender: Any) { + delegate?.tapButtonTransfer(sender) + } + + @objc func touchUpInsideViewRichWorkspace(_ sender: Any) { + delegate?.tapRichWorkspace(sender) + } +} + +protocol NCSectionHeaderMenuDelegate: AnyObject { + func tapButtonSwitch(_ sender: Any) + func tapButtonOrder(_ sender: Any) + func tapButtonMore(_ sender: Any) + func tapButtonTransfer(_ sender: Any) + func tapRichWorkspace(_ sender: Any) +} + +// optional func +extension NCSectionHeaderMenuDelegate { + func tapButtonSwitch(_ sender: Any) {} + func tapButtonOrder(_ sender: Any) {} + func tapButtonMore(_ sender: Any) {} + func tapButtonTransfer(_ sender: Any) {} + func tapRichWorkspace(_ sender: Any) {} +} + +// optional func +extension NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) {} +} + +//// https://stackoverflow.com/questions/16278463/darken-an-uiimage +//public extension UIImage { +// +// private enum BlendMode { +// case multiply // This results in colors that are at least as dark as either of the two contributing sample colors +// case screen // This results in colors that are at least as light as either of the two contributing sample colors +// } +// +// // A level of zero yeilds the original image, a level of 1 results in black +// func darken(level: CGFloat = 0.5) -> UIImage? { +// return blend(mode: .multiply, level: level) +// } +// +// // A level of zero yeilds the original image, a level of 1 results in white +// func lighten(level: CGFloat = 0.5) -> UIImage? { +// return blend(mode: .screen, level: level) +// } +// +// private func blend(mode: BlendMode, level: CGFloat) -> UIImage? { +// let context = CIContext(options: nil) +// +// var level = level +// if level < 0 { +// level = 0 +// } else if level > 1 { +// level = 1 +// } +// +// let filterName: String +// switch mode { +// case .multiply: // As the level increases we get less white +// level = abs(level - 1.0) +// filterName = "CIMultiplyBlendMode" +// case .screen: // As the level increases we get more white +// filterName = "CIScreenBlendMode" +// } +// +// let blender = CIFilter(name: filterName)! +// let backgroundColor = CIColor(color: UIColor(white: level, alpha: 1)) +// +// guard let inputImage = CIImage(image: self) else { return nil } +// blender.setValue(inputImage, forKey: kCIInputImageKey) +// +// guard let backgroundImageGenerator = CIFilter(name: "CIConstantColorGenerator") else { return nil } +// backgroundImageGenerator.setValue(backgroundColor, forKey: kCIInputColorKey) +// guard let backgroundImage = backgroundImageGenerator.outputImage?.cropped(to: CGRect(origin: CGPoint.zero, size: self.size)) else { return nil } +// blender.setValue(backgroundImage, forKey: kCIInputBackgroundImageKey) +// +// guard let blendedImage = blender.outputImage else { return nil } +// +// guard let cgImage = context.createCGImage(blendedImage, from: blendedImage.extent) else { return nil } +// return UIImage(cgImage: cgImage) +// } +//} diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.xib b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.xib new file mode 100644 index 0000000000..5d206c1d2f --- /dev/null +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionHeaderMenu.xib @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Menu/NCMenuAction.swift b/iOSClient/Menu/NCMenuAction.swift new file mode 100644 index 0000000000..f623c4b139 --- /dev/null +++ b/iOSClient/Menu/NCMenuAction.swift @@ -0,0 +1,293 @@ +// +// NCMenuAction.swift +// Nextcloud +// +// Created by Henrik Storch on 17.02.22. +// Copyright © 2022 Marino Faggiana. All rights reserved. +// +// Author Henrik Storch +// Author Marino Faggiana +// +// 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 UIKit +import NextcloudKit +import SwiftUI + +@available(*, deprecated, message: "Change to using iOS native context menus, as well as using ContextMenuActions and NCViewerContextMenu") +class NCMenuAction { + let accessibilityIdentifier: String? + let title: String + let boldTitle: Bool + let details: String? + let icon: UIImage + let selectable: Bool + var onTitle: String? + var onIcon: UIImage? + let destructive: Bool + var selected: Bool = false + var isOn: Bool = false + var action: ((_ menuAction: NCMenuAction) -> Void)? + var rowHeight: CGFloat { self.title == NCMenuAction.seperatorIdentifier ? NCMenuAction.seperatorHeight : self.details != nil ? 76 : 60 } + var order: Int = 0 + var sender: Any? + + init(title: String, boldTitle: Bool = false, destructive: Bool = false, details: String? = nil, icon: UIImage, order: Int = 0, accessibilityIdentifier: String? = nil, sender: Any?, action: ((_ menuAction: NCMenuAction) -> Void)?) { + self.accessibilityIdentifier = accessibilityIdentifier + self.title = title + self.boldTitle = boldTitle + self.destructive = destructive + self.details = details + self.icon = icon + self.action = action + self.selectable = false + self.order = order + self.sender = sender + } + + init(title: String, boldTitle: Bool = false, destructive: Bool = false, details: String? = nil, icon: UIImage, onTitle: String? = nil, onIcon: UIImage? = nil, selected: Bool, on: Bool, order: Int = 0, accessibilityIdentifier: String? = nil, sender: Any?, action: ((_ menuAction: NCMenuAction) -> Void)?) { + self.accessibilityIdentifier = accessibilityIdentifier + self.title = title + self.boldTitle = boldTitle + self.destructive = destructive + self.details = details + self.icon = icon + self.onTitle = onTitle ?? title + self.onIcon = onIcon ?? icon + self.action = action + self.selected = selected + self.isOn = on + self.selectable = true + self.order = order + self.sender = sender + } +} + +// MARK: - Actions + +extension NCMenuAction { + static let seperatorIdentifier = "NCMenuAction.SEPARATOR" + static let seperatorHeight: CGFloat = 0.5 + + /// A static seperator, with no actions, text, or icons + static func seperator(order: Int = 0, sender: Any?) -> NCMenuAction { + return NCMenuAction(title: seperatorIdentifier, icon: UIImage(), order: order, sender: sender, action: nil) + } + + /// Select all items + static func selectAllAction(sender: Any?, action: @escaping () -> Void) -> NCMenuAction { + NCMenuAction( + title: NSLocalizedString("_select_all_", comment: ""), + icon: NCUtility().loadImage(named: "checkmark.circle.fill", colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + sender: sender, + action: { _ in action() } + ) + } + + /// Cancel + static func cancelAction(sender: Any?, action: @escaping () -> Void) -> NCMenuAction { + NCMenuAction( + title: NSLocalizedString("_cancel_", comment: ""), + icon: NCUtility().loadImage(named: "xmark", colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + sender: sender, + action: { _ in action() } + ) + } + + /// Delete files either from cache or from Nextcloud, or unshare (depending on context) + static func deleteOrUnshareAction(selectedMetadatas: [tableMetadata], metadataFolder: tableMetadata? = nil, controller: NCMainTabBarController?, order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + var titleDelete = NSLocalizedString("_delete_", comment: "") + var message = NSLocalizedString("_want_delete_", comment: "") + var icon = "trashIcon" + var destructive = false + var color = NCBrandColor.shared.iconImageColor + + if selectedMetadatas.count > 1 { + titleDelete = NSLocalizedString("_delete_selected_files_", comment: "") + destructive = true + } else if let metadata = selectedMetadatas.first { + if NCManageDatabase.shared.isMetadataShareOrMounted(metadata: metadata, metadataFolder: metadataFolder) { + titleDelete = NSLocalizedString("_leave_share_", comment: "") + message = NSLocalizedString("_want_leave_share_", comment: "") + icon = "person.2.slash" + } else if metadata.directory { + titleDelete = NSLocalizedString("_delete_folder_", comment: "") + destructive = true + } else { + titleDelete = NSLocalizedString("_delete_file_", comment: "") + destructive = true + } + + if let metadataFolder = metadataFolder { + let isShare = metadata.permissions.contains(NCMetadataPermissions.permissionShared) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionShared) + let isMounted = metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionMounted) + if isShare || isMounted { + titleDelete = NSLocalizedString("_leave_share_", comment: "") + icon = "person.2.slash" + } + } + } // else: no metadata selected + + let canDeleteServer = selectedMetadatas.allSatisfy { !$0.lock } + var fileList = "" + for (ix, metadata) in selectedMetadatas.enumerated() { + guard ix < 3 else { fileList += "\n - ..."; break } + fileList += "\n - " + metadata.fileNameView + } + if destructive { color = .red } + + return NCMenuAction( + title: titleDelete, + destructive: destructive, + icon: NCUtility().loadImage(named: icon, colors: [color]).withTintColor(NCBrandColor.shared.iconImageColor), + order: order, + sender: sender, + action: { _ in + let alertController = UIAlertController.deleteFileOrFolder(titleString: titleDelete + "?", message: message + fileList, canDeleteServer: canDeleteServer, selectedMetadatas: selectedMetadatas, sceneIdentifier: controller?.sceneIdentifier, controller: controller) { _ in + completion?() + } + + controller?.present(alertController, animated: true, completion: nil) + }) + } + + /// Open "share view" (activity VC) to open files in another app + static func share(selectedMetadatas: [tableMetadata], controller: NCMainTabBarController?, order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + NCMenuAction( +// title: NSLocalizedString("_share_", comment: ""), +// icon: NCUtility().loadImage(named: "share", colors: [NCBrandColor.shared.iconImageColor]), + title: NSLocalizedString("_open_in_", comment: ""), + icon: NCUtility().loadImage(named: "open_file",colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + order: order, + sender: sender, + action: { _ in + Task { + await NCCreate().createActivityViewController(selectedMetadata: selectedMetadatas, controller: controller, sender: sender) + completion?() + } + } + ) + } + + /// Set (or remove) a file as *available offline*. Downloads the file if not downloaded already + static func setAvailableOfflineAction(selectedMetadatas: [tableMetadata], isAnyOffline: Bool, viewController: UIViewController, order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + NCMenuAction( + title: isAnyOffline ? NSLocalizedString("_remove_available_offline_", comment: "") : NSLocalizedString("_set_available_offline_", comment: ""), + icon: NCUtility().loadImage(named: "cloudDownload", colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + order: order, + sender: sender, + action: { _ in + if !isAnyOffline, selectedMetadatas.count > 3 { + let alert = UIAlertController( + title: NSLocalizedString("_set_available_offline_", comment: ""), + message: NSLocalizedString("_select_offline_warning_", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("_continue_", comment: ""), style: .default, handler: { _ in + Task { + for metadata in selectedMetadatas { + await NCNetworking.shared.setMetadataAvalableOffline(metadata, isOffline: isAnyOffline) + + } + completion?() + } + })) + alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel)) + viewController.present(alert, animated: true) + } else { + Task { + for metadata in selectedMetadatas { + await NCNetworking.shared.setMetadataAvalableOffline(metadata, isOffline: isAnyOffline) + + } + completion?() + } + } + } + ) + } + + /// Copy files to pasteboard + static func copyAction(fileSelect: [String], controller: NCMainTabBarController?, order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + NCMenuAction( + title: NSLocalizedString("_copy_file_", comment: ""), + icon: NCUtility().loadImage(named: "copy", colors: [NCBrandColor.shared.iconColor]), + order: order, + sender: sender, + action: { _ in +// NCActionCenter.shared.copyPasteboard(pasteboardOcIds: fileSelect, controller: controller) + completion?() + } + ) + } + + /// Open view that lets the user move or copy the files within Nextcloud + static func moveOrCopyAction(selectedMetadatas: [tableMetadata], account: String, viewController: UIViewController, order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + NCMenuAction( + title: NSLocalizedString("_move_or_copy_", comment: ""), + icon: NCUtility().loadImage(named: "move", colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + order: order, + sender: sender, + action: { _ in + Task { @MainActor in + var fileNameError: NKError? + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) + + for metadata in selectedMetadatas { + if let sceneIdentifier = metadata.sceneIdentifier, + let controller = SceneManager.shared.getController(sceneIdentifier: sceneIdentifier), + let checkError = FileNameValidator.checkFileName(metadata.fileNameView, account: controller.account, capabilities: capabilities) { + + fileNameError = checkError + break + } + } + + if let fileNameError { + let message = "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))" + await UIAlertController.warningAsync( message: message, presenter: viewController) + } else { + let controller = viewController.tabBarController as? NCMainTabBarController + NCSelectOpen.shared.openView(items: selectedMetadatas, controller: controller) + } + completion?() + } + } + ) + } + + /// Lock or unlock a file using *files_lock* + static func lockUnlockFiles(shouldLock: Bool, metadatas: [tableMetadata], order: Int = 0, sender: Any?, completion: (() -> Void)? = nil) -> NCMenuAction { + let titleKey: String + if metadatas.count == 1 { + titleKey = shouldLock ? "_lock_file_" : "_unlock_file_" + } else { + titleKey = shouldLock ? "_lock_selected_files_" : "_unlock_selected_files_" + } + let imageName = !shouldLock ? "lock_open" : "lock" + return NCMenuAction( + title: NSLocalizedString(titleKey, comment: ""), + icon: NCUtility().loadImage(named: imageName, colors: [NCBrandColor.shared.iconImageColor]).withTintColor(NCBrandColor.shared.iconImageColor), + order: order, + sender: sender, + action: { _ in + for metadata in metadatas where metadata.lock != shouldLock { + NCNetworking.shared.lockUnlockFile(metadata, shoulLock: shouldLock) + } + completion?() + } + ) + } +} diff --git a/iOSClient/Menu/NCSortMenu.swift b/iOSClient/Menu/NCSortMenu.swift new file mode 100644 index 0000000000..ffb6f0b1d7 --- /dev/null +++ b/iOSClient/Menu/NCSortMenu.swift @@ -0,0 +1,171 @@ +// +// NCSortMenu.swift +// Nextcloud +// +// Created by Marino Faggiana on 27/08/2020. +// Copyright © 2020 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 UIKit +import FloatingPanel +import NextcloudKit + +class NCSortMenu: NSObject { + + private var sortButton: UIButton? + private var serverUrl: String = "" + private var hideDirectoryOnTop: Bool? + private var account: String = "" + + private var key = "" + + func toggleMenu(viewController: UIViewController, account: String, key: String, sortButton: UIButton?, serverUrl: String, hideDirectoryOnTop: Bool = false) { + + self.key = key + self.sortButton = sortButton + self.serverUrl = serverUrl + self.hideDirectoryOnTop = hideDirectoryOnTop + self.account = account + +// guard let layoutForView = NCManageDatabase.shared.getLayoutForView(account: account, key: key, serverUrl: serverUrl) else { return } +// var actions = [NCMenuAction]() +// var title = "" +// var icon = UIImage() +// +// if layoutForView.ascending { +// title = NSLocalizedString("_order_by_name_z_a_", comment: "") +// icon = UIImage(named: "sortFileNameZA")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_name_a_z_", comment: "") +// icon = UIImage(named: "sortFileNameAZ")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// icon = UIImage(named: "sortFileNameZA")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_name_a_z_", comment: "") +// icon = UIImage(named: "sortFileNameAZ")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } +// +// actions.append( +// NCMenuAction( +// title: title, +// icon: icon, +// selected: layoutForView.sort == "fileName", +// on: layoutForView.sort == "fileName", +// action: { _ in +// layoutForView.sort = "fileName" +// layoutForView.ascending = !layoutForView.ascending +// self.actionMenu(layoutForView: layoutForView) +// } +// ) +// ) +// +// if layoutForView.ascending { +// title = NSLocalizedString("_order_by_date_more_recent_", comment: "") +// icon = UIImage(named: "sortDateMoreRecent")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_date_less_recent_", comment: "") +// icon = UIImage(named: "sortDateLessRecent")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// icon = UIImage(named: "sortDateMoreRecent")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_date_less_recent_", comment: "") +// icon = UIImage(named: "sortDateLessRecent")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } +// +// actions.append( +// NCMenuAction( +// title: title, +// icon: icon, +// selected: layoutForView.sort == "date", +// on: layoutForView.sort == "date", +// action: { _ in +// layoutForView.sort = "date" +// layoutForView.ascending = !layoutForView.ascending +// self.actionMenu(layoutForView: layoutForView) +// } +// ) +// ) +// +// if layoutForView.ascending { +// title = NSLocalizedString("_order_by_size_largest_", comment: "") +// icon = UIImage(named: "sortLargest")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_size_smallest_", comment: "") +// icon = UIImage(named: "sortSmallest")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// icon = UIImage(named: "sortLargest")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } else { +// title = NSLocalizedString("_order_by_size_smallest_", comment: "") +// icon = UIImage(named: "sortSmallest")!.image(color: NCBrandColor.shared.iconImageColor, size: 50) +// } +// +// actions.append( +// NCMenuAction( +// title: title, +// icon: icon, +// selected: layoutForView.sort == "size", +// on: layoutForView.sort == "size", +// action: { _ in +// layoutForView.sort = "size" +// layoutForView.ascending = !layoutForView.ascending +// self.actionMenu(layoutForView: layoutForView) +// } +// ) +// ) +// +// if !hideDirectoryOnTop { +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_directory_on_top_no_", comment: ""), +// icon: UIImage(named: "foldersOnTop")!.image(color: NCBrandColor.shared.iconImageColor, size: 50), +// icon: UIImage(named: "foldersOnTop")!.image(color: NCBrandColor.shared.iconImageColor, size: 50), +// selected: layoutForView.directoryOnTop, +// on: layoutForView.directoryOnTop, +// action: { _ in +// layoutForView.directoryOnTop = !layoutForView.directoryOnTop +// NCKeychain().setDirectoryOnTop(account: self.account, value: layoutForView.directoryOnTop) +//// NCKeychain().setDirectoryOnTop(account: self.account, value: layoutForView.directoryOnTop) +// self.actionMenu(layoutForView: layoutForView) +// } +// ) +// ) +// } +// +// viewController.presentMenu(with: actions) + } +// + func actionMenu(layoutForView: NCDBLayoutForView) { +// +// switch layoutForView.sort { +// case "fileName": +// layoutForView.titleButtonHeader = layoutForView.ascending ? "_sorted_by_name_a_z_" : "_sorted_by_name_z_a_" +// case "date": +// layoutForView.titleButtonHeader = layoutForView.ascending ? "_sorted_by_date_less_recent_" : "_sorted_by_date_more_recent_" +// case "size": +// layoutForView.titleButtonHeader = layoutForView.ascending ? "_sorted_by_size_smallest_" : "_sorted_by_size_largest_" +// default: +// break +// } +// +// self.sortButton?.setTitle(NSLocalizedString(layoutForView.titleButtonHeader, comment: ""), for: .normal) +// NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView) +// NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeLayout, +// object: nil, +// userInfo: ["account": self.account, +// "serverUrl": self.serverUrl, +// "layoutForView": layoutForView]) +// NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource) + } +} diff --git a/iOSClient/Networking/NCDownloadAction.swift b/iOSClient/Networking/NCDownloadAction.swift new file mode 100644 index 0000000000..982aca2a66 --- /dev/null +++ b/iOSClient/Networking/NCDownloadAction.swift @@ -0,0 +1,740 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import Queuer +import SVGKit +import Photos +import Alamofire + +class NCDownloadAction: NSObject, UIDocumentInteractionControllerDelegate, NCSelectDelegate, NCTransferDelegate { + static let shared = NCDownloadAction() + + var viewerQuickLook: NCViewerQuickLook? + var documentController: UIDocumentInteractionController? + let utilityFileSystem = NCUtilityFileSystem() + let utility = NCUtility() + let global = NCGlobal.shared + var sceneIdentifier: String = "" + + override private init() { } + + func setup(sceneIdentifier: String) { + self.sceneIdentifier = sceneIdentifier + + Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + } + } + + // MARK: - Download + + func transferChange(status: String, metadata: tableMetadata, error: NKError) { + DispatchQueue.main.async { + switch status { + /// DOWNLOADED + case self.global.networkingStatusDownloaded: + self.downloadedFile(metadata: metadata, error: error) + default: + break + } + } + } + + func downloadedFile(metadata: tableMetadata, error: NKError) { + guard error == .success else { + return + } + /// Select UIWindowScene active in serverUrl + var controller: NCMainTabBarController? + let windowScenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + if windowScenes.count == 1 { + controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController + } else if let sceneIdentifier = metadata.sceneIdentifier, + let tabBarController = SceneManager.shared.getController(sceneIdentifier: sceneIdentifier) { + controller = tabBarController + } else { + for windowScene in windowScenes { + if let rootViewController = windowScene.keyWindow?.rootViewController as? NCMainTabBarController, + rootViewController.currentServerUrl() == metadata.serverUrl { + controller = rootViewController + break + } + } + } + guard let controller else { return } + + switch metadata.sessionSelector { + case NCGlobal.shared.selectorLoadFileQuickLook: + + let fileNamePath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + let fileNameTemp = NSTemporaryDirectory() + metadata.fileNameView + let viewerQuickLook = NCViewerQuickLook(with: URL(fileURLWithPath: fileNameTemp), isEditingEnabled: true, metadata: metadata) + if let image = UIImage(contentsOfFile: fileNamePath) { + if let data = image.jpegData(compressionQuality: 1) { + do { + try data.write(to: URL(fileURLWithPath: fileNameTemp)) + } catch { + return + } + } + let navigationController = UINavigationController(rootViewController: viewerQuickLook) + navigationController.modalPresentationStyle = .fullScreen + controller.present(navigationController, animated: true) + } else { + self.utilityFileSystem.copyFile(atPath: fileNamePath, toPath: fileNameTemp) + controller.present(viewerQuickLook, animated: true) + } + + case NCGlobal.shared.selectorLoadFileView: + guard !isAppInBackground + else { + return + } + + if metadata.contentType.contains("opendocument") && !self.utility.isTypeFileRichDocument(metadata) { + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + } else if metadata.classFile == NKTypeClassFile.compress.rawValue || metadata.classFile == NKTypeClassFile.unknow.rawValue { + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + } else { + if let viewController = controller.currentViewController() { + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) + Task { + if let vc = await NCViewer().getViewerController(metadata: metadata, image: image, delegate: viewController) { + await viewController.navigationController?.pushViewController(vc, animated: true) + } + } + } + } + + case NCGlobal.shared.selectorOpenIn: + guard !isAppInBackground + else { + return + } + + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + + case NCGlobal.shared.selectorPrint: + // waiting close menu + // https://github.com/nextcloud/ios/issues/2278 +// DispatchQueue.main.async { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.printDocument(metadata: metadata) + } + + case NCGlobal.shared.selectorSaveAlbum: + + self.saveAlbum(metadata: metadata, controller: controller) + + case NCGlobal.shared.selectorSaveAsScan: + + self.saveAsScan(metadata: metadata, controller: controller) + + case NCGlobal.shared.selectorOpenDetail: + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterOpenMediaDetail, userInfo: ["ocId": metadata.ocId]) + + default: + let applicationHandle = NCApplicationHandle() + applicationHandle.downloadedFile(selector: metadata.sessionSelector, metadata: metadata) + } + } + + // MARK: - + + func setMetadataAvalableOffline(_ metadata: tableMetadata, isOffline: Bool) async { + if isOffline { + if metadata.directory { + await NCManageDatabase.shared.setDirectoryAsync(serverUrl: metadata.serverUrlFileName, offline: false, metadata: metadata) + let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND sessionSelector == %@ AND status == %d", metadata.account, metadata.serverUrlFileName, NCGlobal.shared.selectorSynchronizationOffline, NCGlobal.shared.metadataStatusWaitDownload) + if let metadatas = await NCManageDatabase.shared.getMetadatasAsync(predicate: predicate) { + await NCManageDatabase.shared.clearMetadatasSessionAsync(metadatas: metadatas) + } + } else { + await NCManageDatabase.shared.setOffLocalFileAsync(ocId: metadata.ocId) + } + } else if metadata.directory { + await NCManageDatabase.shared.cleanTablesOcIds(account: metadata.account, userId: metadata.userId, urlBase: metadata.urlBase) + await NCManageDatabase.shared.setDirectoryAsync(serverUrl: metadata.serverUrlFileName, offline: true, metadata: metadata) + await NCNetworking.shared.synchronization(account: metadata.account, serverUrl: metadata.serverUrlFileName, userId: metadata.userId, urlBase: metadata.urlBase, metadatasInDownload: nil) + } else { + var metadatasSynchronizationOffline: [tableMetadata] = [] + metadatasSynchronizationOffline.append(metadata) + if let metadata = await NCManageDatabase.shared.getMetadataLivePhotoAsync(metadata: metadata) { + metadatasSynchronizationOffline.append(metadata) + } + await NCManageDatabase.shared.addLocalFileAsync(metadata: metadata, offline: true) + for metadata in metadatasSynchronizationOffline { + await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownloadBackground, + selector: NCGlobal.shared.selectorSynchronizationOffline) + } + } + } + + // MARK: - + + @MainActor + func viewerFile(account: String, fileId: String, viewController: UIViewController) async { + var downloadRequest: DownloadRequest? + let hud = NCHud(viewController.tabBarController?.view) + + if let metadata = await NCManageDatabase.shared.getMetadataFromFileIdAsync(fileId) { + do { + let attr = try FileManager.default.attributesOfItem(atPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + let fileSize = attr[FileAttributeKey.size] as? UInt64 ?? 0 + if fileSize > 0 { + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + return + } + } catch { + print("Error: \(error)") + } + } + + hud.ringProgress(tapToCancelDetailText: true) { + if let request = downloadRequest { + request.cancel() + } + } + + let resultsFile = await NextcloudKit.shared.getFileFromFileIdAsync(fileId: fileId, account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: fileId, + name: "getFileFromFileId") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + hud.dismiss() + guard resultsFile.error == .success, let file = resultsFile.file else { + NCContentPresenter().showError(error: resultsFile.error) + return + } + + let metadata = await NCManageDatabase.shared.convertFileToMetadataAsync(file) + await NCManageDatabase.shared.addMetadataAsync(metadata) + + let fileNameLocalPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + + if metadata.isAudioOrVideo { + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + return + } + + hud.show() + let download = await NextcloudKit.shared.downloadAsync(serverUrlFileName: metadata.serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account) { request in + downloadRequest = request + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, + path: metadata.serverUrlFileName, + name: "download") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + + let ocId = metadata.ocId + await NCManageDatabase.shared.setMetadataSessionAsync(ocId: ocId, + sessionTaskIdentifier: task.taskIdentifier, + status: self.global.metadataStatusDownloading) + } + } progressHandler: { progress in + hud.progress(progress.fractionCompleted) + } + + hud.dismiss() + await NCManageDatabase.shared.setMetadataSessionAsync(ocId: metadata.ocId, + session: "", + sessionTaskIdentifier: 0, + sessionError: "", + status: self.global.metadataStatusNormal, + etag: download.etag) + + if download.nkError == .success { + await NCManageDatabase.shared.addLocalFileAsync(metadata: metadata) + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + } + } + + // MARK: - + + func openShare(viewController: UIViewController, metadata: tableMetadata, page: NCBrandOptions.NCInfoPagingTab) { + var page = page + let capabilities = NCNetworking.shared.capabilities[metadata.account] ?? NKCapabilities.Capabilities() + + NCActivityIndicator.shared.start(backgroundView: viewController.view) + NCNetworking.shared.readFile(serverUrlFileName: metadata.serverUrlFileName, account: metadata.account) { _, metadata, file, error in + Task { @MainActor in + NCActivityIndicator.shared.stop() + + if let metadata = metadata, error == .success { + let shareNavigationController = UIStoryboard(name: "NCShare", bundle: nil).instantiateInitialViewController() as? UINavigationController + let shareViewController = shareNavigationController?.topViewController as? NCShare + shareViewController?.metadata = metadata + shareNavigationController?.modalPresentationStyle = .formSheet + if let shareNavigationController = shareNavigationController { + viewController.present(shareNavigationController, animated: true, completion: nil) + } + } + } + } + } + + // MARK: - Open Activity [Share] ... + + func openActivityViewController(selectedMetadata: [tableMetadata], controller: NCMainTabBarController?, sender: Any?) { + guard let controller else { return } + let metadatas = selectedMetadata.filter({ !$0.directory }) + var urls: [URL] = [] + var downloadMetadata: [(tableMetadata, URL)] = [] + + for metadata in metadatas { + let fileURL = URL(fileURLWithPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + if utilityFileSystem.fileProviderStorageExists(metadata) { + urls.append(fileURL) + } else { + downloadMetadata.append((metadata, fileURL)) + } + } + + let processor = ParallelWorker(n: 5, titleKey: "_downloading_", totalTasks: downloadMetadata.count, controller: controller) + for (metadata, url) in downloadMetadata { + processor.execute { completion in + Task { + guard let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: "", + sceneIdentifier: controller.sceneIdentifier) else { + return completion() + } + + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + } progressHandler: { progress in + processor.hud.progress(progress.fractionCompleted) + } + + if self.utilityFileSystem.fileProviderStorageExists(metadata) { + urls.append(url) + } + completion() + } + } + } + + processor.completeWork { + guard !urls.isEmpty else { return } + let activityViewController = UIActivityViewController(activityItems: urls, applicationActivities: nil) + + // iPad + if let popover = activityViewController.popoverPresentationController { + if let view = sender as? UIView { + popover.sourceView = view + popover.sourceRect = view.bounds + } else { + popover.sourceView = controller.view + popover.sourceRect = CGRect(x: controller.view.bounds.midX, + y: controller.view.bounds.midY, + width: 0, + height: 0) + popover.permittedArrowDirections = [] + } + } + + controller.present(activityViewController, animated: true) + } + } + + // MARK: - Save as scan + + func saveAsScan(metadata: tableMetadata, controller: NCMainTabBarController?) { + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + let fileNameDestination = utilityFileSystem.createFileName("scan.png", fileDate: Date(), fileType: PHAssetMediaType.image, notUseMask: true) + let fileNamePathDestination = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryScan, fileName: fileNameDestination) + + utilityFileSystem.copyFile(atPath: fileNamePath, toPath: fileNamePathDestination) + + if let navigationController = UIStoryboard(name: "NCScan", bundle: nil).instantiateInitialViewController() { + navigationController.modalPresentationStyle = UIModalPresentationStyle.pageSheet + let viewController = navigationController.presentedViewController as? NCScan + viewController?.serverUrl = controller?.currentServerUrl() + viewController?.controller = controller + controller?.present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - Save photo + + func saveAlbum(metadata: tableMetadata, controller: NCMainTabBarController?) { + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + + NCAskAuthorization().askAuthorizationPhotoLibrary(controller: controller) { hasPermission in + guard hasPermission else { + let error = NKError(errorCode: NCGlobal.shared.errorFileNotSaved, errorDescription: "_access_photo_not_enabled_msg_") + return NCContentPresenter().messageNotification("_access_photo_not_enabled_", error: error, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + + let errorSave = NKError(errorCode: NCGlobal.shared.errorFileNotSaved, errorDescription: "_file_not_saved_cameraroll_") + + do { + if metadata.isImage { + let data = try Data(contentsOf: URL(fileURLWithPath: fileNamePath)) + PHPhotoLibrary.shared().performChanges({ + let assetRequest = PHAssetCreationRequest.forAsset() + assetRequest.addResource(with: .photo, data: data, options: nil) + }) { success, _ in + if !success { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } else if metadata.isVideo { + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: fileNamePath)) + }) { success, _ in + if !success { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } else { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + return + } + } catch { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } + + // MARK: - Copy & Paste + + func copyPasteboard(pasteboardOcIds: [String], controller: NCMainTabBarController?) { + var items = [[String: Any]]() + let hudView = controller + + // getting file data can take some time and block the main queue + DispatchQueue.global(qos: .userInitiated).async { + var downloadMetadatas: [tableMetadata] = [] + for ocid in pasteboardOcIds { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocid) else { continue } + if let pasteboardItem = metadata.toPasteBoardItem() { + items.append(pasteboardItem) + } else { + downloadMetadatas.append(metadata) + } + } + + let processor = ParallelWorker(n: 5, titleKey: "_downloading_", totalTasks: downloadMetadatas.count, controller: controller) + for metadata in downloadMetadatas { + processor.execute { completion in + Task { + guard let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: "", + sceneIdentifier: controller?.sceneIdentifier) else { + return completion() + } + + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + } progressHandler: { progress in + processor.hud.progress(progress.fractionCompleted) + } + + completion() + } + } + } + + processor.completeWork { + items.append(contentsOf: downloadMetadatas.compactMap({ $0.toPasteBoardItem() })) + UIPasteboard.general.setItems(items, options: [:]) + } + } + } + + func pastePasteboard(serverUrl: String, account: String, controller: NCMainTabBarController?) async { + var fractionCompleted: Float = 0 + let processor = ParallelWorker(n: 5, titleKey: "_status_uploading_", totalTasks: nil, controller: controller) + guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: account) else { + return + } + + func uploadPastePasteboard(fileName: String, serverUrlFileName: String, fileNameLocalPath: String, serverUrl: String, completion: @escaping () -> Void) { + NextcloudKit.shared.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account) { _ in + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: serverUrlFileName, + name: "upload") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } progressHandler: { progress in + if Float(progress.fractionCompleted) > fractionCompleted || fractionCompleted == 0 { + processor.hud.progress(progress.fractionCompleted) + fractionCompleted = Float(progress.fractionCompleted) + } + } completionHandler: { account, ocId, etag, _, _, _, error in + if error == .success && etag != nil && ocId != nil { + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocId!, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) + NCManageDatabase.shared.addLocalFile(account: account, etag: etag!, ocId: ocId!, fileName: fileName) + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferRequestData(serverUrl: serverUrl) + } + } + } else { + NCContentPresenter().showError(error: error) + } + fractionCompleted = 0 + completion() + } + } + + for (index, items) in UIPasteboard.general.items.enumerated() { + for item in items { + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) + let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + guard let data = UIPasteboard.general.data(forPasteboardType: item.key, inItemSet: IndexSet([index]))?.first else { + continue + } + let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext + let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) + let ocIdUpload = UUID().uuidString + let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(ocIdUpload, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + do { try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) } catch { continue } + processor.execute { completion in + uploadPastePasteboard(fileName: fileName, serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, serverUrl: serverUrl, completion: completion) + } + } + } + processor.completeWork() + } + + // MARK: - + + func openFileViewInFolder(serverUrl: String, fileNameBlink: String?, fileNameOpen: String?, sceneIdentifier: String) { + guard let controller = SceneManager.shared.getController(sceneIdentifier: sceneIdentifier), + let navigationController = controller.viewControllers?.first as? UINavigationController + else { return } + let session = NCSession.shared.getSession(controller: controller) + var serverUrlPush = self.utilityFileSystem.getHomeServer(session: session) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + navigationController.popToRootViewController(animated: false) + controller.selectedIndex = 0 + if serverUrlPush == serverUrl, + let viewController = navigationController.topViewController as? NCFiles { + viewController.blinkCell(fileName: fileNameBlink) + viewController.openFile(fileName: fileNameOpen) + return + } + + let diffDirectory = serverUrl.replacingOccurrences(of: serverUrlPush, with: "") + var subDirs = diffDirectory.split(separator: "/") + + while serverUrlPush != serverUrl, !subDirs.isEmpty { + + guard let dir = subDirs.first else { + return + } + serverUrlPush = self.utilityFileSystem.createServerUrl(serverUrl: serverUrlPush, fileName: String(dir)) + + if let viewController = controller.navigationCollectionViewCommon.first(where: { $0.navigationController == navigationController && $0.serverUrl == serverUrlPush})?.viewController as? NCFiles, viewController.isViewLoaded { + viewController.fileNameBlink = fileNameBlink + viewController.fileNameOpen = fileNameOpen + navigationController.pushViewController(viewController, animated: false) + } else { + if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { + viewController.serverUrl = serverUrlPush + viewController.titleCurrentFolder = String(dir) + viewController.navigationItem.backButtonTitle = viewController.titleCurrentFolder + + controller.navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: navigationController, viewController: viewController)) + + if serverUrlPush == serverUrl { + viewController.fileNameBlink = fileNameBlink + viewController.fileNameOpen = fileNameOpen + } + navigationController.pushViewController(viewController, animated: false) + } + } + subDirs.remove(at: 0) + } + } + } + + // MARK: - NCSelect + Delegate + + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool) {//, session: NCSession.Session) { + if let destination = serverUrl, !items.isEmpty { + if copy { + for case let metadata as tableMetadata in items { + if metadata.status != NCGlobal.shared.metadataStatusNormal, metadata.status != NCGlobal.shared.metadataStatusWaitCopy { + continue + } + + NCNetworking.shared.copyMetadata(metadata, destination: destination, overwrite: overwrite) + } + + } else if move { + for case let metadata as tableMetadata in items { + if metadata.status != NCGlobal.shared.metadataStatusNormal, metadata.status != NCGlobal.shared.metadataStatusWaitMove { + continue + } + + NCNetworking.shared.moveMetadata(metadata, destination: destination, overwrite: overwrite) + } + } + } + } + + func openSelectView(items: [tableMetadata], controller: NCMainTabBarController?) { + let session = NCSession.shared.getSession(controller: controller) + let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController + let topViewController = navigationController?.topViewController as? NCSelect + var listViewController = [NCSelect]() + var copyItems: [tableMetadata] = [] + let capabilities = NCNetworking.shared.capabilities[controller?.account ?? ""] ?? NKCapabilities.Capabilities() + + for item in items { + if let fileNameError = FileNameValidator.checkFileName(item.fileNameView, account: controller?.account, capabilities: capabilities) { + controller?.present(UIAlertController.warning(message: "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"), animated: true) + return + } + copyItems.append(item) + } + + let home = utilityFileSystem.getHomeServer(session: session) + var serverUrl = copyItems[0].serverUrl + + // Setup view controllers such that the current view is of the same directory the items to be copied are in + while true { + // If not in the topmost directory, create a new view controller and set correct title. + // If in the topmost directory, use the default view controller as the base. + var viewController: NCSelect? + if serverUrl != home { + viewController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateViewController(withIdentifier: "NCSelect.storyboard") as? NCSelect + if viewController == nil { + return + } + viewController!.titleCurrentFolder = (serverUrl as NSString).lastPathComponent + } else { + viewController = topViewController + } + guard let vc = viewController else { return } + + vc.delegate = self + vc.typeOfCommandView = .copyMove + vc.items = copyItems + vc.serverUrl = serverUrl + vc.session = session + + vc.navigationItem.backButtonTitle = vc.titleCurrentFolder + listViewController.insert(vc, at: 0) + + if serverUrl != home { + if let serverDirectoryUp = utilityFileSystem.serverDirectoryUp(serverUrl: serverUrl, home: home) { + serverUrl = serverDirectoryUp + } + } else { + break + } + } + + navigationController?.setViewControllers(listViewController, animated: false) + navigationController?.modalPresentationStyle = .formSheet + + if let navigationController = navigationController { + controller?.present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - Print + + func printDocument(metadata: tableMetadata) { + + let fileNameURL = URL(fileURLWithPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + let printController = UIPrintInteractionController.shared + let printInfo = UIPrintInfo(dictionary: nil) + + printInfo.jobName = fileNameURL.lastPathComponent + printInfo.outputType = metadata.isImage ? .photo : .general + printController.printInfo = printInfo + printController.showsNumberOfCopies = true + + guard !UIPrintInteractionController.canPrint(fileNameURL) else { + printController.printingItem = fileNameURL + printController.present(animated: true) + return + } + + // can't print without data + guard let data = try? Data(contentsOf: fileNameURL) else { return } + + if let svg = SVGKImage(data: data) { + printController.printingItem = svg.uiImage + printController.present(animated: true) + return + } + + guard let text = String(data: data, encoding: .utf8) else { return } + let formatter = UISimpleTextPrintFormatter(text: text) + formatter.perPageContentInsets.top = 72 + formatter.perPageContentInsets.bottom = 72 + formatter.perPageContentInsets.left = 72 + formatter.perPageContentInsets.right = 72 + printController.printFormatter = formatter + printController.present(animated: true) + } +} + +fileprivate extension tableMetadata { + func toPasteBoardItem() -> [String: Any]? { + // Get Data + let fileUrl = URL(fileURLWithPath: NCUtilityFileSystem().getDirectoryProviderStorageOcId(ocId, + fileName: fileNameView, + userId: userId, + urlBase: urlBase)) + guard NCUtilityFileSystem().fileProviderStorageExists(self), + let data = try? Data(contentsOf: fileUrl) else { return nil } + + // Determine the UTI for the file + guard let fileUTI = UTType(filenameExtension: fileExtension)?.identifier else { return nil } + + // Pasteboard item + return [fileUTI: data] + } +} diff --git a/iOSClient/Offline/NCOffline.swift b/iOSClient/Offline/NCOffline.swift index c677762068..6c2d512a6d 100644 --- a/iOSClient/Offline/NCOffline.swift +++ b/iOSClient/Offline/NCOffline.swift @@ -34,7 +34,7 @@ class NCOffline: NCCollectionViewCommon { layoutKey = NCGlobal.shared.layoutViewOffline enableSearchBar = false headerRichWorkspaceDisable = true - emptyImageName = "icloud.and.arrow.down" + emptyImageName = "cloudDownload" emptyTitle = "_files_no_files_" emptyDescription = "_tutorial_offline_view_" emptyDataPortaitOffset = 30 @@ -49,6 +49,7 @@ class NCOffline: NCCollectionViewCommon { Task { await getServerData() } + AnalyticsHelper.shared.trackEvent(eventName: .SCREEN_EVENT__OFFLINE_FILES) } // MARK: - DataSource