From 99332cadf055bf275252f7dd82aa553e6bf46725 Mon Sep 17 00:00:00 2001 From: TSI-amrutwaghmare <96108296+TSI-amrutwaghmare@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:33:00 +0530 Subject: [PATCH 1/2] NMC 2169 - Media theming customisation changes --- iOSClient/AppDelegate.swift | 2 + .../Data/NCManageDatabase+Metadata.swift | 28 +++ iOSClient/Media/NCMedia.storyboard | 116 +++++----- iOSClient/Media/NCMediaCommandView.xib | 178 +++++++++++++++ iOSClient/Menu/NCMedia+Menu.swift | 203 ++++++++++++++++++ iOSClient/NCImageCache.swift | 13 ++ iOSClient/Settings/NCKeychain.swift | 37 ++++ .../NCViewerMediaDetailView.swift | 1 + .../NCViewerQuickLook/NCViewerQuickLook.swift | 2 +- 9 files changed, 512 insertions(+), 68 deletions(-) create mode 100644 iOSClient/Media/NCMediaCommandView.xib create mode 100644 iOSClient/Menu/NCMedia+Menu.swift diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index fd47529505..0b05110c3e 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -53,6 +53,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if isUiTestingEnabled { NCAccount().deleteAllAccounts() } + UINavigationBar.appearance().tintColor = NCBrandColor.shared.customer + UIToolbar.appearance().tintColor = NCBrandColor.shared.customer let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index b9a48061c4..036be3dec4 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -1260,4 +1260,32 @@ extension NCManageDatabase { let concatenatedEtags = metadatas.map { $0.etag }.joined(separator: "-") return sha256Hash(concatenatedEtags) } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch NCKeychain().mediaSortDate { + case "date": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.date as Date) > ($1.date as Date) } + case "creationDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.creationDate as Date) > ($1.creationDate as Date) } + case "uploadDate": + results = realm.objects(tableMetadata.self).filter(predicate).sorted { ($0.uploadDate as Date) > ($1.uploadDate as Date) } + default: + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } else { + let results = realm.objects(tableMetadata.self).filter(predicate) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + return nil + } } diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index 2658482974..517f2fdfb8 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -1,11 +1,10 @@ - + - + - @@ -19,7 +18,7 @@ - + @@ -32,88 +31,78 @@ - + - - - - - - - - - + + + + + + + + + - - - + + + + + + + + + - + - + @@ -122,11 +111,4 @@ - - - - - - - diff --git a/iOSClient/Media/NCMediaCommandView.xib b/iOSClient/Media/NCMediaCommandView.xib new file mode 100644 index 0000000000..b2df2d68eb --- /dev/null +++ b/iOSClient/Media/NCMediaCommandView.xib @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Menu/NCMedia+Menu.swift b/iOSClient/Menu/NCMedia+Menu.swift new file mode 100644 index 0000000000..cc1d094e39 --- /dev/null +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -0,0 +1,203 @@ +// +// NCMedia+Menu.swift +// Nextcloud +// +// Created by Marino Faggiana on 03/03/2021. +// Copyright © 2021 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 + +extension NCMedia { + func tapSelect() { + self.isEditMode = false + self.selectOcId.removeAll() + self.selectIndexPath.removeAll() + self.collectionView?.reloadData() + } + + func toggleMenu() { + + var actions: [NCMenuAction] = [] + + defer { presentMenu(with: actions) } + + if !isEditMode { + if let metadatas = self.metadatas, !metadatas.isEmpty { + actions.append( + NCMenuAction( + title: NSLocalizedString("_select_", comment: ""), + icon: utility.loadImage(named: "selectFull", color: NCBrandColor.shared.iconColor), + action: { _ in + self.isEditMode = true + } + ) + ) + } + + actions.append(.seperator(order: 0)) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_viewimage_show_", comment: ""), + icon: utility.loadImage(named: showOnlyImages ? "nocamera" : "file_photo_menu",color: NCBrandColor.shared.iconColor), + selected: showOnlyImages, + on: true, + action: { _ in + self.showOnlyImages = true + self.showOnlyVideos = false + self.reloadDataSource() + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_viewvideo_show_", comment: ""), + icon: utility.loadImage(named: showOnlyVideos ? "videono" : "videoyes",color: NCBrandColor.shared.iconColor), + selected: showOnlyVideos, + on: true, + action: { _ in + self.showOnlyImages = false + self.showOnlyVideos = true + self.reloadDataSource() + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_show_all_", comment: ""), + icon: utility.loadImage(named: "photo.on.rectangle.angled", color: NCBrandColor.shared.iconColor), + selected: !showOnlyImages && !showOnlyVideos, + on: true, + action: { _ in + self.showOnlyImages = false + self.showOnlyVideos = false + self.reloadDataSource() + } + ) + ) + + actions.append(.seperator(order: 0)) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_select_media_folder_", comment: ""), + icon: utility.loadImage(named: "folder", color: NCBrandColor.shared.iconColor), + action: { _ in + if let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController, + let viewController = navigationController.topViewController as? NCSelect { + + viewController.delegate = self + viewController.typeOfCommandView = .select + viewController.type = "mediaFolder" + viewController.selectIndexPath = self.selectIndexPath + + self.present(navigationController, animated: true, completion: nil) + } + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_by_modified_date_", comment: ""), + icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + selected: NCKeychain().mediaSortDate == "date", + on: true, + action: { _ in + NCKeychain().mediaSortDate = "date" + self.reloadDataSource() + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_by_created_date_", comment: ""), + icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + selected: NCKeychain().mediaSortDate == "creationDate", + on: true, + action: { _ in + NCKeychain().mediaSortDate = "creationDate" + self.reloadDataSource() + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_media_by_upload_date_", comment: ""), + icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + selected: NCKeychain().mediaSortDate == "uploadDate", + on: true, + action: { _ in + NCKeychain().mediaSortDate = "uploadDate" + self.reloadDataSource() + } + ) + ) + + } else { + + // + // CANCEL + // + actions.append( + NCMenuAction( + title: NSLocalizedString("_cancel_", comment: ""), + icon: utility.loadImage(named: "xmark", color: NCBrandColor.shared.iconColor), + action: { _ in self.tapSelect() } + ) + ) + + guard !selectOcId.isEmpty else { return } + let selectedMetadatas = selectOcId.compactMap(NCManageDatabase.shared.getMetadataFromOcId) + + // + // OPEN IN + // + actions.append(.openInAction(selectedMetadatas: selectedMetadatas, viewController: self, completion: tapSelect)) + + // + // SAVE TO PHOTO GALLERY + // + actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMetadatas, completion: tapSelect)) + + // + // COPY - MOVE + // + actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, indexPath: selectIndexPath, completion: tapSelect)) + + // + // COPY + // + actions.append(.copyAction(selectOcId: selectOcId, completion: tapSelect)) + + // + // DELETE + // can't delete from cache because is needed for NCMedia view, and if locked can't delete from server either. + if !selectedMetadatas.contains(where: { $0.lock && $0.lockOwner != appDelegate.userId }) { + actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, indexPath: selectIndexPath, metadataFolder: nil, viewController: self, completion: tapSelect)) + } + } + } +} diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 8277236a94..496d15bc2d 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -116,6 +116,19 @@ final class NCImageCache: @unchecked Sendable { NotificationCenter.default.removeObserver(self, name: LRUCacheMemoryWarningNotification, object: nil) } + func calculateMaxImages(percentage: Double, imageSizeKB: Double) -> Int { + let totalRamBytes = Double(ProcessInfo.processInfo.physicalMemory) + let cacheSizeBytes = totalRamBytes * (percentage / 100.0) + let imageSizeBytes = imageSizeKB * 1024 + let maxImages = Int(cacheSizeBytes / imageSizeBytes) + + return maxImages + } + + func getMediaMetadatas(account: String, predicate: NSPredicate? = nil) -> ThreadSafeArray? { + return NCManageDatabase.shared.getMediaMetadatas(predicate: predicate ?? predicateBoth, sorted: "date") + } + func allowExtensions(ext: String) -> Bool { return allowExtensions.contains(ext) } diff --git a/iOSClient/Settings/NCKeychain.swift b/iOSClient/Settings/NCKeychain.swift index fa9b17d79a..c3f8ab7405 100644 --- a/iOSClient/Settings/NCKeychain.swift +++ b/iOSClient/Settings/NCKeychain.swift @@ -296,6 +296,43 @@ import KeychainAccess } } + var mediaColumnCount: Int { + get { + if let value = try? keychain.get("mediaColumnCount"), let result = Int(value) { + return result + } + return 3 + } + set { + keychain["mediaColumnCount"] = String(newValue) + } + } + + var mediaTypeLayout: String { + get { + if let value = try? keychain.get("mediaTypeLayout") { + return value + } + return NCGlobal.shared.mediaLayoutRatio + } + set { + keychain["mediaTypeLayout"] = String(newValue) + } + } + + var mediaSortDate: String { + get { + migrate(key: "mediaSortDate") + if let value = try? keychain.get("mediaSortDate") { + return value + } + return "date" + } + set { + keychain["mediaSortDate"] = newValue + } + } + var textRecognitionStatus: Bool { get { migrate(key: "textRecognitionStatus") diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index b2eddaab9a..6ed1d5bd7c 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -209,6 +209,7 @@ class NCViewerMediaDetailView: UIView { } if metadata.isImage && !utilityFileSystem.fileProviderStorageExists(metadata) && metadata.session.isEmpty { + downloadImageButton.tintColor = NCBrandColor.shared.brand downloadImageButton.setTitle(NSLocalizedString("_try_download_full_resolution_", comment: ""), for: .normal) downloadImageLabel.text = NSLocalizedString("_full_resolution_image_info_", comment: "") downloadImageButtonContainer.isHidden = false diff --git a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift index aa700a0c1d..9f75b7f6bb 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -177,7 +177,7 @@ private var hasChangesQuickLook: Bool = false toolbarConfig.optionButtonFontSize = 16 toolbarConfig.optionButtonFontSizeForPad = 21 toolbarConfig.backgroundColor = .systemGray6 - toolbarConfig.foregroundColor = .systemBlue + toolbarConfig.foregroundColor = NCBrandColor.shared.customer var viewConfig = CropViewConfig() viewConfig.cropMaskVisualEffectType = .none From 40e95cbddb451f7069af97954c96a911cd7b6c59 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Mon, 14 Apr 2025 11:39:24 +0530 Subject: [PATCH 2/2] NMC 2169 - Media theming changes --- Nextcloud.xcodeproj/project.pbxproj | 11 ++ .../Data/NCManageDatabase+Metadata.swift | 133 ++++++++++++++++-- iOSClient/Menu/NCMedia+Menu.swift | 65 +++++---- iOSClient/NCImageCache.swift | 12 +- iOSClient/Settings/NCKeychain.swift | 100 +++++++++---- 5 files changed, 258 insertions(+), 63 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 05654d1b07..be7002df6c 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -92,6 +92,10 @@ AFCE353527E4ED5900FEA6C2 /* DateFormatter+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */; }; AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; }; AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; }; + AFCE353927E5DE0500FEA6C2 /* NCShare+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */; }; + B5C980182DACD51C0041B146 /* NCMediaCommandView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5C980172DACD51C0041B146 /* NCMediaCommandView.xib */; }; + B5C9801A2DACD56C0041B146 /* NCMedia+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C980192DACD56C0041B146 /* NCMedia+Menu.swift */; }; + C04E2F232A17BB4D001BAD85 /* FilesIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04E2F222A17BB4D001BAD85 /* FilesIntegrationTests.swift */; }; D575039F27146F93008DC9DC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; }; @@ -1330,6 +1334,9 @@ AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extension.swift"; sourceTree = ""; }; AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCells.swift; sourceTree = ""; }; AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; + AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Helper.swift"; sourceTree = ""; }; + B5C980172DACD51C0041B146 /* NCMediaCommandView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMediaCommandView.xib; sourceTree = ""; }; + B5C980192DACD56C0041B146 /* NCMedia+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+Menu.swift"; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = ""; }; @@ -2076,6 +2083,7 @@ 371B5A2F23D0B04B00FAFAE9 /* Menu */ = { isa = PBXGroup; children = ( + B5C980192DACD56C0041B146 /* NCMedia+Menu.swift */, 3704EB2923D5A58400455C5B /* NCMenu.storyboard */, 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */, AF68326927BE65A90010BF0B /* NCMenuAction.swift */, @@ -3219,6 +3227,7 @@ F7EC9CB921185F2000F1C5CE /* Media */ = { isa = PBXGroup; children = ( + B5C980172DACD51C0041B146 /* NCMediaCommandView.xib */, F720B5B72507B9A5008C94E5 /* Cell */, F7501C302212E57400FB1415 /* NCMedia.storyboard */, F7501C312212E57400FB1415 /* NCMedia.swift */, @@ -3991,6 +4000,7 @@ F74C0437253F1CDC009762AB /* NCShares.storyboard in Resources */, F7F4F10C27ECDBDB008676F9 /* Inconsolata-Regular.ttf in Resources */, F7B8B83025681C3400967775 /* GoogleService-Info.plist in Resources */, + B5C980182DACD51C0041B146 /* NCMediaCommandView.xib in Resources */, F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */, F768822D2C0DD1E7001CF441 /* Acknowledgements.rtf in Resources */, F7E0CDCF265CE8610044854E /* NCUserStatus.storyboard in Resources */, @@ -4854,6 +4864,7 @@ F758B45E212C569D00515F55 /* NCScanCell.swift in Sources */, F78B87E72B62527100C65ADC /* NCMediaDataSource.swift in Sources */, F76882272C0DD1E7001CF441 /* NCManageE2EEView.swift in Sources */, + B5C9801A2DACD56C0041B146 /* NCMedia+Menu.swift in Sources */, F7864ACC2A78FE73004870E0 /* NCManageDatabase+LocalFile.swift in Sources */, F7327E302B73A86700A462C7 /* NCNetworking+WebDAV.swift in Sources */, F79FFB262A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */, diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 036be3dec4..d4df52ab38 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -166,15 +166,23 @@ extension tableMetadata { return true } + var isPrintable: Bool { + if isDocumentViewableOnly { + return false + } + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKCommon.TypeClassFile.image.rawValue { + return true + } + return false + } + var isSavebleInCameraRoll: Bool { return (classFile == NKCommon.TypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKCommon.TypeClassFile.video.rawValue } - - /* + var isDocumentViewableOnly: Bool { sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKCommon.TypeClassFile.document.rawValue } - */ var isAudioOrVideo: Bool { return classFile == NKCommon.TypeClassFile.audio.rawValue || classFile == NKCommon.TypeClassFile.video.rawValue @@ -201,15 +209,15 @@ extension tableMetadata { } var isCopyableInPasteboard: Bool { - !directory + !isDocumentViewableOnly && !directory } var isCopyableMovable: Bool { - !isDirectoryE2EE && !e2eEncrypted + !isDocumentViewableOnly && !isDirectoryE2EE && !e2eEncrypted } var isModifiableWithQuickLook: Bool { - if directory || isDirectoryE2EE { + if directory || isDocumentViewableOnly || isDirectoryE2EE { return false } return isPDF || isImage @@ -223,11 +231,12 @@ extension tableMetadata { } var canSetAsAvailableOffline: Bool { - return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted +// return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted + return session.isEmpty && !isDocumentViewableOnly } var canShare: Bool { - return session.isEmpty && !directory && !NCBrandOptions.shared.disable_openin_file + return session.isEmpty && !isDocumentViewableOnly && !directory && !NCBrandOptions.shared.disable_openin_file } var canSetDirectoryAsE2EE: Bool { @@ -238,6 +247,32 @@ extension tableMetadata { return !isDirectoryE2EE && directory && size == 0 && e2eEncrypted && NCKeychain().isEndToEndEnabled(account: account) } + var canOpenExternalEditor: Bool { + if isDocumentViewableOnly { + return false + } + let utility = NCUtility() + let editors = utility.editorsDirectEditing(account: account, contentType: contentType) + let isRichDocument = utility.isTypeFileRichDocument(self) + return classFile == NKCommon.TypeClassFile.document.rawValue && editors.contains(NCGlobal.shared.editorText) && ((editors.contains(NCGlobal.shared.editorOnlyoffice) || isRichDocument)) + } + + var isWaitingTransfer: Bool { + status == NCGlobal.shared.metadataStatusWaitDownload || status == NCGlobal.shared.metadataStatusWaitUpload || status == NCGlobal.shared.metadataStatusUploadError + } + + var isInTransfer: Bool { + status == NCGlobal.shared.metadataStatusDownloading || status == NCGlobal.shared.metadataStatusUploading + } + + var isTransferInForeground: Bool { + (status > 0 && (chunk > 0 || e2eEncrypted)) + } + + var isDownloadUpload: Bool { + status == NCGlobal.shared.metadataStatusDownloading || status == NCGlobal.shared.metadataStatusUploading + } + var isDownload: Bool { status == NCGlobal.shared.metadataStatusWaitDownload || status == NCGlobal.shared.metadataStatusDownloading } @@ -328,7 +363,7 @@ extension tableMetadata { if !NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingApiEnabled || (NCCapabilities.shared.getCapabilities(account: account).capabilityE2EEEnabled && isDirectoryE2EE) { return false } - return true + return !e2eEncrypted } } @@ -467,6 +502,42 @@ extension NCManageDatabase { completion(metadataFolder, metadatas) } + func convertFilesToMetadatas(_ files: [NKFile], useMetadataFolder: Bool, completion: @escaping (_ metadataFolder: tableMetadata, _ metadatasFolder: [tableMetadata], _ metadatas: [tableMetadata]) -> Void) { + + var counter: Int = 0 + var isDirectoryE2EE: Bool = false + let listServerUrl = ThreadSafeDictionary() + + var metadataFolder = tableMetadata() + var metadataFolders: [tableMetadata] = [] + var metadatas: [tableMetadata] = [] + + for file in files { + + if let key = listServerUrl[file.serverUrl] { + isDirectoryE2EE = key + } else { + isDirectoryE2EE = NCUtilityFileSystem().isDirectoryE2EE(file: file) + listServerUrl[file.serverUrl] = isDirectoryE2EE + } + + let metadata = convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) + + if counter == 0 && useMetadataFolder { + metadataFolder = tableMetadata.init(value: metadata) + } else { + metadatas.append(metadata) + if metadata.directory { + metadataFolders.append(metadata) + } + } + + counter += 1 + } + + completion(metadataFolder, metadataFolders, metadatas) + } + func getMetadataDirectoryFrom(files: [NKFile]) -> tableMetadata? { guard let file = files.first else { return nil } let isDirectoryE2EE = NCUtilityFileSystem().isDirectoryE2EE(file: file) @@ -1168,7 +1239,7 @@ extension NCManageDatabase { return listIdentifierRank } - func clearMetadatasUpload(account: String) { + @objc func clearMetadatasUpload(account: String) { do { let realm = try Realm() try realm.write { @@ -1261,6 +1332,19 @@ extension NCManageDatabase { return sha256Hash(concatenatedEtags) } + func getMediaMetadatas(predicate: NSPredicate) -> ThreadSafeArray? { + + do { + let realm = try Realm() + let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: "date", ascending: false) + return ThreadSafeArray(results.map { tableMetadata.init(value: $0) }) + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + + return nil + } + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { do { @@ -1288,4 +1372,33 @@ extension NCManageDatabase { } return nil } + + func getAdvancedMetadatas(predicate: NSPredicate, page: Int = 0, limit: Int = 0, sorted: String, ascending: Bool) -> [tableMetadata] { + + var metadatas: [tableMetadata] = [] + + do { + let realm = try Realm() + realm.refresh() + let results = realm.objects(tableMetadata.self).filter(predicate).sorted(byKeyPath: sorted, ascending: ascending) + if !results.isEmpty { + if page == 0 || limit == 0 { + return Array(results.map { tableMetadata.init(value: $0) }) + } else { + let nFrom = (page - 1) * limit + let nTo = nFrom + (limit - 1) + for n in nFrom...nTo { + if n == results.count { + break + } + metadatas.append(tableMetadata.init(value: results[n])) + } + } + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("Could not access database: \(error)") + } + + return metadatas + } } diff --git a/iOSClient/Menu/NCMedia+Menu.swift b/iOSClient/Menu/NCMedia+Menu.swift index cc1d094e39..72c6cec47b 100644 --- a/iOSClient/Menu/NCMedia+Menu.swift +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -28,11 +28,19 @@ import NextcloudKit extension NCMedia { func tapSelect() { self.isEditMode = false - self.selectOcId.removeAll() - self.selectIndexPath.removeAll() + self.fileSelect.removeAll() self.collectionView?.reloadData() } + func selectAll() { + if !fileSelect.isEmpty, self.dataSource.metadatas.count == fileSelect.count { + fileSelect = [] + } else { + fileSelect = self.dataSource.metadatas.compactMap({ $0.ocId }) + } + self.collectionView.reloadData() + } + func toggleMenu() { var actions: [NCMenuAction] = [] @@ -44,7 +52,7 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_select_", comment: ""), - icon: utility.loadImage(named: "selectFull", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "selectFull", colors: [NCBrandColor.shared.iconColor]), action: { _ in self.isEditMode = true } @@ -57,13 +65,13 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_viewimage_show_", comment: ""), - icon: utility.loadImage(named: showOnlyImages ? "nocamera" : "file_photo_menu",color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: showOnlyImages ? "nocamera" : "file_photo_menu", colors: [NCBrandColor.shared.iconColor]), selected: showOnlyImages, on: true, action: { _ in self.showOnlyImages = true self.showOnlyVideos = false - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -71,13 +79,13 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_viewvideo_show_", comment: ""), - icon: utility.loadImage(named: showOnlyVideos ? "videono" : "videoyes",color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: showOnlyVideos ? "videono" : "videoyes", colors: [NCBrandColor.shared.iconColor]), selected: showOnlyVideos, on: true, action: { _ in self.showOnlyImages = false self.showOnlyVideos = true - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -85,13 +93,13 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_show_all_", comment: ""), - icon: utility.loadImage(named: "photo.on.rectangle.angled", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "photo.on.rectangle.angled", colors: [NCBrandColor.shared.iconColor]), selected: !showOnlyImages && !showOnlyVideos, on: true, action: { _ in self.showOnlyImages = false self.showOnlyVideos = false - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -101,7 +109,7 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_select_media_folder_", comment: ""), - icon: utility.loadImage(named: "folder", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "folder", colors: [NCBrandColor.shared.iconColor]), action: { _ in if let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController, let viewController = navigationController.topViewController as? NCSelect { @@ -109,7 +117,6 @@ extension NCMedia { viewController.delegate = self viewController.typeOfCommandView = .select viewController.type = "mediaFolder" - viewController.selectIndexPath = self.selectIndexPath self.present(navigationController, animated: true, completion: nil) } @@ -120,12 +127,12 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_by_modified_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), selected: NCKeychain().mediaSortDate == "date", on: true, action: { _ in NCKeychain().mediaSortDate = "date" - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -133,12 +140,12 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_by_created_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), selected: NCKeychain().mediaSortDate == "creationDate", on: true, action: { _ in NCKeychain().mediaSortDate = "creationDate" - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -146,12 +153,12 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_media_by_upload_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), selected: NCKeychain().mediaSortDate == "uploadDate", on: true, action: { _ in NCKeychain().mediaSortDate = "uploadDate" - self.reloadDataSource() + self.loadDataSource() } ) ) @@ -164,39 +171,45 @@ extension NCMedia { actions.append( NCMenuAction( title: NSLocalizedString("_cancel_", comment: ""), - icon: utility.loadImage(named: "xmark", color: NCBrandColor.shared.iconColor), + icon: utility.loadImage(named: "xmark", colors: [NCBrandColor.shared.iconColor]), action: { _ in self.tapSelect() } ) ) - guard !selectOcId.isEmpty else { return } - let selectedMetadatas = selectOcId.compactMap(NCManageDatabase.shared.getMetadataFromOcId) + if fileSelect.count != dataSource.metadatas.count { + actions.append(.selectAllAction(action: selectAll)) + } + guard !fileSelect.isEmpty else { return } + + actions.append(.seperator(order: 0)) + + let selectedMetadatas = fileSelect.compactMap(NCManageDatabase.shared.getMetadataFromOcId) // // OPEN IN // - actions.append(.openInAction(selectedMetadatas: selectedMetadatas, viewController: self, completion: tapSelect)) + actions.append(.openInAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) // // SAVE TO PHOTO GALLERY // - actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMetadatas, completion: tapSelect)) + actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) // // COPY - MOVE // - actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, indexPath: selectIndexPath, completion: tapSelect)) + actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) // // COPY // - actions.append(.copyAction(selectOcId: selectOcId, completion: tapSelect)) + actions.append(.copyAction(fileSelect: fileSelect, controller: self.controller, completion: tapSelect)) // // DELETE // can't delete from cache because is needed for NCMedia view, and if locked can't delete from server either. - if !selectedMetadatas.contains(where: { $0.lock && $0.lockOwner != appDelegate.userId }) { - actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, indexPath: selectIndexPath, metadataFolder: nil, viewController: self, completion: tapSelect)) + if !selectedMetadatas.contains(where: { $0.lock && $0.lockOwner != session.userId }) { + actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) } } } diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 496d15bc2d..2cf28132c6 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -44,7 +44,14 @@ final class NCImageCache: @unchecked Sendable { public var isLoadingCache: Bool = false var isDidEnterBackground: Bool = false - init() { + var createMediaCacheInProgress: Bool = false + let showAllPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload')" + let showBothPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" + let showOnlyPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND classFile == %@ AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')" + + override init() { + super.init() + NotificationCenter.default.addObserver(forName: LRUCacheMemoryWarningNotification, object: nil, queue: nil) { _ in self.cache.removeAllValues() self.cache = LRUCache(countLimit: self.countLimit) @@ -126,6 +133,9 @@ final class NCImageCache: @unchecked Sendable { } func getMediaMetadatas(account: String, predicate: NSPredicate? = nil) -> ThreadSafeArray? { + guard let tableAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", account)) else { return nil } + let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: tableAccount.urlBase, userId: tableAccount.userId) + tableAccount.mediaPath + let predicateBoth = NSPredicate(format: showBothPredicateMediaString, account, startServerUrl) return NCManageDatabase.shared.getMediaMetadatas(predicate: predicate ?? predicateBoth, sorted: "date") } diff --git a/iOSClient/Settings/NCKeychain.swift b/iOSClient/Settings/NCKeychain.swift index c3f8ab7405..b097f1e010 100644 --- a/iOSClient/Settings/NCKeychain.swift +++ b/iOSClient/Settings/NCKeychain.swift @@ -79,7 +79,7 @@ import KeychainAccess } } - var resetAppCounterFail: Bool { + @objc var resetAppCounterFail: Bool { get { if let value = try? keychain.get("resetAppCounterFail"), let result = Bool(value) { return result @@ -115,7 +115,7 @@ import KeychainAccess } } - var requestPasscodeAtStart: Bool { + @objc var requestPasscodeAtStart: Bool { get { let keychainOLD = Keychain(service: "Crypto Cloud") if let value = keychainOLD["notPasscodeAtStart"], !value.isEmpty { @@ -138,7 +138,7 @@ import KeychainAccess } } - var touchFaceID: Bool { + @objc var touchFaceID: Bool { get { migrate(key: "enableTouchFaceID") if let value = try? keychain.get("enableTouchFaceID"), let result = Bool(value) { @@ -155,7 +155,7 @@ import KeychainAccess return passcode != nil && requestPasscodeAtStart } - var incrementalNumber: String { + @objc var incrementalNumber: String { migrate(key: "incrementalnumber") var incrementalString = String(format: "%04ld", 0) if let value = try? keychain.get("incrementalnumber"), var result = Int(value) { @@ -166,7 +166,7 @@ import KeychainAccess return incrementalString } - var showHiddenFiles: Bool { + @objc var showHiddenFiles: Bool { get { migrate(key: "showHiddenFiles") if let value = try? keychain.get("showHiddenFiles"), let result = Bool(value) { @@ -179,7 +179,7 @@ import KeychainAccess } } - var formatCompatibility: Bool { + @objc var formatCompatibility: Bool { get { migrate(key: "formatCompatibility") if let value = try? keychain.get("formatCompatibility"), let result = Bool(value) { @@ -192,7 +192,7 @@ import KeychainAccess } } - var disableFilesApp: Bool { + @objc var disableFilesApp: Bool { get { migrate(key: "disablefilesapp") if let value = try? keychain.get("disablefilesapp"), let result = Bool(value) { @@ -205,7 +205,7 @@ import KeychainAccess } } - var livePhoto: Bool { + @objc var livePhoto: Bool { get { migrate(key: "livePhoto") if let value = try? keychain.get("livePhoto"), let result = Bool(value) { @@ -218,7 +218,7 @@ import KeychainAccess } } - var disableCrashservice: Bool { + @objc var disableCrashservice: Bool { get { migrate(key: "crashservice") if let value = try? keychain.get("crashservice"), let result = Bool(value) { @@ -231,7 +231,7 @@ import KeychainAccess } } - var logLevel: Int { + @objc var logLevel: Int { get { migrate(key: "logLevel") if let value = try? keychain.get("logLevel"), let result = Int(value) { @@ -244,7 +244,7 @@ import KeychainAccess } } - var accountRequest: Bool { + @objc var accountRequest: Bool { get { migrate(key: "accountRequest") if let value = try? keychain.get("accountRequest"), let result = Bool(value) { @@ -257,7 +257,7 @@ import KeychainAccess } } - var removePhotoCameraRoll: Bool { + @objc var removePhotoCameraRoll: Bool { get { migrate(key: "removePhotoCameraRoll") if let value = try? keychain.get("removePhotoCameraRoll"), let result = Bool(value) { @@ -270,7 +270,7 @@ import KeychainAccess } } - var privacyScreenEnabled: Bool { + @objc var privacyScreenEnabled: Bool { get { migrate(key: "privacyScreen") if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { @@ -283,7 +283,7 @@ import KeychainAccess } } - var cleanUpDay: Int { + @objc var cleanUpDay: Int { get { migrate(key: "cleanUpDay") if let value = try? keychain.get("cleanUpDay"), let result = Int(value) { @@ -508,6 +508,54 @@ import KeychainAccess } */ + func setTitleButtonHeader(account: String, value: String?) { + let key = "titleButtonHeader" + account + keychain[key] = value + } + + func getTitleButtonHeader(account: String) -> String? { + let key = "titleButtonHeader" + account + return (try? keychain.get(key)) ?? "" + } + + @objc func getOriginalFileName(key: String) -> Bool { + migrate(key: key) + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } + return false + } + + @objc func setOriginalFileName(key: String, value: Bool) { + keychain[key] = String(value) + } + + @objc func getFileNameMask(key: String) -> String { + migrate(key: key) + if let value = try? keychain.get(key) { + return value + } else { + return "" + } + } + + @objc func setFileNameMask(key: String, mask: String?) { + keychain[key] = mask + } + + @objc func getFileNameType(key: String) -> Bool { + migrate(key: key) + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return false + } + } + + @objc func setFileNameType(key: String, prefix: Bool) { + keychain[key] = String(prefix) + } + // MARK: - E2EE func getEndToEndCertificate(account: String) -> String? { @@ -573,7 +621,7 @@ import KeychainAccess // MARK: - PUSHNOTIFICATION - func getPushNotificationPublicKey(account: String) -> Data? { + @objc func getPushNotificationPublicKey(account: String) -> Data? { let key = "PNPublicKey" + account return try? keychain.getData(key) } @@ -583,7 +631,7 @@ import KeychainAccess keychain[data: key] = data } - func getPushNotificationPrivateKey(account: String) -> Data? { + @objc func getPushNotificationPrivateKey(account: String) -> Data? { let key = "PNPrivateKey" + account return try? keychain.getData(key) } @@ -593,47 +641,47 @@ import KeychainAccess keychain[data: key] = data } - func getPushNotificationSubscribingPublicKey(account: String) -> String? { + @objc func getPushNotificationSubscribingPublicKey(account: String) -> String? { let key = "PNSubscribingPublicKey" + account return try? keychain.get(key) } - func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { + @objc func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { let key = "PNSubscribingPublicKey" + account keychain[key] = publicKey } - func getPushNotificationToken(account: String) -> String? { + @objc func getPushNotificationToken(account: String) -> String? { let key = "PNToken" + account return try? keychain.get(key) } - func setPushNotificationToken(account: String, token: String?) { + @objc func setPushNotificationToken(account: String, token: String?) { let key = "PNToken" + account keychain[key] = token } - func getPushNotificationDeviceIdentifier(account: String) -> String? { + @objc func getPushNotificationDeviceIdentifier(account: String) -> String? { let key = "PNDeviceIdentifier" + account return try? keychain.get(key) } - func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { + @objc func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { let key = "PNDeviceIdentifier" + account keychain[key] = deviceIdentifier } - func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { + @objc func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { let key = "PNDeviceIdentifierSignature" + account return try? keychain.get(key) } - func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { + @objc func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { let key = "PNDeviceIdentifierSignature" + account keychain[key] = deviceIdentifierSignature } - func clearAllKeysPushNotification(account: String) { + @objc func clearAllKeysPushNotification(account: String) { setPushNotificationPublicKey(account: account, data: nil) setPushNotificationSubscribingPublicKey(account: account, publicKey: nil) setPushNotificationPrivateKey(account: account, data: nil) @@ -684,7 +732,7 @@ import KeychainAccess } } - func removeAll() { + @objc func removeAll() { try? keychain.removeAll() } }