From ac481a6aea064e30de724be708164d33f60b8cde 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/9] NMC 2169 - Media theming customisation changes --- iOSClient/AppDelegate.swift | 3 + .../Data/NCManageDatabase+Metadata.swift | 49 +++-- iOSClient/Media/NCMedia.storyboard | 74 +++++-- iOSClient/Media/NCMediaCommandView.xib | 178 +++++++++++++++ iOSClient/Menu/NCMedia+Menu.swift | 203 ++++++++++++++++++ iOSClient/NCImageCache.swift | 13 ++ iOSClient/Settings/NCPreferences.swift | 37 ++++ .../NCViewerMediaDetailView.swift | 1 + .../NCViewerQuickLook/NCViewerQuickLook.swift | 2 +- 9 files changed, 525 insertions(+), 35 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 133431af35..8b1e04cf8c 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -37,6 +37,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD await 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 2892c7cae0..b46d5d386c 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -1302,17 +1302,41 @@ extension NCManageDatabase { } ?? false } - func getMetadataDirectoryAsync(serverUrl: String, account: String) async -> tableMetadata? { - guard let url = URL(string: serverUrl) else { - return nil - } - let fileName = url.lastPathComponent - var baseUrl = url.deletingLastPathComponent().absoluteString - if baseUrl.hasSuffix("/") { - baseUrl.removeLast() - } - guard let decodedBaseUrl = baseUrl.removingPercentEncoding else { - return nil + func createMetadatasFolder(assets: [PHAsset], + useSubFolder: Bool, + session: NCSession.Session, completion: @escaping ([tableMetadata]) -> Void) { + var foldersCreated: Set = [] + var metadatas: [tableMetadata] = [] + let serverUrlBase = getAccountAutoUploadDirectory(session: session) + let fileNameBase = getAccountAutoUploadFileName(account: session.account) + let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, serverUrlBase) + + func createMetadata(serverUrl: String, fileName: String, metadata: tableMetadata?) { + guard !foldersCreated.contains(serverUrl + "/" + fileName) else { + return + } + foldersCreated.insert(serverUrl + "/" + fileName) + + if let metadata { + metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder + metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload + metadata.sessionDate = Date() + metadatas.append(tableMetadata(value: metadata)) + } else { + let metadata = NCManageDatabase.shared.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: NSUUID().uuidString, + serverUrl: serverUrl, + url: "", + contentType: "httpd/unix-directory", + directory: true, + session: session, + sceneIdentifier: nil) + metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder + metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload + metadata.sessionDate = Date() + metadatas.append(metadata) + } } return await core.performRealmReadAsync { realm in @@ -1322,7 +1346,7 @@ extension NCManageDatabase { return object?.detachedCopy() } } - + func getMetadataDirectory(serverUrl: String, account: String) -> tableMetadata? { guard let url = URL(string: serverUrl) else { return nil @@ -1342,6 +1366,7 @@ extension NCManageDatabase { .first return object?.detachedCopy() } + return nil } func getTransferAsync(tranfersSuccess: [tableMetadata]) async -> [tableMetadata] { diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index fcdb7e83f1..517f2fdfb8 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -1,10 +1,10 @@ - + - + + - @@ -18,7 +18,7 @@ - + @@ -31,43 +31,78 @@ - + - + + + + - - - - + + + + + + + + + - - - + + + + - + + + + @@ -76,9 +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 eb40357d0f..b8aaeb9a22 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -93,6 +93,19 @@ final class NCImageCache: @unchecked Sendable { } } + 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/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 387fb10bfd..8a133deeb9 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -225,6 +225,43 @@ final class NCPreferences: NSObject { } } + 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 { return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 0cf1201651..9c2c44cc3b 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -188,6 +188,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 36dab8777f..97d333624c 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -168,7 +168,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 470501213d389f353c060a141fa7275bd228a6b8 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Mon, 14 Apr 2025 11:39:24 +0530 Subject: [PATCH 2/9] NMC 2169 - Media theming changes --- Nextcloud.xcodeproj/project.pbxproj | 23 + .../Data/NCManageDatabase+Metadata.swift | 130 ++++- iOSClient/Menu/NCMedia+Menu.swift | 65 ++- iOSClient/NCImageCache.swift | 8 + iOSClient/Settings/NCPreferences.swift | 507 +++++++++++------- 5 files changed, 503 insertions(+), 230 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index b24d42ac75..4271eeae82 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -90,6 +90,11 @@ AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; }; AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.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 */; }; F31165022F9674A1009A1E37 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = F31165012F9674A1009A1E37 /* AppIcon.icon */; }; @@ -1280,6 +1285,9 @@ AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; B4C7A5B36D1ED178FB6B76CB /* NCContextMenuPlayerTracks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuPlayerTracks.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.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 = ""; }; @@ -2074,6 +2082,18 @@ AF93471127E2341B002537EE /* NCContextMenuShare.swift */, F75D19E225EFE09000D74598 /* NCContextMenuTrash.swift */, F710D2012405826100A6033D /* NCContextMenuViewer.swift */, + F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */, + F78C6FDD296D677300C952C3 /* NCContextMenu.swift */, + B5C980192DACD56C0041B146 /* NCMedia+Menu.swift */, + 3704EB2923D5A58400455C5B /* NCMenu.storyboard */, + 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */, + AF935066276B84E700BD078F /* NCMenu+FloatingPanel.swift */, + AF68326927BE65A90010BF0B /* NCMenuAction.swift */, + F7FAFD3928BFA947000777FE /* NCNotification+Menu.swift */, + AF93471127E2341B002537EE /* NCShare+Menu.swift */, + F75D19E225EFE09000D74598 /* NCTrash+Menu.swift */, + F710D2012405826100A6033D /* NCViewerContextMenu.swift */, + 8491B1CC273BBA82001C8C5B /* UIViewController+Menu.swift */, ); path = Menu; sourceTree = ""; @@ -3246,6 +3266,7 @@ F7EC9CB921185F2000F1C5CE /* Media */ = { isa = PBXGroup; children = ( + B5C980172DACD51C0041B146 /* NCMediaCommandView.xib */, F720B5B72507B9A5008C94E5 /* Cell */, F7501C302212E57400FB1415 /* NCMedia.storyboard */, F7501C312212E57400FB1415 /* NCMedia.swift */, @@ -4079,6 +4100,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 */, F76D3CF32428B94E005DFA87 /* NCViewerPDFSearchCell.xib in Resources */, @@ -4914,6 +4936,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 */, F3DDFE1E2F1F8EC600A784C8 /* ChatInputField.swift in Sources */, diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index b46d5d386c..2737cb1ebb 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,9 +151,36 @@ extension tableMetadata { (fileNameView as NSString).deletingPathExtension } + var isRenameable: Bool { + if !NCMetadataPermissions.canRename(self) { + return false + } + if lock { + return false + } + if !isDirectoryE2EE && e2eEncrypted { + return false + } + 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 == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKCommon.TypeClassFile.document.rawValue + } var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue @@ -180,7 +207,7 @@ extension tableMetadata { } var isCopyableInPasteboard: Bool { - !directory + !isDocumentViewableOnly && !directory } #if !EXTENSION_FILE_PROVIDER_EXTENSION @@ -189,11 +216,11 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDirectoryE2EE && !e2eEncrypted + !isDocumentViewableOnly && !isDirectoryE2EE && !e2eEncrypted } var isModifiableWithQuickLook: Bool { - if directory || isDirectoryE2EE { + if directory || isDocumentViewableOnly || isDirectoryE2EE { return false } return isPDF || isImage @@ -224,17 +251,62 @@ extension tableMetadata { } var canSetAsAvailableOffline: Bool { - return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted +// return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted + return session.isEmpty && !isDocumentViewableOnly } func isSharable() -> Bool { guard let capabilities = NCNetworking.shared.capabilities[account] else { return false } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE) { - return false - } - return true + 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 + } + + var isUpload: Bool { + status == NCGlobal.shared.metadataStatusWaitUpload || status == NCGlobal.shared.metadataStatusUploading + } + + var isDirectory: Bool { + directory + } + + @objc var isDirectoryE2EE: Bool { + return NCUtilityFileSystem().isDirectoryE2EE(serverUrl: serverUrl, urlBase: urlBase, userId: userId, account: account) + } + + var isLivePhoto: Bool { + !livePhotoFile.isEmpty + } + + var isNotFlaggedAsLivePhotoByServer: Bool { + !isFlaggedAsLivePhotoByServer + } + + var imageSize: CGSize { + CGSize(width: width, height: height) } var hasPreviewBorder: Bool { @@ -343,6 +415,17 @@ extension tableMetadata { return !lock || (lockOwner == user && lockOwnerType == 0) } + // Return if is sharable + func isSharable() -> Bool { + guard let capabilities = NCNetworking.shared.capabilities[account] else { + return false + } + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE) { + return false + } + return !e2eEncrypted + } + /// Returns a detached (unmanaged) deep copy of the current `tableMetadata` object. /// /// - Note: Primitive properties and lists of primitive values (for example `shareType`) @@ -1194,6 +1277,37 @@ extension NCManageDatabase { } ?? [] } + func getTableMetadatasDirectoryFavoriteIdentifierRankAsync(account: String) async -> [String: NSNumber] { + let result = await performRealmReadAsync { realm in + var listIdentifierRank: [String: NSNumber] = [:] + var counter = Int64(10) + + let results = realm.objects(tableMetadata.self) + .filter("account == %@ AND directory == true AND favorite == true", account) + .sorted(byKeyPath: "fileNameView", ascending: true) + + results.forEach { item in + counter += 1 + listIdentifierRank[item.ocId] = NSNumber(value: counter) + } + + return listIdentifierRank + } + return result ?? [:] + } + + @objc func clearMetadatasUpload(account: String) { + do { + let realm = try Realm() + try realm.write { + let results = realm.objects(tableMetadata.self).filter("account == %@ AND (status == %d OR status == %d)", account, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploadError) + realm.delete(results) + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + func getAssetLocalIdentifiersUploadedAsync() async -> [String]? { return await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") 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 b8aaeb9a22..f8859ca1da 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -29,6 +29,11 @@ final class NCImageCache: @unchecked Sendable { public var isLoadingCache: Bool = false public var controller: UITabBarController? + 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)')" + init() { observerToken = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: nil) { _ in self.cache.removeAll() @@ -103,6 +108,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/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 8a133deeb9..9c6488b1c6 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -1,44 +1,72 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2023 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCKeychain.swift +// Nextcloud +// +// Created by Marino Faggiana on 23/10/23. +// Copyright © 2023 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 UIKit import KeychainAccess import NextcloudKit -final class NCPreferences: NSObject { +@objc class NCKeychain: NSObject { let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { get { - return getBoolPreference(key: "showDescription", defaultValue: true) + if let value = try? keychain.get("showDescription"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "showDescription") + keychain["showDescription"] = String(newValue) } } var showRecommendedFiles: Bool { get { - return getBoolPreference(key: "showRecommendedFiles", defaultValue: true) + if let value = try? keychain.get("showRecommendedFiles"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "showRecommendedFiles") + keychain["showRecommendedFiles"] = String(newValue) } } var typeFilterScanDocument: NCGlobal.TypeFilterScanDocument { get { - let rawValue = getStringPreference(key: "ScanDocumentTypeFilter", defaultValue: NCGlobal.TypeFilterScanDocument.original.rawValue) - return NCGlobal.TypeFilterScanDocument(rawValue: rawValue) ?? .original + if let rawValue = try? keychain.get("ScanDocumentTypeFilter"), let value = NCGlobal.TypeFilterScanDocument(rawValue: rawValue) { + return value + } else { + return .original + } } set { - setUserDefaults(newValue.rawValue, forKey: "ScanDocumentTypeFilter") + keychain["ScanDocumentTypeFilter"] = newValue.rawValue } } - var passcode: String? { + @objc var passcode: String? { get { migrate(key: "passcodeBlock") if let value = try? keychain.get("passcodeBlock"), !value.isEmpty { @@ -51,7 +79,7 @@ final class NCPreferences: NSObject { } } - var resetAppCounterFail: Bool { + @objc var resetAppCounterFail: Bool { get { if let value = try? keychain.get("resetAppCounterFail"), let result = Bool(value) { return result @@ -87,7 +115,7 @@ final class NCPreferences: NSObject { } } - var requestPasscodeAtStart: Bool { + @objc var requestPasscodeAtStart: Bool { get { let keychainOLD = Keychain(service: "Crypto Cloud") if let value = keychainOLD["notPasscodeAtStart"], !value.isEmpty { @@ -110,7 +138,7 @@ final class NCPreferences: NSObject { } } - var touchFaceID: Bool { + @objc var touchFaceID: Bool { get { migrate(key: "enableTouchFaceID") if let value = try? keychain.get("enableTouchFaceID"), let result = Bool(value) { @@ -127,101 +155,151 @@ final class NCPreferences: NSObject { return passcode != nil && requestPasscodeAtStart } - var incrementalNumber: String { + @objc var incrementalNumber: String { + migrate(key: "incrementalnumber") var incrementalString = String(format: "%04ld", 0) - let value = getStringPreference(key: "incrementalnumber", defaultValue: incrementalString) - if var intValue = Int(value) { - intValue += 1 - incrementalString = String(format: "%04ld", intValue) + if let value = try? keychain.get("incrementalnumber"), var result = Int(value) { + result += 1 + incrementalString = String(format: "%04ld", result) } - setUserDefaults(incrementalString, forKey: "incrementalnumber") + keychain["incrementalnumber"] = incrementalString return incrementalString } - var formatCompatibility: Bool { + @objc var showHiddenFiles: Bool { get { - return getBoolPreference(key: "formatCompatibility", defaultValue: true) + migrate(key: "showHiddenFiles") + if let value = try? keychain.get("showHiddenFiles"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "formatCompatibility") + keychain["showHiddenFiles"] = String(newValue) } } - var disableFilesApp: Bool { + @objc var formatCompatibility: Bool { get { - return getBoolPreference(key: "disablefilesapp", defaultValue: false) + migrate(key: "formatCompatibility") + if let value = try? keychain.get("formatCompatibility"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "disablefilesapp") + keychain["formatCompatibility"] = String(newValue) } } - var livePhoto: Bool { + @objc var disableFilesApp: Bool { get { - return getBoolPreference(key: "livePhoto", defaultValue: true) + migrate(key: "disablefilesapp") + if let value = try? keychain.get("disablefilesapp"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "livePhoto") + keychain["disablefilesapp"] = String(newValue) } } - var disableCrashservice: Bool { + @objc var livePhoto: Bool { get { - return getBoolPreference(key: "crashservice", defaultValue: false) + migrate(key: "livePhoto") + if let value = try? keychain.get("livePhoto"), let result = Bool(value) { + return result + } + return true + } + set { + keychain["livePhoto"] = String(newValue) + } + } + + @objc var disableCrashservice: Bool { + get { + migrate(key: "crashservice") + if let value = try? keychain.get("crashservice"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "crashservice") + keychain["crashservice"] = String(newValue) } } /// Stores and retrieves the current log level from the keychain. var log: NKLogLevel { +// @objc var logLevel: Int { get { - let value = getIntPreference(key: "logLevel", defaultValue: NKLogLevel.normal.rawValue) - return NKLogLevel(rawValue: value) ?? NKLogLevel.normal + migrate(key: "logLevel") + if let value = try? keychain.get("logLevel"), + let intValue = Int(value), + let level = NKLogLevel(rawValue: intValue) { + return level + } + return NKLogLevel.normal } set { - setUserDefaults(newValue.rawValue, forKey: "logLevel") + keychain["logLevel"] = String(newValue.rawValue) } } - var accountRequest: Bool { + @objc var accountRequest: Bool { get { - return getBoolPreference(key: "accountRequest", defaultValue: false) + migrate(key: "accountRequest") + if let value = try? keychain.get("accountRequest"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "accountRequest") + keychain["accountRequest"] = String(newValue) } } - var removePhotoCameraRoll: Bool { + @objc var removePhotoCameraRoll: Bool { get { - return getBoolPreference(key: "removePhotoCameraRoll", defaultValue: false) + migrate(key: "removePhotoCameraRoll") + if let value = try? keychain.get("removePhotoCameraRoll"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "removePhotoCameraRoll") + keychain["removePhotoCameraRoll"] = String(newValue) } } - var privacyScreenEnabled: Bool { + @objc var privacyScreenEnabled: Bool { get { + migrate(key: "privacyScreen") if NCBrandOptions.shared.enforce_privacyScreenEnabled { return true } - return getBoolPreference(key: "privacyScreen", defaultValue: false) + if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "privacyScreen") + keychain["privacyScreen"] = String(newValue) } } - var cleanUpDay: Int { + @objc var cleanUpDay: Int { get { - let value = getIntPreference(key: "cleanUpDay", defaultValue: NCBrandOptions.shared.cleanUpDay) - return value + migrate(key: "cleanUpDay") + if let value = try? keychain.get("cleanUpDay"), let result = Int(value) { + return result + } + return NCBrandOptions.shared.cleanUpDay } set { - setUserDefaults(newValue, forKey: "cleanUpDay") + keychain["cleanUpDay"] = String(newValue) } } @@ -264,124 +342,144 @@ final class NCPreferences: NSObject { var textRecognitionStatus: Bool { get { - return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) + migrate(key: "textRecognitionStatus") + if let value = try? keychain.get("textRecognitionStatus"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "textRecognitionStatus") + keychain["textRecognitionStatus"] = String(newValue) } } var deleteAllScanImages: Bool { get { - return getBoolPreference(key: "deleteAllScanImages", defaultValue: false) + migrate(key: "deleteAllScanImages") + if let value = try? keychain.get("deleteAllScanImages"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "deleteAllScanImages") + keychain["deleteAllScanImages"] = String(newValue) } } var qualityScanDocument: Double { get { - let value = getIntPreference(key: "qualityScanDocument", defaultValue: 2) - return Double(value) + migrate(key: "qualityScanDocument") + if let value = try? keychain.get("qualityScanDocument"), let result = Double(value) { + return result + } + return 2 } set { - setUserDefaults(newValue, forKey: "qualityScanDocument") + keychain["qualityScanDocument"] = String(newValue) } } var appearanceAutomatic: Bool { get { - let value = getBoolPreference(key: "appearanceAutomatic", defaultValue: true) - return value + if let value = try? keychain.get("appearanceAutomatic"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "appearanceAutomatic") + keychain["appearanceAutomatic"] = String(newValue) } } var appearanceInterfaceStyle: UIUserInterfaceStyle { get { - let value = getStringPreference(key: "appearanceInterfaceStyle", defaultValue: "light") - if value == "light" { - return .light - } else { - return .dark + if let value = try? keychain.get("appearanceInterfaceStyle") { + if value == "light" { + return .light + } else { + return .dark + } } + return .light } set { if newValue == .light { - setUserDefaults("light", forKey: "appearanceInterfaceStyle") + keychain["appearanceInterfaceStyle"] = "light" } else { - setUserDefaults("dark", forKey: "appearanceInterfaceStyle") + keychain["appearanceInterfaceStyle"] = "dark" } } } var screenAwakeMode: AwakeMode { get { - let value = getStringPreference(key: "screenAwakeMode", defaultValue: "off") - if value == "off" { - return .off - } else if value == "on" { - return .on - } else { - return .whileCharging + if let value = try? keychain.get("screenAwakeMode") { + if value == "off" { + return .off + } else if value == "on" { + return .on + } else { + return .whileCharging + } } + return .off } set { if newValue == .off { - setUserDefaults("off", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "off" } else if newValue == .on { - setUserDefaults("on", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "on" } else { - setUserDefaults("whileCharging", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "whileCharging" } } } var fileNameType: Bool { get { - return getBoolPreference(key: "fileNameType", defaultValue: false) + if let value = try? keychain.get("fileNameType"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "fileNameType") + keychain["fileNameType"] = String(newValue) } } var fileNameOriginal: Bool { get { - return getBoolPreference(key: "fileNameOriginal", defaultValue: false) + if let value = try? keychain.get("fileNameOriginal"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "fileNameOriginal") + keychain["fileNameOriginal"] = String(newValue) } } var fileNameMask: String { get { - return getStringPreference(key: "fileNameMask", defaultValue: "") + if let value = try? keychain.get("fileNameMask") { + return value + } + return "" } set { - setUserDefaults(newValue, forKey: "fileNameMask") + keychain["fileNameMask"] = String(newValue) } } var location: Bool { get { - return getBoolPreference(key: "location", defaultValue: false) - } - set { - setUserDefaults(newValue, forKey: "location") - } - } - - var deviceTokenPushNotification: String { - get { - return getStringPreference(key: "deviceTokenPushNotification", defaultValue: "") + if let value = try? keychain.get("location"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "deviceTokenPushNotification") + keychain["location"] = String(newValue) } } @@ -400,41 +498,109 @@ final class NCPreferences: NSObject { } func setPersonalFilesOnly(account: String, value: Bool) { - let userDefaultsKey = "personalfilesonly" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "personalFilesOnly" + account + keychain[key] = String(value) } func getPersonalFilesOnly(account: String) -> Bool { - return getBoolPreference(key: "personalfilesonly", account: account, defaultValue: false) + let key = "personalFilesOnly" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return false + } } func setFavoriteOnTop(account: String, value: Bool) { - let userDefaultsKey = "favoriteOnTop" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "favoriteOnTop" + account + keychain[key] = String(value) } func getFavoriteOnTop(account: String) -> Bool { - return getBoolPreference(key: "favoriteOnTop", account: account, defaultValue: true) + let key = "favoriteOnTop" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return true + } } func setDirectoryOnTop(account: String, value: Bool) { - let userDefaultsKey = "directoryOnTop" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "directoryOnTop" + account + keychain[key] = String(value) } func getDirectoryOnTop(account: String) -> Bool { - return getBoolPreference(key: "directoryOnTop", account: account, defaultValue: true) + let key = "directoryOnTop" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return true + } } func setShowHiddenFiles(account: String, value: Bool) { - let userDefaultsKey = "showHiddenFiles" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "showHiddenFiles" + account + keychain[key] = String(value) } func getShowHiddenFiles(account: String) -> Bool { - return getBoolPreference(key: "showHiddenFiles", account: account, defaultValue: false) + let key = "showHiddenFiles" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return false + } } + 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? { @@ -482,6 +648,7 @@ final class NCPreferences: NSObject { } func isEndToEndEnabled(account: String) -> Bool { + let capabilities = NKCapabilities.shared.getCapabilitiesBlocking(for: account) guard let certificate = getEndToEndCertificate(account: account), !certificate.isEmpty, let publicKey = getEndToEndPublicKey(account: account), !publicKey.isEmpty, let privateKey = getEndToEndPrivateKey(account: account), !privateKey.isEmpty, @@ -498,62 +665,73 @@ final class NCPreferences: NSObject { setEndToEndPassphrase(account: account, passphrase: nil) } - // MARK: - PUSH NOTIFICATION + // MARK: - PUSHNOTIFICATION - func getPushNotificationPrivateKey(account: String) -> Data? { - let key = "PushPrivateKey" + account + @objc func getPushNotificationPublicKey(account: String) -> Data? { + let key = "PNPublicKey" + account return try? keychain.getData(key) } - func setPushNotificationPrivateKey(account: String, data: Data?) { - let key = "PushPrivateKey" + account + @objc func setPushNotificationPublicKey(account: String, data: Data?) { + let key = "PNPublicKey" + account keychain[data: key] = data } - func getPushNotificationPublicKey(account: String) -> Data? { - let key = "PushPublicKey" + account + @objc func getPushNotificationPrivateKey(account: String) -> Data? { + let key = "PNPrivateKey" + account return try? keychain.getData(key) } - func setPushNotificationPublicKey(account: String, data: Data?) { - let key = "PushPublicKey" + account + @objc func setPushNotificationPrivateKey(account: String, data: Data?) { + let key = "PNPrivateKey" + account keychain[data: key] = data } - func getPushNotificationSubscribingPublicKey(account: String) -> String? { - let key = "PushSubscribingPublicKey" + account + @objc func getPushNotificationSubscribingPublicKey(account: String) -> String? { + let key = "PNSubscribingPublicKey" + account return try? keychain.get(key) } - func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { - let key = "PushSubscribingPublicKey" + account + @objc func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { + let key = "PNSubscribingPublicKey" + account keychain[key] = publicKey } - func getPushNotificationDeviceIdentifier(account: String) -> String? { - let value = getStringPreference(key: "PushDeviceIdentifier", account: account, defaultValue: "") - return value + @objc func getPushNotificationToken(account: String) -> String? { + let key = "PNToken" + account + return try? keychain.get(key) + } + + @objc func setPushNotificationToken(account: String, token: String?) { + let key = "PNToken" + account + keychain[key] = token + } + + @objc func getPushNotificationDeviceIdentifier(account: String) -> String? { + let key = "PNDeviceIdentifier" + account + return try? keychain.get(key) } - func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { - let userDefaultsKey = "PushDeviceIdentifier" + "_\(account)" - setUserDefaults(deviceIdentifier, forKey: userDefaultsKey) + @objc func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { + let key = "PNDeviceIdentifier" + account + keychain[key] = deviceIdentifier } - func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { - let key = "PushDeviceIdentifierSignature" + account + @objc func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { + let key = "PNDeviceIdentifierSignature" + account return try? keychain.get(key) } - func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { - let key = "PushDeviceIdentifierSignature" + account + @objc func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { + let key = "PNDeviceIdentifierSignature" + account keychain[key] = deviceIdentifierSignature } - func clearAllKeysPushNotification(account: String) { - setPushNotificationPrivateKey(account: account, data: nil) + @objc func clearAllKeysPushNotification(account: String) { setPushNotificationPublicKey(account: account, data: nil) setPushNotificationSubscribingPublicKey(account: account, publicKey: nil) + setPushNotificationPrivateKey(account: account, data: nil) + setPushNotificationToken(account: account, token: nil) setPushNotificationDeviceIdentifier(account: account, deviceIdentifier: nil) setPushNotificationDeviceIdentifierSignature(account: account, deviceIdentifierSignature: nil) } @@ -581,9 +759,8 @@ final class NCPreferences: NSObject { // MARK: - Albums func setAutoUploadAlbumIds(account: String, albumIds: [String]) { - let userDefaultsKey = "AlbumIds" + "_\(account)" - let value = albumIds.joined(separator: ",") - setUserDefaults(value, forKey: userDefaultsKey) + let key = "AlbumIds" + account + keychain[key] = albumIds.joined(separator: ",") } func getAutoUploadAlbumIds(account: String) -> [String] { @@ -641,69 +818,7 @@ final class NCPreferences: NSObject { } } - func removeAll() { + @objc func removeAll() { try? keychain.removeAll() } - - private func setUserDefaults(_ value: Any?, forKey key: String) { - let keyPreferences = "Preferences_\(key)" - UserDefaults.standard.set(value, forKey: keyPreferences) - } - - private func getBoolPreference(key: String, account: String? = nil, defaultValue: Bool) -> Bool { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Bool { - return value - } - - if let value = try? keychain.get(keychainKey), let boolValue = Bool(value) { - UserDefaults.standard.set(boolValue, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return boolValue - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } - - private func getStringPreference(key: String, account: String? = nil, defaultValue: String) -> String { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? String { - return value - } - - if let value = try? keychain.get(keychainKey) { - UserDefaults.standard.set(value, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return value - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } - - private func getIntPreference(key: String, account: String? = nil, defaultValue: Int) -> Int { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Int { - return value - } - - if let value = try? keychain.get(keychainKey), let intValue = Int(value) { - UserDefaults.standard.set(intValue, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return intValue - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } } From 0ae6fac4125fdfa135c12717903c177eda9d33b1 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Mon, 6 Oct 2025 11:07:18 +0530 Subject: [PATCH 3/9] NMC 2169 - Media theming customisation changes --- iOSClient/Menu/NCMedia+Menu.swift | 1 + iOSClient/Settings/NCPreferences.swift | 83 +++++-------------- .../NCViewerMediaDetailView.swift | 1 + 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/iOSClient/Menu/NCMedia+Menu.swift b/iOSClient/Menu/NCMedia+Menu.swift index 72c6cec47b..4951c4711a 100644 --- a/iOSClient/Menu/NCMedia+Menu.swift +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -55,6 +55,7 @@ extension NCMedia { icon: utility.loadImage(named: "selectFull", colors: [NCBrandColor.shared.iconColor]), action: { _ in self.isEditMode = true + self.collectionView.reloadData() } ) ) diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 9c6488b1c6..4e08a797bc 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -24,9 +24,9 @@ import Foundation import UIKit import KeychainAccess -import NextcloudKit @objc class NCKeychain: NSObject { + let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { @@ -231,20 +231,16 @@ import NextcloudKit } } - /// Stores and retrieves the current log level from the keychain. - var log: NKLogLevel { -// @objc var logLevel: Int { + @objc var logLevel: Int { get { migrate(key: "logLevel") - if let value = try? keychain.get("logLevel"), - let intValue = Int(value), - let level = NKLogLevel(rawValue: intValue) { - return level + if let value = try? keychain.get("logLevel"), let result = Int(value) { + return result } - return NKLogLevel.normal + return 1 } set { - keychain["logLevel"] = String(newValue.rawValue) + keychain["logLevel"] = String(newValue) } } @@ -277,9 +273,6 @@ import NextcloudKit @objc var privacyScreenEnabled: Bool { get { migrate(key: "privacyScreen") - if NCBrandOptions.shared.enforce_privacyScreenEnabled { - return true - } if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { return result } @@ -471,25 +464,12 @@ import NextcloudKit } } - var location: Bool { - get { - if let value = try? keychain.get("location"), let result = Bool(value) { - return result - } - return false - } - set { - keychain["location"] = String(newValue) - } - } - // MARK: - - func getPassword(account: String) -> String { + @objc func getPassword(account: String) -> String { let key = "password" + account migrate(key: key) - let password = (try? keychain.get(key)) ?? "" - return password + return (try? keychain.get(key)) ?? "" } func setPassword(account: String, password: String?) { @@ -511,20 +491,7 @@ import NextcloudKit } } - func setFavoriteOnTop(account: String, value: Bool) { - let key = "favoriteOnTop" + account - keychain[key] = String(value) - } - - func getFavoriteOnTop(account: String) -> Bool { - let key = "favoriteOnTop" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return true - } - } - + /* OBSOLETE func setDirectoryOnTop(account: String, value: Bool) { let key = "directoryOnTop" + account keychain[key] = String(value) @@ -538,20 +505,7 @@ import NextcloudKit return true } } - - func setShowHiddenFiles(account: String, value: Bool) { - let key = "showHiddenFiles" + account - keychain[key] = String(value) - } - - func getShowHiddenFiles(account: String) -> Bool { - let key = "showHiddenFiles" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return false - } - } + */ func setTitleButtonHeader(account: String, value: String?) { let key = "titleButtonHeader" + account @@ -648,13 +602,12 @@ import NextcloudKit } func isEndToEndEnabled(account: String) -> Bool { - let capabilities = NKCapabilities.shared.getCapabilitiesBlocking(for: account) + let capabilities = NCCapabilities.shared.getCapabilities(account: account) guard let certificate = getEndToEndCertificate(account: account), !certificate.isEmpty, let publicKey = getEndToEndPublicKey(account: account), !publicKey.isEmpty, let privateKey = getEndToEndPrivateKey(account: account), !privateKey.isEmpty, - let passphrase = getEndToEndPassphrase(account: account), !passphrase.isEmpty else { - return false - } + let passphrase = getEndToEndPassphrase(account: account), !passphrase.isEmpty, + NCGlobal.shared.e2eeVersions.contains(capabilities.capabilityE2EEApiVersion) else { return false } return true } @@ -755,6 +708,16 @@ import NextcloudKit return (data, password) } + + @objc func setAccountName(account: String) { + let key = "AccountName" + keychain[key] = account + } + + @objc func getAccountName() -> String? { + let key = "AccountName" + return try? keychain.get(key) + } // MARK: - Albums diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 9c2c44cc3b..2615c5cad7 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -5,6 +5,7 @@ import UIKit import MapKit import NextcloudKit +import Alamofire public protocol NCViewerMediaDetailViewDelegate: AnyObject { func downloadFullResolution() From c44fcbfccea9ce0840d0d5b9bc426a1aeb774215 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 16 Dec 2025 17:23:54 +0530 Subject: [PATCH 4/9] NMC 2169 - Media theming customisation changes --- iOSClient/AppDelegate.swift | 381 +++++++++++- .../Data/NCManageDatabase+Metadata.swift | 109 ++-- iOSClient/Media/NCMedia.storyboard | 75 +-- iOSClient/Menu/NCMedia+Menu.swift | 340 +++++------ iOSClient/NCImageCache.swift | 84 ++- iOSClient/Settings/NCPreferences.swift | 545 ++++++++---------- .../NCViewerMediaDetailView.swift | 2 - .../NCViewerQuickLook/NCViewerQuickLook.swift | 29 +- 8 files changed, 919 insertions(+), 646 deletions(-) diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 8b1e04cf8c..8ceda5f386 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -16,6 +16,11 @@ import RealmSwift class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var backgroundSessionCompletionHandler: (() -> Void)? + var activeLogin: NCLogin? + var activeLoginWeb: NCLoginWeb? + var taskAutoUploadDate: Date = Date() + var orientationLock = UIInterfaceOrientationMask.all + @objc let adjust = AdjustHelper() var isUiTestingEnabled: Bool { return ProcessInfo.processInfo.arguments.contains("UI_TESTING") } @@ -30,16 +35,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var bgTask: UIBackgroundTaskIdentifier = .invalid var pushSubscriptionTask: Task? - + var window: UIWindow? + var sceneIdentifier: String = "" + var activeViewController: UIViewController? + var account: String = "" + var urlBase: String = "" + var user: String = "" + var userId: String = "" + var password: String = "" + var timerErrorNetworking: Timer? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if isUiTestingEnabled { Task { await NCAccount().deleteAllAccounts() } } - UINavigationBar.appearance().tintColor = NCBrandColor.shared.customer - UIToolbar.appearance().tintColor = NCBrandColor.shared.customer - let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() @@ -47,6 +58,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD utilityFileSystem.emptyTemporaryDirectory() utilityFileSystem.clearCacheDirectory("com.limit-point.LivePhoto") + UINavigationBar.appearance().tintColor = NCBrandColor.shared.brand + UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = NCBrandColor.shared.brand + let versionNextcloudiOS = String(format: NCBrandOptions.shared.textCopyrightNextcloudiOS, utility.getVersionBuild()) NCAppVersionManager.shared.checkAndUpdateInstallState() @@ -117,6 +131,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if NCBrandOptions.shared.enforce_passcode_lock { NCPreferences().requestPasscodeAtStart = true } + +// if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { +// for window in windowScene.windows { +// let imageViews = window.allImageViews() +// // Do something with the imageViews +// for imageView in imageViews { +// print("Found image view: \(imageView)") +// imageView.tintColor = UITraitCollection.current.userInterfaceStyle == .dark ? .white : .black +// } +// } +// } return true } @@ -148,6 +173,203 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } + // MARK: - Background Task + + /* + @discussion Schedule a refresh task request to ask that the system launch your app briefly so that you can download data and keep your app's contents up-to-date. The system will fulfill this request intelligently based on system conditions and app usage. + */ + func scheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: global.refreshTask) + + request.earliestBeginDate = Date(timeIntervalSinceNow: 60) // Refresh after 60 seconds. + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Refresh task failed to submit request: \(error)") + } + } + + /* + @discussion Schedule a processing task request to ask that the system launch your app when conditions are favorable for battery life to handle deferrable, longer-running processing, such as syncing, database maintenance, or similar tasks. The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week. + */ + func scheduleAppProcessing() { + let request = BGProcessingTaskRequest(identifier: global.processingTask) + + request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // Refresh after 5 minutes. + request.requiresNetworkConnectivity = false + request.requiresExternalPower = false + + do { + try BGTaskScheduler.shared.submit(request) + } catch { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Processing task failed to submit request: \(error)") + } + } + + func handleAppRefresh(_ task: BGAppRefreshTask) { + nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start refresh task") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") + task.setTaskCompleted(success: false) + return + } + + // Schedule next refresh + scheduleAppRefresh() + + Task { + defer { + task.setTaskCompleted(success: true) + } + + guard let tblAccount = await NCManageDatabase.shared.getActiveTableAccountAsync() else { + nkLog(tag: self.global.logTagTask, emoji: .info, message: "No active account or background task already running") + return + } + + await backgroundSync(tblAccount: tblAccount, task: task) + } + } + + func handleProcessingTask(_ task: BGProcessingTask) { + nkLog(tag: self.global.logTagTask, emoji: .start, message: "Start processing task") + guard NCManageDatabase.shared.openRealmBackground() else { + nkLog(tag: self.global.logTagTask, emoji: .error, message: "Failed to open Realm in background") + task.setTaskCompleted(success: false) + return + } + var expired = false + task.expirationHandler = { + expired = true + } + + // Schedule next processing task + scheduleAppProcessing() + + Task { + defer { + task.setTaskCompleted(success: true) + } + + // If possible, cleaning every week + if NCPreferences().cleaningWeek() { + // BGTask expiration flag + nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Start cleaning week") + let tblAccounts = await NCManageDatabase.shared.getAllTableAccountAsync() + for tblAccount in tblAccounts { + await NCManageDatabase.shared.cleanTablesOcIds(account: tblAccount.account, userId: tblAccount.userId, urlBase: tblAccount.urlBase) + guard !expired else { return } + } + await NCUtilityFileSystem().cleanUpAsync() + + NCPreferences().setDoneCleaningWeek() + nkLog(tag: self.global.logTagBgSync, emoji: .stop, message: "Stop cleaning week") + } else { + await backgroundSync(task: task) + } + } + } + + func backgroundSync(task: BGTask? = nil) async { + defer { + // Update badge safely at the end of the background sync + Task { @MainActor in + do { + let count = await NCManageDatabase.shared.getMetadatasInWaitingCountAsync() + try await UNUserNotificationCenter.current().setBadgeCount(count) + } catch { } + } + } + + // BGTask expiration flag + var expired = false + task?.expirationHandler = { + expired = true + } + + // Discover new items for Auto Upload + let numAutoUpload = await NCAutoUpload.shared.initAutoUpload() + nkLog(tag: self.global.logTagBgSync, emoji: .start, message: "Auto upload found \(numAutoUpload) new items") + guard !expired else { return } + + // Fetch METADATAS + let metadatas = await NCManageDatabase.shared.getMetadataProcess() + guard !metadatas.isEmpty, !expired else { + return + } + + // Create all pending Auto Upload folders (fail-fast) + let pendingCreateFolders = metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitCreateFolder && + $0.sessionSelector == self.global.selectorUploadAutoUpload + } + + for metadata in pendingCreateFolders { + guard !expired else { return } + + let err = await NCNetworking.shared.createFolderForAutoUpload( + serverUrlFileName: meta.serverUrlFileName, + account: meta.account + ) + // Fail-fast: abort the whole sync on first failure + if err != .success { + nkLog(tag: self.global.logTagBgSync, emoji: .error, message: "Create folder '\(meta.serverUrlFileName)' failed: \(err.errorCode) – aborting sync") + return + } + } + + // Capacity computation + let downloading = metadatas.lazy.filter { $0.status == self.global.metadataStatusDownloading }.count + let uploading = metadatas.lazy.filter { $0.status == self.global.metadataStatusUploading }.count + let availableProcess = max(0, NCBrandOptions.shared.numMaximumProcess - (downloading + uploading)) + + // Start Auto Uploads + let metadatasToUpload = Array( + metadatas.lazy.filter { + $0.status == self.global.metadataStatusWaitUpload && + $0.sessionSelector == self.global.selectorUploadAutoUpload && + $0.chunk == 0 + } + .prefix(availableProcess) + ) + + let cameraRoll = NCCameraRoll() + + for metadata in metadatasToUpload { + guard !expired else { return } + + // File exists? skip it + let existsResult = await NCNetworking.shared.fileExists(serverUrlFileName: metadata.serverUrlFileName, account: metadata.account) + if existsResult == .success { + // File exists → delete from local metadata and skip + await NCManageDatabase.shared.deleteMetadataAsync(id: metadata.ocId) + continue + } else if existsResult.errorCode == 404 { + // 404 Not Found → directory does not exist + // Proceed + } else { + // Any other error (423 locked, 401 auth, 403 forbidden, 5xx, etc.) + continue + } + + // Expand seed into concrete metadatas (e.g., Live Photo pair) + let extracted = await cameraRoll.extractCameraRoll(from: metadata) + guard !expired else { return } + + for metadata in extracted { + // Sequential await keeps ordering and simplifies backpressure + let err = await NCNetworking.shared.uploadFileInBackground(metadata: metadata.detachedCopy()) + if err == .success { + nkLog(tag: self.global.logTagBgSync, message: "In queued upload \(metadata.fileName) -> \(metadata.serverUrl)") + } else { + nkLog(tag: self.global.logTagBgSync, emoji: .error, message: "Upload failed \(metadata.fileName) -> \(metadata.serverUrl) [\(err.errorDescription)]") + } + guard !expired else { return } + } + } + } + // MARK: - Background Networking Session func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { @@ -248,7 +470,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // MARK: - + // MARK: - Trust Certificate Error func trustCertificateError(host: String) { guard let activeTblAccount = NCManageDatabase.shared.getActiveTableAccount(), @@ -300,6 +522,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NCPreferences().removeAll() + // Reset App Icon badge / File Icon badge + if #available(iOS 17.0, *) { + UNUserNotificationCenter.current().setBadgeCount(0) + } exit(0) } @@ -308,6 +534,151 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return false } + + // MARK: - Login + + func openLogin(selector: Int, window: UIWindow? = nil) { + UIApplication.shared.allSceneSessionDestructionExceptFirst() + + // Nextcloud standard login + if selector == NCGlobal.shared.introSignup { + if activeLogin?.view.window == nil { + if selector == NCGlobal.shared.introSignup { + let web = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginProvider") as? NCLoginProvider + web?.initialURLString = NCBrandOptions.shared.linkloginPreferredProviders + showLoginViewController(web) + } else { + activeLogin = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLogin") as? NCLogin + if let controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController, !controller.account.isEmpty { + let session = NCSession.shared.getSession(account: controller.account) + activeLogin?.urlBase = session.urlBase + } + showLoginViewController(activeLogin) + } + } + } else { + if activeLogin?.view.window == nil { + activeLogin = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLogin") as? NCLogin + activeLogin?.urlBase = NCBrandOptions.shared.disable_request_login_url ? NCBrandOptions.shared.loginBaseUrl : "" + showLoginViewController(activeLogin) + } + } + } + + func openLogin(viewController: UIViewController?, selector: Int, openLoginWeb: Bool) { +// openLogin(selector: NCGlobal.shared.introLogin) + // [WEBPersonalized] [AppConfig] + if NCBrandOptions.shared.use_login_web_personalized || NCBrandOptions.shared.use_AppConfig { + + if activeLoginWeb?.view.window == nil { + activeLoginWeb = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginWeb") as? NCLoginWeb + activeLoginWeb?.urlBase = NCBrandOptions.shared.loginBaseUrl + showLoginViewController(activeLoginWeb, contextViewController: viewController) + } + return + } + + // Nextcloud standard login + if selector == NCGlobal.shared.introSignup { + + if activeLoginWeb?.view.window == nil { + activeLoginWeb = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginWeb") as? NCLoginWeb + if selector == NCGlobal.shared.introSignup { + activeLoginWeb?.urlBase = NCBrandOptions.shared.linkloginPreferredProviders + } else { + activeLoginWeb?.urlBase = self.urlBase + } + showLoginViewController(activeLoginWeb, contextViewController: viewController) + } + + } else if NCBrandOptions.shared.disable_intro && NCBrandOptions.shared.disable_request_login_url { + + if activeLoginWeb?.view.window == nil { + activeLoginWeb = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginWeb") as? NCLoginWeb + activeLoginWeb?.urlBase = NCBrandOptions.shared.loginBaseUrl + showLoginViewController(activeLoginWeb, contextViewController: viewController) + } + + } else if openLoginWeb { + + // Used also for reinsert the account (change passwd) + if activeLoginWeb?.view.window == nil { + activeLoginWeb = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLoginWeb") as? NCLoginWeb + activeLoginWeb?.urlBase = urlBase + activeLoginWeb?.user = user + showLoginViewController(activeLoginWeb, contextViewController: viewController) + } + + } else { + + if activeLogin?.view.window == nil { + activeLogin = UIStoryboard(name: "NCLogin", bundle: nil).instantiateViewController(withIdentifier: "NCLogin") as? NCLogin + showLoginViewController(activeLogin, contextViewController: viewController) + } + } + } + + func showLoginViewController(_ viewController: UIViewController?) { + guard let viewController else { return } + let navigationController = UINavigationController(rootViewController: viewController) + + navigationController.modalPresentationStyle = .fullScreen + navigationController.navigationBar.barStyle = .black + navigationController.navigationBar.tintColor = NCBrandColor.shared.customerText + navigationController.navigationBar.barTintColor = NCBrandColor.shared.customer + navigationController.navigationBar.isTranslucent = false + + if let controller = UIApplication.shared.firstWindow?.rootViewController { + if let presentedVC = controller.presentedViewController, !(presentedVC is UINavigationController) { + presentedVC.dismiss(animated: false) { + controller.present(navigationController, animated: true) + } + } else { + controller.present(navigationController, animated: true) + } + } else { + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } + } + + func showLoginViewController(_ viewController: UIViewController?, contextViewController: UIViewController?) { + + if contextViewController == nil { + if let viewController = viewController { + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.barStyle = .black + navigationController.navigationBar.tintColor = NCBrandColor.shared.customerText + navigationController.navigationBar.barTintColor = NCBrandColor.shared.customer + navigationController.navigationBar.isTranslucent = false + window?.rootViewController = navigationController + window?.makeKeyAndVisible() + } + } else if contextViewController is UINavigationController { + if let contextViewController = contextViewController, let viewController = viewController { + (contextViewController as? UINavigationController)?.pushViewController(viewController, animated: true) + } + } else { + if let viewController = viewController, let contextViewController = contextViewController { + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.modalPresentationStyle = .fullScreen + navigationController.navigationBar.barStyle = .black + navigationController.navigationBar.tintColor = NCBrandColor.shared.customerText + navigationController.navigationBar.barTintColor = NCBrandColor.shared.customer + navigationController.navigationBar.isTranslucent = false + contextViewController.present(navigationController, animated: true) { } + } + } + } + + @objc func startTimerErrorNetworking() { + timerErrorNetworking = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(checkErrorNetworking), userInfo: nil, repeats: true) + } + + @objc private func checkErrorNetworking() { + guard !account.isEmpty, NCKeychain().getPassword(account: account).isEmpty else { return } + openLogin(viewController: window?.rootViewController, selector: NCGlobal.shared.introLogin, openLoginWeb: true) + } } // MARK: - Extension diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 2737cb1ebb..989e27ca1d 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -168,7 +168,7 @@ extension tableMetadata { if isDocumentViewableOnly { return false } - if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKCommon.TypeClassFile.image.rawValue { + if ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/") || classFile == NKTypeClassFile.image.rawValue { return true } return false @@ -177,11 +177,11 @@ extension tableMetadata { var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - + var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKCommon.TypeClassFile.document.rawValue + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue } - + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -207,7 +207,7 @@ extension tableMetadata { } var isCopyableInPasteboard: Bool { - !isDocumentViewableOnly && !directory + !directory } #if !EXTENSION_FILE_PROVIDER_EXTENSION @@ -216,11 +216,11 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDocumentViewableOnly && !isDirectoryE2EE && !e2eEncrypted + !isDirectoryE2EE && !e2eEncrypted } var isModifiableWithQuickLook: Bool { - if directory || isDocumentViewableOnly || isDirectoryE2EE { + if directory || isDirectoryE2EE { return false } return isPDF || isImage @@ -251,8 +251,7 @@ extension tableMetadata { } var canSetAsAvailableOffline: Bool { -// return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted - return session.isEmpty && !isDocumentViewableOnly + return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted } func isSharable() -> Bool { @@ -420,7 +419,7 @@ extension tableMetadata { guard let capabilities = NCNetworking.shared.capabilities[account] else { return false } - if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE) { + if !capabilities.fileSharingApiEnabled || (capabilities.e2EEEnabled && isDirectoryE2EE), !e2eEncrypted { return false } return !e2eEncrypted @@ -1000,6 +999,34 @@ extension NCManageDatabase { .map { $0.detachedCopy() } } ?? [] } + + func getMediaMetadatas(predicate: NSPredicate, sorted: String? = nil, ascending: Bool = false) -> ThreadSafeArray? { + + do { + let realm = try Realm() + if let sorted { + var results: [tableMetadata] = [] + switch sorted {//NCPreferences().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 + } func getMetadatas(predicate: NSPredicate, sortedByKeyPath: String, @@ -1290,24 +1317,12 @@ extension NCManageDatabase { counter += 1 listIdentifierRank[item.ocId] = NSNumber(value: counter) } - + return listIdentifierRank } return result ?? [:] } - @objc func clearMetadatasUpload(account: String) { - do { - let realm = try Realm() - try realm.write { - let results = realm.objects(tableMetadata.self).filter("account == %@ AND (status == %d OR status == %d)", account, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploadError) - realm.delete(results) - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - func getAssetLocalIdentifiersUploadedAsync() async -> [String]? { return await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") @@ -1416,41 +1431,17 @@ extension NCManageDatabase { } ?? false } - func createMetadatasFolder(assets: [PHAsset], - useSubFolder: Bool, - session: NCSession.Session, completion: @escaping ([tableMetadata]) -> Void) { - var foldersCreated: Set = [] - var metadatas: [tableMetadata] = [] - let serverUrlBase = getAccountAutoUploadDirectory(session: session) - let fileNameBase = getAccountAutoUploadFileName(account: session.account) - let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, serverUrlBase) - - func createMetadata(serverUrl: String, fileName: String, metadata: tableMetadata?) { - guard !foldersCreated.contains(serverUrl + "/" + fileName) else { - return - } - foldersCreated.insert(serverUrl + "/" + fileName) - - if let metadata { - metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder - metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload - metadata.sessionDate = Date() - metadatas.append(tableMetadata(value: metadata)) - } else { - let metadata = NCManageDatabase.shared.createMetadata(fileName: fileName, - fileNameView: fileName, - ocId: NSUUID().uuidString, - serverUrl: serverUrl, - url: "", - contentType: "httpd/unix-directory", - directory: true, - session: session, - sceneIdentifier: nil) - metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder - metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload - metadata.sessionDate = Date() - metadatas.append(metadata) - } + func getMetadataDirectoryAsync(serverUrl: String, account: String) async -> tableMetadata? { + guard let url = URL(string: serverUrl) else { + return nil + } + let fileName = url.lastPathComponent + var baseUrl = url.deletingLastPathComponent().absoluteString + if baseUrl.hasSuffix("/") { + baseUrl.removeLast() + } + guard let decodedBaseUrl = baseUrl.removingPercentEncoding else { + return nil } return await core.performRealmReadAsync { realm in @@ -1460,7 +1451,7 @@ extension NCManageDatabase { return object?.detachedCopy() } } - + func getMetadataDirectory(serverUrl: String, account: String) -> tableMetadata? { guard let url = URL(string: serverUrl) else { return nil diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index 517f2fdfb8..5733a680ee 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -1,10 +1,10 @@ - + - - + + @@ -18,7 +18,7 @@ - + @@ -31,78 +31,44 @@ - + - + - - - - - - - + + + + - - - - - - - - + + + - - - - - + + @@ -111,4 +77,9 @@ + + + + + diff --git a/iOSClient/Menu/NCMedia+Menu.swift b/iOSClient/Menu/NCMedia+Menu.swift index 4951c4711a..495fd11d09 100644 --- a/iOSClient/Menu/NCMedia+Menu.swift +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -43,175 +43,175 @@ extension NCMedia { 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", colors: [NCBrandColor.shared.iconColor]), - action: { _ in - self.isEditMode = true - self.collectionView.reloadData() - } - ) - ) - } - - actions.append(.seperator(order: 0)) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_viewimage_show_", comment: ""), - 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.loadDataSource() - } - ) - ) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_viewvideo_show_", comment: ""), - icon: utility.loadImage(named: showOnlyVideos ? "videono" : "videoyes", colors: [NCBrandColor.shared.iconColor]), - selected: showOnlyVideos, - on: true, - action: { _ in - self.showOnlyImages = false - self.showOnlyVideos = true - self.loadDataSource() - } - ) - ) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_show_all_", comment: ""), - 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.loadDataSource() - } - ) - ) - - actions.append(.seperator(order: 0)) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_select_media_folder_", comment: ""), - 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 { - - viewController.delegate = self - viewController.typeOfCommandView = .select - viewController.type = "mediaFolder" - - self.present(navigationController, animated: true, completion: nil) - } - } - ) - ) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_by_modified_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), - selected: NCKeychain().mediaSortDate == "date", - on: true, - action: { _ in - NCKeychain().mediaSortDate = "date" - self.loadDataSource() - } - ) - ) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_by_created_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), - selected: NCKeychain().mediaSortDate == "creationDate", - on: true, - action: { _ in - NCKeychain().mediaSortDate = "creationDate" - self.loadDataSource() - } - ) - ) - - actions.append( - NCMenuAction( - title: NSLocalizedString("_media_by_upload_date_", comment: ""), - icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconColor]), - selected: NCKeychain().mediaSortDate == "uploadDate", - on: true, - action: { _ in - NCKeychain().mediaSortDate = "uploadDate" - self.loadDataSource() - } - ) - ) - - } else { - - // - // CANCEL - // - actions.append( - NCMenuAction( - title: NSLocalizedString("_cancel_", comment: ""), - icon: utility.loadImage(named: "xmark", colors: [NCBrandColor.shared.iconColor]), - action: { _ in self.tapSelect() } - ) - ) - - 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, controller: self.controller, completion: tapSelect)) - - // - // SAVE TO PHOTO GALLERY - // - actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) - - // - // COPY - MOVE - // - actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) - - // - // COPY - // - 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 != session.userId }) { - actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) - } - } +// 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", colors: [NCBrandColor.shared.iconImageColor]), +// action: { _ in +// self.isEditMode = true +// self.collectionView.reloadData() +// } +// ) +// ) +// } +// +// actions.append(.seperator(order: 0)) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_viewimage_show_", comment: ""), +// icon: utility.loadImage(named: showOnlyImages ? "nocamera" : "file_photo_menu", colors: [NCBrandColor.shared.iconImageColor]), +// selected: showOnlyImages, +// on: true, +// action: { _ in +// self.showOnlyImages = true +// self.showOnlyVideos = false +// self.loadDataSource() +// } +// ) +// ) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_viewvideo_show_", comment: ""), +// icon: utility.loadImage(named: showOnlyVideos ? "videono" : "videoyes", colors: [NCBrandColor.shared.iconImageColor]), +// selected: showOnlyVideos, +// on: true, +// action: { _ in +// self.showOnlyImages = false +// self.showOnlyVideos = true +// self.loadDataSource() +// } +// ) +// ) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_show_all_", comment: ""), +// icon: utility.loadImage(named: "photo.on.rectangle.angled", colors: [NCBrandColor.shared.iconImageColor]), +// selected: !showOnlyImages && !showOnlyVideos, +// on: true, +// action: { _ in +// self.showOnlyImages = false +// self.showOnlyVideos = false +// self.loadDataSource() +// } +// ) +// ) +// +// actions.append(.seperator(order: 0)) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_select_media_folder_", comment: ""), +// icon: utility.loadImage(named: "folder", colors: [NCBrandColor.shared.iconImageColor]), +// 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" +// +// self.present(navigationController, animated: true, completion: nil) +// } +// } +// ) +// ) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_by_modified_date_", comment: ""), +// icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconImageColor]), +// selected: NCKeychain().mediaSortDate == "date", +// on: true, +// action: { _ in +// NCKeychain().mediaSortDate = "date" +// self.loadDataSource() +// } +// ) +// ) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_by_created_date_", comment: ""), +// icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconImageColor]), +// selected: NCKeychain().mediaSortDate == "creationDate", +// on: true, +// action: { _ in +// NCKeychain().mediaSortDate = "creationDate" +// self.loadDataSource() +// } +// ) +// ) +// +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_media_by_upload_date_", comment: ""), +// icon: utility.loadImage(named: "sortFileNameAZ", colors: [NCBrandColor.shared.iconImageColor]), +// selected: NCKeychain().mediaSortDate == "uploadDate", +// on: true, +// action: { _ in +// NCKeychain().mediaSortDate = "uploadDate" +// self.loadDataSource() +// } +// ) +// ) +// +// } else { +// +// // +// // CANCEL +// // +// actions.append( +// NCMenuAction( +// title: NSLocalizedString("_cancel_", comment: ""), +// icon: utility.loadImage(named: "xmark", colors: [NCBrandColor.shared.iconImageColor]), +// action: { _ in self.tapSelect() } +// ) +// ) +// +// 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, controller: self.controller, completion: tapSelect)) +// +// // +// // SAVE TO PHOTO GALLERY +// // +// actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) +// +// // +// // COPY - MOVE +// // +// actions.append(.moveOrCopyAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) +// +// // +// // COPY +// // +// 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 != session.userId }) { +// actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, controller: self.controller, completion: tapSelect)) +// } +// } } } diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index f8859ca1da..7d07aa5a31 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -19,8 +19,6 @@ final class NCImageCache: @unchecked Sendable { private let allowExtensions = [NCGlobal.shared.previewExt256] private var brandElementColor: UIColor? - private var observerToken: NSObjectProtocol? - public var countLimit: Int = 2000 lazy var cache: LRUCache = { return LRUCache(countLimit: countLimit) @@ -29,10 +27,7 @@ final class NCImageCache: @unchecked Sendable { public var isLoadingCache: Bool = false public var controller: UITabBarController? - 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)')" + let showBothPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKTypeClassFile.image.rawValue)' OR classFile == '\(NKTypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKTypeClassFile.video.rawValue)')" init() { observerToken = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: nil) { _ in @@ -41,7 +36,8 @@ final class NCImageCache: @unchecked Sendable { } NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { _ in - self.cache.removeAll() + self.cache.removeAllValues() +// self.cache.removeAll() self.cache = LRUCache(countLimit: self.countLimit) } @@ -68,10 +64,10 @@ final class NCImageCache: @unchecked Sendable { self.isLoadingCache = true self.database.filterAndNormalizeLivePhotos(from: metadatas) { metadatas in autoreleasepool { - self.cache.removeAll() + self.cache.removeAllValues() for metadata in metadatas { guard !isAppInBackground else { - self.cache.removeAll() + self.cache.removeAllValues() break } if let image = self.utility.getImage(ocId: metadata.ocId, @@ -93,27 +89,16 @@ final class NCImageCache: @unchecked Sendable { } deinit { - if let token = observerToken { - NotificationCenter.default.removeObserver(token) - } + 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? { 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") } - + func allowExtensions(ext: String) -> Bool { return allowExtensions.contains(ext) } @@ -150,7 +135,8 @@ final class NCImageCache: @unchecked Sendable { } func removeAll() { - cache.removeAll() +// cache.removeAll() + self.cache.removeAllValues() } // MARK: - MEDIA - @@ -213,31 +199,35 @@ final class NCImageCache: @unchecked Sendable { // MARK: - func getImageFile(colors: [UIColor] = [NCBrandColor.shared.iconImageColor2]) -> UIImage { - return utility.loadImage(named: "doc", colors: colors) + return UIImage(named: "file")!.image(color: colors.first!, size: 24) } - func getImageShared(colors: [UIColor] = NCBrandColor.shared.iconImageMultiColors) -> UIImage { - return utility.loadImage(named: "person.fill.badge.plus", colors: colors) + func getImageShared(colors: [UIColor] = [NCBrandColor.shared.iconSystemGrayColor]) -> UIImage { + return utility.loadImage(named: "share", colors: colors, size: 24) } - func getImageCanShare(colors: [UIColor] = NCBrandColor.shared.iconImageMultiColors) -> UIImage { - return utility.loadImage(named: "person.fill.badge.plus", colors: colors) + func getImageCanShare(colors: [UIColor] = [NCBrandColor.shared.iconSystemGrayColor]) -> UIImage { + return utility.loadImage(named: "share", colors: colors, size: 24) } - func getImageShareByLink(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { - return utility.loadImage(named: "link", colors: colors) + func getImageShareByLink(colors: [UIColor] = [NCBrandColor.shared.iconSystemGrayColor]) -> UIImage { + return utility.loadImage(named: "share", colors: colors, size: 24) } + func getImageSharedWithMe(colors: [UIColor] = [NCBrandColor.shared.iconSystemGrayColor]) -> UIImage { + return utility.loadImage(named: "cloudUpload", colors: [NCBrandColor.shared.nmcIconSharedWithMe], size: 24) + } + func getImageFavorite(colors: [UIColor] = [NCBrandColor.shared.yellowFavorite]) -> UIImage { - return utility.loadImage(named: "star.fill", colors: colors) + return utility.loadImage(named: "star.fill", colors: colors, size: 24) } func getImageOfflineFlag(colors: [UIColor] = [.systemGreen]) -> UIImage { - return utility.loadImage(named: "arrow.down.circle.fill", colors: colors) + return utility.loadImage(named: "arrow.down.circle.fill", colors: colors, size: 24) } func getImageLocal(colors: [UIColor] = [.systemGreen]) -> UIImage { - return utility.loadImage(named: "checkmark.circle.fill", colors: colors) + return utility.loadImage(named: "checkmark.circle.fill", colors: colors, size: 24) } func getImageCheckedYes(color: UIColor) -> UIImage? { @@ -252,43 +242,51 @@ final class NCImageCache: @unchecked Sendable { return UIImage(systemName: "circle", withConfiguration: config) } + func getImageCheckedYes(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { + return UIImage(named: "checkedYes")! + } + + func getImageCheckedNo(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { + return utility.loadImage(named: "circle", colors: colors, size: 24) + } + func getImageButtonMore(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { - return utility.loadImage(named: "ellipsis", colors: colors) + return UIImage(named: "more")!.image(color: .systemGray, size: 24) } func getImageButtonStop(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { - return utility.loadImage(named: "stop.circle", colors: colors) + return utility.loadImage(named: "stop.circle", colors: colors, size: 24) } func getImageButtonMoreLock(colors: [UIColor] = [NCBrandColor.shared.iconImageColor]) -> UIImage { - return utility.loadImage(named: "lock.fill", colors: colors) + return utility.loadImage(named: "lock.fill", colors: colors, size: 24) } func getFolder(account: String) -> UIImage { - return UIImage(named: "folder")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folder")! } func getFolderEncrypted(account: String) -> UIImage { - return UIImage(named: "folderEncrypted")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folderEncrypted")! } func getFolderSharedWithMe(account: String) -> UIImage { - return UIImage(named: "folder_shared_with_me")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folder_shared_with_me")! } func getFolderPublic(account: String) -> UIImage { - return UIImage(named: "folder_public")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folder_public")! } func getFolderGroup(account: String) -> UIImage { - return UIImage(named: "folder_group")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folder_group")! } func getFolderExternal(account: String) -> UIImage { - return UIImage(named: "folder_external")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folder_external")! } func getFolderAutomaticUpload(account: String) -> UIImage { - return UIImage(named: "folderAutomaticUpload")!.image(color: NCBrandColor.shared.getElement(account: account)) + return UIImage(named: "folderAutomaticUpload")! } } diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 4e08a797bc..96ce2c79ee 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -1,72 +1,44 @@ -// -// NCKeychain.swift -// Nextcloud -// -// Created by Marino Faggiana on 23/10/23. -// Copyright © 2023 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 . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit import KeychainAccess +import NextcloudKit -@objc class NCKeychain: NSObject { - +final class NCPreferences: NSObject { let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { get { - if let value = try? keychain.get("showDescription"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "showDescription", defaultValue: true) } set { - keychain["showDescription"] = String(newValue) + setUserDefaults(newValue, forKey: "showDescription") } } var showRecommendedFiles: Bool { get { - if let value = try? keychain.get("showRecommendedFiles"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "showRecommendedFiles", defaultValue: true) } set { - keychain["showRecommendedFiles"] = String(newValue) + setUserDefaults(newValue, forKey: "showRecommendedFiles") } } var typeFilterScanDocument: NCGlobal.TypeFilterScanDocument { get { - if let rawValue = try? keychain.get("ScanDocumentTypeFilter"), let value = NCGlobal.TypeFilterScanDocument(rawValue: rawValue) { - return value - } else { - return .original - } + let rawValue = getStringPreference(key: "ScanDocumentTypeFilter", defaultValue: NCGlobal.TypeFilterScanDocument.original.rawValue) + return NCGlobal.TypeFilterScanDocument(rawValue: rawValue) ?? .original } set { - keychain["ScanDocumentTypeFilter"] = newValue.rawValue + setUserDefaults(newValue.rawValue, forKey: "ScanDocumentTypeFilter") } } - @objc var passcode: String? { + var passcode: String? { get { migrate(key: "passcodeBlock") if let value = try? keychain.get("passcodeBlock"), !value.isEmpty { @@ -79,7 +51,7 @@ import KeychainAccess } } - @objc var resetAppCounterFail: Bool { + var resetAppCounterFail: Bool { get { if let value = try? keychain.get("resetAppCounterFail"), let result = Bool(value) { return result @@ -115,7 +87,7 @@ import KeychainAccess } } - @objc var requestPasscodeAtStart: Bool { + var requestPasscodeAtStart: Bool { get { let keychainOLD = Keychain(service: "Crypto Cloud") if let value = keychainOLD["notPasscodeAtStart"], !value.isEmpty { @@ -138,7 +110,7 @@ import KeychainAccess } } - @objc var touchFaceID: Bool { + var touchFaceID: Bool { get { migrate(key: "enableTouchFaceID") if let value = try? keychain.get("enableTouchFaceID"), let result = Bool(value) { @@ -155,321 +127,264 @@ import KeychainAccess return passcode != nil && requestPasscodeAtStart } - @objc var incrementalNumber: String { - migrate(key: "incrementalnumber") + var incrementalNumber: String { var incrementalString = String(format: "%04ld", 0) - if let value = try? keychain.get("incrementalnumber"), var result = Int(value) { - result += 1 - incrementalString = String(format: "%04ld", result) + let value = getStringPreference(key: "incrementalnumber", defaultValue: incrementalString) + if var intValue = Int(value) { + intValue += 1 + incrementalString = String(format: "%04ld", intValue) } - keychain["incrementalnumber"] = incrementalString + setUserDefaults(incrementalString, forKey: "incrementalnumber") return incrementalString } - @objc var showHiddenFiles: Bool { + var formatCompatibility: Bool { get { - migrate(key: "showHiddenFiles") - if let value = try? keychain.get("showHiddenFiles"), let result = Bool(value) { - return result - } - return false - } - set { - keychain["showHiddenFiles"] = String(newValue) - } - } - - @objc var formatCompatibility: Bool { - get { - migrate(key: "formatCompatibility") - if let value = try? keychain.get("formatCompatibility"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "formatCompatibility", defaultValue: true) } set { - keychain["formatCompatibility"] = String(newValue) + setUserDefaults(newValue, forKey: "formatCompatibility") } } - @objc var disableFilesApp: Bool { + var disableFilesApp: Bool { get { - migrate(key: "disablefilesapp") - if let value = try? keychain.get("disablefilesapp"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "disablefilesapp", defaultValue: false) } set { - keychain["disablefilesapp"] = String(newValue) + setUserDefaults(newValue, forKey: "disablefilesapp") } } - @objc var livePhoto: Bool { + var livePhoto: Bool { get { - migrate(key: "livePhoto") - if let value = try? keychain.get("livePhoto"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "livePhoto", defaultValue: true) } set { - keychain["livePhoto"] = String(newValue) + setUserDefaults(newValue, forKey: "livePhoto") } } - @objc var disableCrashservice: Bool { + var disableCrashservice: Bool { get { - migrate(key: "crashservice") - if let value = try? keychain.get("crashservice"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "crashservice", defaultValue: false) } set { - keychain["crashservice"] = String(newValue) + setUserDefaults(newValue, forKey: "crashservice") } } - @objc var logLevel: Int { + /// Stores and retrieves the current log level from the keychain. + var log: NKLogLevel { get { - migrate(key: "logLevel") - if let value = try? keychain.get("logLevel"), let result = Int(value) { - return result - } - return 1 + let value = getIntPreference(key: "logLevel", defaultValue: NKLogLevel.normal.rawValue) + return NKLogLevel(rawValue: value) ?? NKLogLevel.normal } set { - keychain["logLevel"] = String(newValue) + setUserDefaults(newValue.rawValue, forKey: "logLevel") } } - @objc var accountRequest: Bool { + var accountRequest: Bool { get { - migrate(key: "accountRequest") - if let value = try? keychain.get("accountRequest"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "accountRequest", defaultValue: false) } set { - keychain["accountRequest"] = String(newValue) + setUserDefaults(newValue, forKey: "accountRequest") } } - @objc var removePhotoCameraRoll: Bool { + var removePhotoCameraRoll: Bool { get { - migrate(key: "removePhotoCameraRoll") - if let value = try? keychain.get("removePhotoCameraRoll"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "removePhotoCameraRoll", defaultValue: false) } set { - keychain["removePhotoCameraRoll"] = String(newValue) + setUserDefaults(newValue, forKey: "removePhotoCameraRoll") } } - @objc var privacyScreenEnabled: Bool { + var privacyScreenEnabled: Bool { get { - migrate(key: "privacyScreen") - if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { - return result + if NCBrandOptions.shared.enforce_privacyScreenEnabled { + return true } - return false + return getBoolPreference(key: "privacyScreen", defaultValue: false) } set { - keychain["privacyScreen"] = String(newValue) + setUserDefaults(newValue, forKey: "privacyScreen") } } - @objc var cleanUpDay: Int { + var cleanUpDay: Int { get { - migrate(key: "cleanUpDay") - if let value = try? keychain.get("cleanUpDay"), let result = Int(value) { - return result - } - return NCBrandOptions.shared.cleanUpDay + let value = getIntPreference(key: "cleanUpDay", defaultValue: NCBrandOptions.shared.cleanUpDay) + return value } set { - keychain["cleanUpDay"] = String(newValue) + setUserDefaults(newValue, forKey: "cleanUpDay") } } var mediaColumnCount: Int { get { - if let value = try? keychain.get("mediaColumnCount"), let result = Int(value) { - return result - } - return 3 + let value = getIntPreference(key: "mediaColumnCount", defaultValue: 3) + return value } set { - keychain["mediaColumnCount"] = String(newValue) + setUserDefaults(newValue, forKey: "mediaColumnCount") } } var mediaTypeLayout: String { get { - if let value = try? keychain.get("mediaTypeLayout") { - return value - } - return NCGlobal.shared.mediaLayoutRatio + let value = getStringPreference(key: "mediaTypeLayout", defaultValue: NCGlobal.shared.mediaLayoutRatio) + return value } set { - keychain["mediaTypeLayout"] = String(newValue) + setUserDefaults(newValue, forKey: "mediaTypeLayout") } } var mediaSortDate: String { get { - migrate(key: "mediaSortDate") - if let value = try? keychain.get("mediaSortDate") { - return value - } - return "date" + let value = getStringPreference(key: "mediaSortDate", defaultValue: "date") + return value } set { - keychain["mediaSortDate"] = newValue + setUserDefaults(newValue, forKey: "mediaSortDate") } } var textRecognitionStatus: Bool { get { - migrate(key: "textRecognitionStatus") - if let value = try? keychain.get("textRecognitionStatus"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) } set { - keychain["textRecognitionStatus"] = String(newValue) + setUserDefaults(newValue, forKey: "textRecognitionStatus") } } var deleteAllScanImages: Bool { get { - migrate(key: "deleteAllScanImages") - if let value = try? keychain.get("deleteAllScanImages"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "deleteAllScanImages", defaultValue: false) } set { - keychain["deleteAllScanImages"] = String(newValue) + setUserDefaults(newValue, forKey: "deleteAllScanImages") } } var qualityScanDocument: Double { get { - migrate(key: "qualityScanDocument") - if let value = try? keychain.get("qualityScanDocument"), let result = Double(value) { - return result - } - return 2 + let value = getIntPreference(key: "qualityScanDocument", defaultValue: 2) + return Double(value) } set { - keychain["qualityScanDocument"] = String(newValue) + setUserDefaults(newValue, forKey: "qualityScanDocument") } } var appearanceAutomatic: Bool { get { - if let value = try? keychain.get("appearanceAutomatic"), let result = Bool(value) { - return result - } - return true + let value = getBoolPreference(key: "appearanceAutomatic", defaultValue: true) + return value } set { - keychain["appearanceAutomatic"] = String(newValue) + setUserDefaults(newValue, forKey: "appearanceAutomatic") } } var appearanceInterfaceStyle: UIUserInterfaceStyle { get { - if let value = try? keychain.get("appearanceInterfaceStyle") { - if value == "light" { - return .light - } else { - return .dark - } + let value = getStringPreference(key: "appearanceInterfaceStyle", defaultValue: "light") + if value == "light" { + return .light + } else { + return .dark } - return .light } set { if newValue == .light { - keychain["appearanceInterfaceStyle"] = "light" + setUserDefaults("light", forKey: "appearanceInterfaceStyle") } else { - keychain["appearanceInterfaceStyle"] = "dark" + setUserDefaults("dark", forKey: "appearanceInterfaceStyle") } } } var screenAwakeMode: AwakeMode { get { - if let value = try? keychain.get("screenAwakeMode") { - if value == "off" { - return .off - } else if value == "on" { - return .on - } else { - return .whileCharging - } + let value = getStringPreference(key: "screenAwakeMode", defaultValue: "off") + if value == "off" { + return .off + } else if value == "on" { + return .on + } else { + return .whileCharging } - return .off } set { if newValue == .off { - keychain["screenAwakeMode"] = "off" + setUserDefaults("off", forKey: "screenAwakeMode") } else if newValue == .on { - keychain["screenAwakeMode"] = "on" + setUserDefaults("on", forKey: "screenAwakeMode") } else { - keychain["screenAwakeMode"] = "whileCharging" + setUserDefaults("whileCharging", forKey: "screenAwakeMode") } } } var fileNameType: Bool { get { - if let value = try? keychain.get("fileNameType"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "fileNameType", defaultValue: false) } set { - keychain["fileNameType"] = String(newValue) + setUserDefaults(newValue, forKey: "fileNameType") } } var fileNameOriginal: Bool { get { - if let value = try? keychain.get("fileNameOriginal"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "fileNameOriginal", defaultValue: false) } set { - keychain["fileNameOriginal"] = String(newValue) + setUserDefaults(newValue, forKey: "fileNameOriginal") } } var fileNameMask: String { get { - if let value = try? keychain.get("fileNameMask") { - return value - } - return "" + return getStringPreference(key: "fileNameMask", defaultValue: "") + } + set { + setUserDefaults(newValue, forKey: "fileNameMask") + } + } + + var location: Bool { + get { + return getBoolPreference(key: "location", defaultValue: false) + } + set { + setUserDefaults(newValue, forKey: "location") + } + } + + var deviceTokenPushNotification: String { + get { + return getStringPreference(key: "deviceTokenPushNotification", defaultValue: "") } set { - keychain["fileNameMask"] = String(newValue) + setUserDefaults(newValue, forKey: "deviceTokenPushNotification") } } // MARK: - - @objc func getPassword(account: String) -> String { + func getPassword(account: String) -> String { let key = "password" + account migrate(key: key) - return (try? keychain.get(key)) ?? "" + let password = (try? keychain.get(key)) ?? "" + return password } func setPassword(account: String, password: String?) { @@ -478,83 +393,41 @@ import KeychainAccess } func setPersonalFilesOnly(account: String, value: Bool) { - let key = "personalFilesOnly" + account - keychain[key] = String(value) + let userDefaultsKey = "personalfilesonly" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } func getPersonalFilesOnly(account: String) -> Bool { - let key = "personalFilesOnly" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return false - } - } - - /* OBSOLETE - func setDirectoryOnTop(account: String, value: Bool) { - let key = "directoryOnTop" + account - keychain[key] = String(value) - } - - func getDirectoryOnTop(account: String) -> Bool { - let key = "directoryOnTop" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return true - } + return getBoolPreference(key: "personalfilesonly", account: account, defaultValue: false) } - */ - func setTitleButtonHeader(account: String, value: String?) { - let key = "titleButtonHeader" + account - keychain[key] = value + func setFavoriteOnTop(account: String, value: Bool) { + let userDefaultsKey = "favoriteOnTop" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - 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 + func getFavoriteOnTop(account: String) -> Bool { + return getBoolPreference(key: "favoriteOnTop", account: account, defaultValue: true) } - @objc func setOriginalFileName(key: String, value: Bool) { - keychain[key] = String(value) + func setDirectoryOnTop(account: String, value: Bool) { + let userDefaultsKey = "directoryOnTop" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - @objc func getFileNameMask(key: String) -> String { - migrate(key: key) - if let value = try? keychain.get(key) { - return value - } else { - return "" - } + func getDirectoryOnTop(account: String) -> Bool { + return getBoolPreference(key: "directoryOnTop", account: account, defaultValue: true) } - @objc func setFileNameMask(key: String, mask: String?) { - keychain[key] = mask + func setShowHiddenFiles(account: String, value: Bool) { + let userDefaultsKey = "showHiddenFiles" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - @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 - } + func getShowHiddenFiles(account: String) -> Bool { + return getBoolPreference(key: "showHiddenFiles", account: account, defaultValue: false) } - @objc func setFileNameType(key: String, prefix: Bool) { - keychain[key] = String(prefix) - } - // MARK: - E2EE func getEndToEndCertificate(account: String) -> String? { @@ -602,12 +475,14 @@ import KeychainAccess } func isEndToEndEnabled(account: String) -> Bool { - let capabilities = NCCapabilities.shared.getCapabilities(account: account) - guard let certificate = getEndToEndCertificate(account: account), !certificate.isEmpty, + guard let capabilities = NCNetworking.shared.capabilities[account], + let certificate = getEndToEndCertificate(account: account), !certificate.isEmpty, let publicKey = getEndToEndPublicKey(account: account), !publicKey.isEmpty, let privateKey = getEndToEndPrivateKey(account: account), !privateKey.isEmpty, let passphrase = getEndToEndPassphrase(account: account), !passphrase.isEmpty, - NCGlobal.shared.e2eeVersions.contains(capabilities.capabilityE2EEApiVersion) else { return false } + NCGlobal.shared.e2eeVersions.contains(capabilities.e2EEApiVersion) else { + return false + } return true } @@ -618,73 +493,62 @@ import KeychainAccess setEndToEndPassphrase(account: account, passphrase: nil) } - // MARK: - PUSHNOTIFICATION + // MARK: - PUSH NOTIFICATION - @objc func getPushNotificationPublicKey(account: String) -> Data? { - let key = "PNPublicKey" + account + func getPushNotificationPrivateKey(account: String) -> Data? { + let key = "PushPrivateKey" + account return try? keychain.getData(key) } - @objc func setPushNotificationPublicKey(account: String, data: Data?) { - let key = "PNPublicKey" + account + func setPushNotificationPrivateKey(account: String, data: Data?) { + let key = "PushPrivateKey" + account keychain[data: key] = data } - @objc func getPushNotificationPrivateKey(account: String) -> Data? { - let key = "PNPrivateKey" + account + func getPushNotificationPublicKey(account: String) -> Data? { + let key = "PushPublicKey" + account return try? keychain.getData(key) } - @objc func setPushNotificationPrivateKey(account: String, data: Data?) { - let key = "PNPrivateKey" + account + func setPushNotificationPublicKey(account: String, data: Data?) { + let key = "PushPublicKey" + account keychain[data: key] = data } - @objc func getPushNotificationSubscribingPublicKey(account: String) -> String? { - let key = "PNSubscribingPublicKey" + account + func getPushNotificationSubscribingPublicKey(account: String) -> String? { + let key = "PushSubscribingPublicKey" + account return try? keychain.get(key) } - @objc func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { - let key = "PNSubscribingPublicKey" + account + func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { + let key = "PushSubscribingPublicKey" + account keychain[key] = publicKey } - @objc func getPushNotificationToken(account: String) -> String? { - let key = "PNToken" + account - return try? keychain.get(key) - } - - @objc func setPushNotificationToken(account: String, token: String?) { - let key = "PNToken" + account - keychain[key] = token - } - - @objc func getPushNotificationDeviceIdentifier(account: String) -> String? { - let key = "PNDeviceIdentifier" + account - return try? keychain.get(key) + func getPushNotificationDeviceIdentifier(account: String) -> String? { + let value = getStringPreference(key: "PushDeviceIdentifier", account: account, defaultValue: "") + return value } - @objc func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { - let key = "PNDeviceIdentifier" + account - keychain[key] = deviceIdentifier + func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { + let userDefaultsKey = "PushDeviceIdentifier" + "_\(account)" + setUserDefaults(deviceIdentifier, forKey: userDefaultsKey) } - @objc func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { - let key = "PNDeviceIdentifierSignature" + account + func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { + let key = "PushDeviceIdentifierSignature" + account return try? keychain.get(key) } - @objc func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { - let key = "PNDeviceIdentifierSignature" + account + func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { + let key = "PushDeviceIdentifierSignature" + account keychain[key] = deviceIdentifierSignature } - @objc func clearAllKeysPushNotification(account: String) { + func clearAllKeysPushNotification(account: String) { + setPushNotificationPrivateKey(account: account, data: nil) setPushNotificationPublicKey(account: account, data: nil) setPushNotificationSubscribingPublicKey(account: account, publicKey: nil) - setPushNotificationPrivateKey(account: account, data: nil) - setPushNotificationToken(account: account, token: nil) setPushNotificationDeviceIdentifier(account: account, deviceIdentifier: nil) setPushNotificationDeviceIdentifierSignature(account: account, deviceIdentifierSignature: nil) } @@ -708,22 +572,13 @@ import KeychainAccess return (data, password) } - - @objc func setAccountName(account: String) { - let key = "AccountName" - keychain[key] = account - } - - @objc func getAccountName() -> String? { - let key = "AccountName" - return try? keychain.get(key) - } // MARK: - Albums func setAutoUploadAlbumIds(account: String, albumIds: [String]) { - let key = "AlbumIds" + account - keychain[key] = albumIds.joined(separator: ",") + let userDefaultsKey = "AlbumIds" + "_\(account)" + let value = albumIds.joined(separator: ",") + setUserDefaults(value, forKey: userDefaultsKey) } func getAutoUploadAlbumIds(account: String) -> [String] { @@ -781,7 +636,69 @@ import KeychainAccess } } - @objc func removeAll() { + func removeAll() { try? keychain.removeAll() } + + private func setUserDefaults(_ value: Any?, forKey key: String) { + let keyPreferences = "Preferences_\(key)" + UserDefaults.standard.set(value, forKey: keyPreferences) + } + + private func getBoolPreference(key: String, account: String? = nil, defaultValue: Bool) -> Bool { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Bool { + return value + } + + if let value = try? keychain.get(keychainKey), let boolValue = Bool(value) { + UserDefaults.standard.set(boolValue, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return boolValue + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } + + private func getStringPreference(key: String, account: String? = nil, defaultValue: String) -> String { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? String { + return value + } + + if let value = try? keychain.get(keychainKey) { + UserDefaults.standard.set(value, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return value + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } + + private func getIntPreference(key: String, account: String? = nil, defaultValue: Int) -> Int { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Int { + return value + } + + if let value = try? keychain.get(keychainKey), let intValue = Int(value) { + UserDefaults.standard.set(intValue, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return intValue + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } } diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 2615c5cad7..0cf1201651 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -5,7 +5,6 @@ import UIKit import MapKit import NextcloudKit -import Alamofire public protocol NCViewerMediaDetailViewDelegate: AnyObject { func downloadFullResolution() @@ -189,7 +188,6 @@ 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 97d333624c..71d7f2768c 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -168,7 +168,7 @@ private var hasChangesQuickLook: Bool = false toolbarConfig.optionButtonFontSize = 16 toolbarConfig.optionButtonFontSizeForPad = 21 toolbarConfig.backgroundColor = .systemGray6 - toolbarConfig.foregroundColor = NCBrandColor.shared.customer + toolbarConfig.foregroundColor = .systemBlue var viewConfig = CropViewConfig() viewConfig.cropMaskVisualEffectType = .none @@ -208,6 +208,16 @@ extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerD return isEditingEnabled ? .createCopy : .disabled // File is in private storage, so .updateContents is not possible and will still act as .createCopy. } +// func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode { +// // Check if the editing mode allows updating the original contents +// if isEditingEnabled { +// hasChangesQuickLook = true // Mark changes if editing is enabled +// return .createCopy // Allows editing and overwriting the original file +// } +// +// return .disabled // Disable editing if not enabled +// } + fileprivate func saveModifiedFile(override: Bool) { guard let metadata = self.metadata else { return @@ -290,6 +300,23 @@ extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerD guard utilityFileSystem.copyFile(atPath: modifiedContentsURL.path, toPath: url.path) else { return } hasChangesQuickLook = true } + +// func previewController(_ controller: QLPreviewController, didSaveEditedContentsOf previewItem: QLPreviewItem, at modifiedURL: URL) { +// // This method is called if the user saves a *new* copy of the edited file. +// print("Content was saved to a new URL: \(modifiedURL)") +// hasChangesQuickLook = true +// // You might need to update your internal fileURL reference here if you want to use the new file. +// } +// +// func previewController(_ controller: QLPreviewController, didUpdateContentsOf previewItem: QLPreviewItem) { +// // Check if the file contents have actually been updated +// if let fileURL = previewItem.previewItemURL { +// // Custom logic to check if contents have been modified +// print("File contents updated at: \(fileURL)") +// hasChangesQuickLook = true // Mark as changed +// } +// } + } extension NCViewerQuickLook: CropViewControllerDelegate { From 694d29c2e54553eb15d7412cbcb6a707ccbafd98 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 5/9] NMC 2169 - Media theming customisation changes --- iOSClient/AppDelegate.swift | 3 + .../Data/NCManageDatabase+Metadata.swift | 48 ++++++++++---- iOSClient/Media/NCMedia.storyboard | 64 +++++++++++++------ iOSClient/NCImageCache.swift | 13 ++++ .../NCViewerMediaDetailView.swift | 1 + .../NCViewerQuickLook/NCViewerQuickLook.swift | 2 +- 6 files changed, 99 insertions(+), 32 deletions(-) diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 8ceda5f386..729e83113e 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -51,6 +51,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD await 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 989e27ca1d..f3f49c210c 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -1431,17 +1431,41 @@ extension NCManageDatabase { } ?? false } - func getMetadataDirectoryAsync(serverUrl: String, account: String) async -> tableMetadata? { - guard let url = URL(string: serverUrl) else { - return nil - } - let fileName = url.lastPathComponent - var baseUrl = url.deletingLastPathComponent().absoluteString - if baseUrl.hasSuffix("/") { - baseUrl.removeLast() - } - guard let decodedBaseUrl = baseUrl.removingPercentEncoding else { - return nil + func createMetadatasFolder(assets: [PHAsset], + useSubFolder: Bool, + session: NCSession.Session, completion: @escaping ([tableMetadata]) -> Void) { + var foldersCreated: Set = [] + var metadatas: [tableMetadata] = [] + let serverUrlBase = getAccountAutoUploadDirectory(session: session) + let fileNameBase = getAccountAutoUploadFileName(account: session.account) + let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, serverUrlBase) + + func createMetadata(serverUrl: String, fileName: String, metadata: tableMetadata?) { + guard !foldersCreated.contains(serverUrl + "/" + fileName) else { + return + } + foldersCreated.insert(serverUrl + "/" + fileName) + + if let metadata { + metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder + metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload + metadata.sessionDate = Date() + metadatas.append(tableMetadata(value: metadata)) + } else { + let metadata = NCManageDatabase.shared.createMetadata(fileName: fileName, + fileNameView: fileName, + ocId: NSUUID().uuidString, + serverUrl: serverUrl, + url: "", + contentType: "httpd/unix-directory", + directory: true, + session: session, + sceneIdentifier: nil) + metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder + metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload + metadata.sessionDate = Date() + metadatas.append(metadata) + } } return await core.performRealmReadAsync { realm in @@ -1451,7 +1475,7 @@ extension NCManageDatabase { return object?.detachedCopy() } } - + func getMetadataDirectory(serverUrl: String, account: String) -> tableMetadata? { guard let url = URL(string: serverUrl) else { return nil diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index 5733a680ee..42e2f62040 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -4,7 +4,6 @@ - @@ -18,7 +17,7 @@ - + @@ -31,41 +30,73 @@ - + - + + + + - - - - + + + + + + + + + - - - + + + + @@ -77,9 +108,4 @@ - - - - - diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index 7d07aa5a31..ba4daa6fad 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -99,6 +99,19 @@ final class NCImageCache: @unchecked Sendable { return NCManageDatabase.shared.getMediaMetadatas(predicate: predicate ?? predicateBoth, sorted: "date") } + 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/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 0cf1201651..9c2c44cc3b 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -188,6 +188,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 71d7f2768c..e84a751ef5 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -168,7 +168,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 ed265c1a43d583f3697697c89c598b598b27580d Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Mon, 14 Apr 2025 11:39:24 +0530 Subject: [PATCH 6/9] NMC 2169 - Media theming changes --- .../Data/NCManageDatabase+Metadata.swift | 26 +- iOSClient/Settings/NCPreferences.swift | 506 +++++++++++------- 2 files changed, 331 insertions(+), 201 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index f3f49c210c..20bb11d7b9 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -177,6 +177,10 @@ extension tableMetadata { var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } + + var isDocumentViewableOnly: Bool { + sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKCommon.TypeClassFile.document.rawValue + } var isDocumentViewableOnly: Bool { sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue @@ -207,7 +211,7 @@ extension tableMetadata { } var isCopyableInPasteboard: Bool { - !directory + !isDocumentViewableOnly && !directory } #if !EXTENSION_FILE_PROVIDER_EXTENSION @@ -216,11 +220,11 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDirectoryE2EE && !e2eEncrypted + !isDocumentViewableOnly && !isDirectoryE2EE && !e2eEncrypted } var isModifiableWithQuickLook: Bool { - if directory || isDirectoryE2EE { + if directory || isDocumentViewableOnly || isDirectoryE2EE { return false } return isPDF || isImage @@ -251,7 +255,8 @@ extension tableMetadata { } var canSetAsAvailableOffline: Bool { - return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted +// return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted + return session.isEmpty && !isDocumentViewableOnly } func isSharable() -> Bool { @@ -1317,12 +1322,23 @@ extension NCManageDatabase { counter += 1 listIdentifierRank[item.ocId] = NSNumber(value: counter) } - return listIdentifierRank } return result ?? [:] } + @objc func clearMetadatasUpload(account: String) { + do { + let realm = try Realm() + try realm.write { + let results = realm.objects(tableMetadata.self).filter("account == %@ AND (status == %d OR status == %d)", account, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploadError) + realm.delete(results) + } + } catch let error { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + func getAssetLocalIdentifiersUploadedAsync() async -> [String]? { return await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 96ce2c79ee..e2d07c720b 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -1,44 +1,72 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2023 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later +// +// NCKeychain.swift +// Nextcloud +// +// Created by Marino Faggiana on 23/10/23. +// Copyright © 2023 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 UIKit import KeychainAccess import NextcloudKit -final class NCPreferences: NSObject { +@objc class NCKeychain: NSObject { let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { get { - return getBoolPreference(key: "showDescription", defaultValue: true) + if let value = try? keychain.get("showDescription"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "showDescription") + keychain["showDescription"] = String(newValue) } } var showRecommendedFiles: Bool { get { - return getBoolPreference(key: "showRecommendedFiles", defaultValue: true) + if let value = try? keychain.get("showRecommendedFiles"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "showRecommendedFiles") + keychain["showRecommendedFiles"] = String(newValue) } } var typeFilterScanDocument: NCGlobal.TypeFilterScanDocument { get { - let rawValue = getStringPreference(key: "ScanDocumentTypeFilter", defaultValue: NCGlobal.TypeFilterScanDocument.original.rawValue) - return NCGlobal.TypeFilterScanDocument(rawValue: rawValue) ?? .original + if let rawValue = try? keychain.get("ScanDocumentTypeFilter"), let value = NCGlobal.TypeFilterScanDocument(rawValue: rawValue) { + return value + } else { + return .original + } } set { - setUserDefaults(newValue.rawValue, forKey: "ScanDocumentTypeFilter") + keychain["ScanDocumentTypeFilter"] = newValue.rawValue } } - var passcode: String? { + @objc var passcode: String? { get { migrate(key: "passcodeBlock") if let value = try? keychain.get("passcodeBlock"), !value.isEmpty { @@ -51,7 +79,7 @@ final class NCPreferences: NSObject { } } - var resetAppCounterFail: Bool { + @objc var resetAppCounterFail: Bool { get { if let value = try? keychain.get("resetAppCounterFail"), let result = Bool(value) { return result @@ -87,7 +115,7 @@ final class NCPreferences: NSObject { } } - var requestPasscodeAtStart: Bool { + @objc var requestPasscodeAtStart: Bool { get { let keychainOLD = Keychain(service: "Crypto Cloud") if let value = keychainOLD["notPasscodeAtStart"], !value.isEmpty { @@ -110,7 +138,7 @@ final class NCPreferences: NSObject { } } - var touchFaceID: Bool { + @objc var touchFaceID: Bool { get { migrate(key: "enableTouchFaceID") if let value = try? keychain.get("enableTouchFaceID"), let result = Bool(value) { @@ -127,101 +155,151 @@ final class NCPreferences: NSObject { return passcode != nil && requestPasscodeAtStart } - var incrementalNumber: String { + @objc var incrementalNumber: String { + migrate(key: "incrementalnumber") var incrementalString = String(format: "%04ld", 0) - let value = getStringPreference(key: "incrementalnumber", defaultValue: incrementalString) - if var intValue = Int(value) { - intValue += 1 - incrementalString = String(format: "%04ld", intValue) + if let value = try? keychain.get("incrementalnumber"), var result = Int(value) { + result += 1 + incrementalString = String(format: "%04ld", result) } - setUserDefaults(incrementalString, forKey: "incrementalnumber") + keychain["incrementalnumber"] = incrementalString return incrementalString } - var formatCompatibility: Bool { + @objc var showHiddenFiles: Bool { get { - return getBoolPreference(key: "formatCompatibility", defaultValue: true) + migrate(key: "showHiddenFiles") + if let value = try? keychain.get("showHiddenFiles"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "formatCompatibility") + keychain["showHiddenFiles"] = String(newValue) } } - var disableFilesApp: Bool { + @objc var formatCompatibility: Bool { get { - return getBoolPreference(key: "disablefilesapp", defaultValue: false) + migrate(key: "formatCompatibility") + if let value = try? keychain.get("formatCompatibility"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "disablefilesapp") + keychain["formatCompatibility"] = String(newValue) } } - var livePhoto: Bool { + @objc var disableFilesApp: Bool { get { - return getBoolPreference(key: "livePhoto", defaultValue: true) + migrate(key: "disablefilesapp") + if let value = try? keychain.get("disablefilesapp"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "livePhoto") + keychain["disablefilesapp"] = String(newValue) } } - var disableCrashservice: Bool { + @objc var livePhoto: Bool { get { - return getBoolPreference(key: "crashservice", defaultValue: false) + migrate(key: "livePhoto") + if let value = try? keychain.get("livePhoto"), let result = Bool(value) { + return result + } + return true + } + set { + keychain["livePhoto"] = String(newValue) + } + } + + @objc var disableCrashservice: Bool { + get { + migrate(key: "crashservice") + if let value = try? keychain.get("crashservice"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "crashservice") + keychain["crashservice"] = String(newValue) } } /// Stores and retrieves the current log level from the keychain. var log: NKLogLevel { +// @objc var logLevel: Int { get { - let value = getIntPreference(key: "logLevel", defaultValue: NKLogLevel.normal.rawValue) - return NKLogLevel(rawValue: value) ?? NKLogLevel.normal + migrate(key: "logLevel") + if let value = try? keychain.get("logLevel"), + let intValue = Int(value), + let level = NKLogLevel(rawValue: intValue) { + return level + } + return NKLogLevel.normal } set { - setUserDefaults(newValue.rawValue, forKey: "logLevel") + keychain["logLevel"] = String(newValue.rawValue) } } - var accountRequest: Bool { + @objc var accountRequest: Bool { get { - return getBoolPreference(key: "accountRequest", defaultValue: false) + migrate(key: "accountRequest") + if let value = try? keychain.get("accountRequest"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "accountRequest") + keychain["accountRequest"] = String(newValue) } } - var removePhotoCameraRoll: Bool { + @objc var removePhotoCameraRoll: Bool { get { - return getBoolPreference(key: "removePhotoCameraRoll", defaultValue: false) + migrate(key: "removePhotoCameraRoll") + if let value = try? keychain.get("removePhotoCameraRoll"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "removePhotoCameraRoll") + keychain["removePhotoCameraRoll"] = String(newValue) } } - var privacyScreenEnabled: Bool { + @objc var privacyScreenEnabled: Bool { get { + migrate(key: "privacyScreen") if NCBrandOptions.shared.enforce_privacyScreenEnabled { return true } - return getBoolPreference(key: "privacyScreen", defaultValue: false) + if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "privacyScreen") + keychain["privacyScreen"] = String(newValue) } } - var cleanUpDay: Int { + @objc var cleanUpDay: Int { get { - let value = getIntPreference(key: "cleanUpDay", defaultValue: NCBrandOptions.shared.cleanUpDay) - return value + migrate(key: "cleanUpDay") + if let value = try? keychain.get("cleanUpDay"), let result = Int(value) { + return result + } + return NCBrandOptions.shared.cleanUpDay } set { - setUserDefaults(newValue, forKey: "cleanUpDay") + keychain["cleanUpDay"] = String(newValue) } } @@ -257,124 +335,144 @@ final class NCPreferences: NSObject { var textRecognitionStatus: Bool { get { - return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) + migrate(key: "textRecognitionStatus") + if let value = try? keychain.get("textRecognitionStatus"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "textRecognitionStatus") + keychain["textRecognitionStatus"] = String(newValue) } } var deleteAllScanImages: Bool { get { - return getBoolPreference(key: "deleteAllScanImages", defaultValue: false) + migrate(key: "deleteAllScanImages") + if let value = try? keychain.get("deleteAllScanImages"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "deleteAllScanImages") + keychain["deleteAllScanImages"] = String(newValue) } } var qualityScanDocument: Double { get { - let value = getIntPreference(key: "qualityScanDocument", defaultValue: 2) - return Double(value) + migrate(key: "qualityScanDocument") + if let value = try? keychain.get("qualityScanDocument"), let result = Double(value) { + return result + } + return 2 } set { - setUserDefaults(newValue, forKey: "qualityScanDocument") + keychain["qualityScanDocument"] = String(newValue) } } var appearanceAutomatic: Bool { get { - let value = getBoolPreference(key: "appearanceAutomatic", defaultValue: true) - return value + if let value = try? keychain.get("appearanceAutomatic"), let result = Bool(value) { + return result + } + return true } set { - setUserDefaults(newValue, forKey: "appearanceAutomatic") + keychain["appearanceAutomatic"] = String(newValue) } } var appearanceInterfaceStyle: UIUserInterfaceStyle { get { - let value = getStringPreference(key: "appearanceInterfaceStyle", defaultValue: "light") - if value == "light" { - return .light - } else { - return .dark + if let value = try? keychain.get("appearanceInterfaceStyle") { + if value == "light" { + return .light + } else { + return .dark + } } + return .light } set { if newValue == .light { - setUserDefaults("light", forKey: "appearanceInterfaceStyle") + keychain["appearanceInterfaceStyle"] = "light" } else { - setUserDefaults("dark", forKey: "appearanceInterfaceStyle") + keychain["appearanceInterfaceStyle"] = "dark" } } } var screenAwakeMode: AwakeMode { get { - let value = getStringPreference(key: "screenAwakeMode", defaultValue: "off") - if value == "off" { - return .off - } else if value == "on" { - return .on - } else { - return .whileCharging + if let value = try? keychain.get("screenAwakeMode") { + if value == "off" { + return .off + } else if value == "on" { + return .on + } else { + return .whileCharging + } } + return .off } set { if newValue == .off { - setUserDefaults("off", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "off" } else if newValue == .on { - setUserDefaults("on", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "on" } else { - setUserDefaults("whileCharging", forKey: "screenAwakeMode") + keychain["screenAwakeMode"] = "whileCharging" } } } var fileNameType: Bool { get { - return getBoolPreference(key: "fileNameType", defaultValue: false) + if let value = try? keychain.get("fileNameType"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "fileNameType") + keychain["fileNameType"] = String(newValue) } } var fileNameOriginal: Bool { get { - return getBoolPreference(key: "fileNameOriginal", defaultValue: false) + if let value = try? keychain.get("fileNameOriginal"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "fileNameOriginal") + keychain["fileNameOriginal"] = String(newValue) } } var fileNameMask: String { get { - return getStringPreference(key: "fileNameMask", defaultValue: "") + if let value = try? keychain.get("fileNameMask") { + return value + } + return "" } set { - setUserDefaults(newValue, forKey: "fileNameMask") + keychain["fileNameMask"] = String(newValue) } } var location: Bool { get { - return getBoolPreference(key: "location", defaultValue: false) - } - set { - setUserDefaults(newValue, forKey: "location") - } - } - - var deviceTokenPushNotification: String { - get { - return getStringPreference(key: "deviceTokenPushNotification", defaultValue: "") + if let value = try? keychain.get("location"), let result = Bool(value) { + return result + } + return false } set { - setUserDefaults(newValue, forKey: "deviceTokenPushNotification") + keychain["location"] = String(newValue) } } @@ -393,41 +491,109 @@ final class NCPreferences: NSObject { } func setPersonalFilesOnly(account: String, value: Bool) { - let userDefaultsKey = "personalfilesonly" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "personalFilesOnly" + account + keychain[key] = String(value) } func getPersonalFilesOnly(account: String) -> Bool { - return getBoolPreference(key: "personalfilesonly", account: account, defaultValue: false) + let key = "personalFilesOnly" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return false + } } func setFavoriteOnTop(account: String, value: Bool) { - let userDefaultsKey = "favoriteOnTop" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "favoriteOnTop" + account + keychain[key] = String(value) } func getFavoriteOnTop(account: String) -> Bool { - return getBoolPreference(key: "favoriteOnTop", account: account, defaultValue: true) + let key = "favoriteOnTop" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return true + } } func setDirectoryOnTop(account: String, value: Bool) { - let userDefaultsKey = "directoryOnTop" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "directoryOnTop" + account + keychain[key] = String(value) } func getDirectoryOnTop(account: String) -> Bool { - return getBoolPreference(key: "directoryOnTop", account: account, defaultValue: true) + let key = "directoryOnTop" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return true + } } func setShowHiddenFiles(account: String, value: Bool) { - let userDefaultsKey = "showHiddenFiles" + "_\(account)" - setUserDefaults(value, forKey: userDefaultsKey) + let key = "showHiddenFiles" + account + keychain[key] = String(value) } func getShowHiddenFiles(account: String) -> Bool { - return getBoolPreference(key: "showHiddenFiles", account: account, defaultValue: false) + let key = "showHiddenFiles" + account + if let value = try? keychain.get(key), let result = Bool(value) { + return result + } else { + return false + } } + 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? { @@ -493,62 +659,73 @@ final class NCPreferences: NSObject { setEndToEndPassphrase(account: account, passphrase: nil) } - // MARK: - PUSH NOTIFICATION + // MARK: - PUSHNOTIFICATION - func getPushNotificationPrivateKey(account: String) -> Data? { - let key = "PushPrivateKey" + account + @objc func getPushNotificationPublicKey(account: String) -> Data? { + let key = "PNPublicKey" + account return try? keychain.getData(key) } - func setPushNotificationPrivateKey(account: String, data: Data?) { - let key = "PushPrivateKey" + account + @objc func setPushNotificationPublicKey(account: String, data: Data?) { + let key = "PNPublicKey" + account keychain[data: key] = data } - func getPushNotificationPublicKey(account: String) -> Data? { - let key = "PushPublicKey" + account + @objc func getPushNotificationPrivateKey(account: String) -> Data? { + let key = "PNPrivateKey" + account return try? keychain.getData(key) } - func setPushNotificationPublicKey(account: String, data: Data?) { - let key = "PushPublicKey" + account + @objc func setPushNotificationPrivateKey(account: String, data: Data?) { + let key = "PNPrivateKey" + account keychain[data: key] = data } - func getPushNotificationSubscribingPublicKey(account: String) -> String? { - let key = "PushSubscribingPublicKey" + account + @objc func getPushNotificationSubscribingPublicKey(account: String) -> String? { + let key = "PNSubscribingPublicKey" + account return try? keychain.get(key) } - func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { - let key = "PushSubscribingPublicKey" + account + @objc func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { + let key = "PNSubscribingPublicKey" + account keychain[key] = publicKey } - func getPushNotificationDeviceIdentifier(account: String) -> String? { - let value = getStringPreference(key: "PushDeviceIdentifier", account: account, defaultValue: "") - return value + @objc func getPushNotificationToken(account: String) -> String? { + let key = "PNToken" + account + return try? keychain.get(key) + } + + @objc func setPushNotificationToken(account: String, token: String?) { + let key = "PNToken" + account + keychain[key] = token + } + + @objc func getPushNotificationDeviceIdentifier(account: String) -> String? { + let key = "PNDeviceIdentifier" + account + return try? keychain.get(key) } - func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { - let userDefaultsKey = "PushDeviceIdentifier" + "_\(account)" - setUserDefaults(deviceIdentifier, forKey: userDefaultsKey) + @objc func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { + let key = "PNDeviceIdentifier" + account + keychain[key] = deviceIdentifier } - func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { - let key = "PushDeviceIdentifierSignature" + account + @objc func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { + let key = "PNDeviceIdentifierSignature" + account return try? keychain.get(key) } - func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { - let key = "PushDeviceIdentifierSignature" + account + @objc func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { + let key = "PNDeviceIdentifierSignature" + account keychain[key] = deviceIdentifierSignature } - func clearAllKeysPushNotification(account: String) { - setPushNotificationPrivateKey(account: account, data: nil) + @objc func clearAllKeysPushNotification(account: String) { setPushNotificationPublicKey(account: account, data: nil) setPushNotificationSubscribingPublicKey(account: account, publicKey: nil) + setPushNotificationPrivateKey(account: account, data: nil) + setPushNotificationToken(account: account, token: nil) setPushNotificationDeviceIdentifier(account: account, deviceIdentifier: nil) setPushNotificationDeviceIdentifierSignature(account: account, deviceIdentifierSignature: nil) } @@ -576,9 +753,8 @@ final class NCPreferences: NSObject { // MARK: - Albums func setAutoUploadAlbumIds(account: String, albumIds: [String]) { - let userDefaultsKey = "AlbumIds" + "_\(account)" - let value = albumIds.joined(separator: ",") - setUserDefaults(value, forKey: userDefaultsKey) + let key = "AlbumIds" + account + keychain[key] = albumIds.joined(separator: ",") } func getAutoUploadAlbumIds(account: String) -> [String] { @@ -636,69 +812,7 @@ final class NCPreferences: NSObject { } } - func removeAll() { + @objc func removeAll() { try? keychain.removeAll() } - - private func setUserDefaults(_ value: Any?, forKey key: String) { - let keyPreferences = "Preferences_\(key)" - UserDefaults.standard.set(value, forKey: keyPreferences) - } - - private func getBoolPreference(key: String, account: String? = nil, defaultValue: Bool) -> Bool { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Bool { - return value - } - - if let value = try? keychain.get(keychainKey), let boolValue = Bool(value) { - UserDefaults.standard.set(boolValue, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return boolValue - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } - - private func getStringPreference(key: String, account: String? = nil, defaultValue: String) -> String { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? String { - return value - } - - if let value = try? keychain.get(keychainKey) { - UserDefaults.standard.set(value, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return value - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } - - private func getIntPreference(key: String, account: String? = nil, defaultValue: Int) -> Int { - let suffix = account ?? "" - let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" - let keychainKey = account != nil ? "\(key)\(suffix)" : key - - if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Int { - return value - } - - if let value = try? keychain.get(keychainKey), let intValue = Int(value) { - UserDefaults.standard.set(intValue, forKey: userDefaultsKey) - try? keychain.remove(keychainKey) - return intValue - } - - UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) - return defaultValue - } } From 7dfaafe77853218f644ea496cab4409851781c96 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Mon, 6 Oct 2025 11:07:18 +0530 Subject: [PATCH 7/9] NMC 2169 - Media theming customisation changes --- iOSClient/Settings/NCPreferences.swift | 76 +++++-------------- .../NCViewerMediaDetailView.swift | 1 + 2 files changed, 21 insertions(+), 56 deletions(-) diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index e2d07c720b..81b18e71ad 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -24,9 +24,9 @@ import Foundation import UIKit import KeychainAccess -import NextcloudKit @objc class NCKeychain: NSObject { + let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { @@ -231,20 +231,16 @@ import NextcloudKit } } - /// Stores and retrieves the current log level from the keychain. - var log: NKLogLevel { -// @objc var logLevel: Int { + @objc var logLevel: Int { get { migrate(key: "logLevel") - if let value = try? keychain.get("logLevel"), - let intValue = Int(value), - let level = NKLogLevel(rawValue: intValue) { - return level + if let value = try? keychain.get("logLevel"), let result = Int(value) { + return result } - return NKLogLevel.normal + return 1 } set { - keychain["logLevel"] = String(newValue.rawValue) + keychain["logLevel"] = String(newValue) } } @@ -277,9 +273,6 @@ import NextcloudKit @objc var privacyScreenEnabled: Bool { get { migrate(key: "privacyScreen") - if NCBrandOptions.shared.enforce_privacyScreenEnabled { - return true - } if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { return result } @@ -464,25 +457,12 @@ import NextcloudKit } } - var location: Bool { - get { - if let value = try? keychain.get("location"), let result = Bool(value) { - return result - } - return false - } - set { - keychain["location"] = String(newValue) - } - } - // MARK: - - func getPassword(account: String) -> String { + @objc func getPassword(account: String) -> String { let key = "password" + account migrate(key: key) - let password = (try? keychain.get(key)) ?? "" - return password + return (try? keychain.get(key)) ?? "" } func setPassword(account: String, password: String?) { @@ -504,20 +484,7 @@ import NextcloudKit } } - func setFavoriteOnTop(account: String, value: Bool) { - let key = "favoriteOnTop" + account - keychain[key] = String(value) - } - - func getFavoriteOnTop(account: String) -> Bool { - let key = "favoriteOnTop" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return true - } - } - + /* OBSOLETE func setDirectoryOnTop(account: String, value: Bool) { let key = "directoryOnTop" + account keychain[key] = String(value) @@ -531,20 +498,7 @@ import NextcloudKit return true } } - - func setShowHiddenFiles(account: String, value: Bool) { - let key = "showHiddenFiles" + account - keychain[key] = String(value) - } - - func getShowHiddenFiles(account: String) -> Bool { - let key = "showHiddenFiles" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return false - } - } + */ func setTitleButtonHeader(account: String, value: String?) { let key = "titleButtonHeader" + account @@ -749,6 +703,16 @@ import NextcloudKit return (data, password) } + + @objc func setAccountName(account: String) { + let key = "AccountName" + keychain[key] = account + } + + @objc func getAccountName() -> String? { + let key = "AccountName" + return try? keychain.get(key) + } // MARK: - Albums diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 9c2c44cc3b..2615c5cad7 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -5,6 +5,7 @@ import UIKit import MapKit import NextcloudKit +import Alamofire public protocol NCViewerMediaDetailViewDelegate: AnyObject { func downloadFullResolution() From 87918385fd623de2a2a140e86c1f01c1c5452c83 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 16 Dec 2025 17:23:54 +0530 Subject: [PATCH 8/9] NMC 2169 - Media theming customisation changes --- iOSClient/AppDelegate.swift | 3 - .../Data/NCManageDatabase+Metadata.swift | 73 +-- iOSClient/Media/NCMedia.storyboard | 64 +-- iOSClient/Settings/NCPreferences.swift | 512 ++++++++---------- .../NCViewerMediaDetailView.swift | 2 - .../NCViewerQuickLook/NCViewerQuickLook.swift | 2 +- 6 files changed, 253 insertions(+), 403 deletions(-) diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 729e83113e..8ceda5f386 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -51,9 +51,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD await 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 20bb11d7b9..105c39d883 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -177,10 +177,6 @@ extension tableMetadata { var isSavebleInCameraRoll: Bool { return (classFile == NKTypeClassFile.image.rawValue && contentType != "image/svg+xml") || classFile == NKTypeClassFile.video.rawValue } - - var isDocumentViewableOnly: Bool { - sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKCommon.TypeClassFile.document.rawValue - } var isDocumentViewableOnly: Bool { sharePermissionsCollaborationServices == NCPermissions().permissionReadShare && classFile == NKTypeClassFile.document.rawValue @@ -211,7 +207,7 @@ extension tableMetadata { } var isCopyableInPasteboard: Bool { - !isDocumentViewableOnly && !directory + !directory } #if !EXTENSION_FILE_PROVIDER_EXTENSION @@ -220,11 +216,11 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDocumentViewableOnly && !isDirectoryE2EE && !e2eEncrypted + !isDirectoryE2EE && !e2eEncrypted } var isModifiableWithQuickLook: Bool { - if directory || isDocumentViewableOnly || isDirectoryE2EE { + if directory || isDirectoryE2EE { return false } return isPDF || isImage @@ -255,8 +251,7 @@ extension tableMetadata { } var canSetAsAvailableOffline: Bool { -// return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted - return session.isEmpty && !isDocumentViewableOnly + return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted } func isSharable() -> Bool { @@ -1327,18 +1322,6 @@ extension NCManageDatabase { return result ?? [:] } - @objc func clearMetadatasUpload(account: String) { - do { - let realm = try Realm() - try realm.write { - let results = realm.objects(tableMetadata.self).filter("account == %@ AND (status == %d OR status == %d)", account, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploadError) - realm.delete(results) - } - } catch let error { - NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") - } - } - func getAssetLocalIdentifiersUploadedAsync() async -> [String]? { return await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") @@ -1447,41 +1430,17 @@ extension NCManageDatabase { } ?? false } - func createMetadatasFolder(assets: [PHAsset], - useSubFolder: Bool, - session: NCSession.Session, completion: @escaping ([tableMetadata]) -> Void) { - var foldersCreated: Set = [] - var metadatas: [tableMetadata] = [] - let serverUrlBase = getAccountAutoUploadDirectory(session: session) - let fileNameBase = getAccountAutoUploadFileName(account: session.account) - let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND directory == true", session.account, serverUrlBase) - - func createMetadata(serverUrl: String, fileName: String, metadata: tableMetadata?) { - guard !foldersCreated.contains(serverUrl + "/" + fileName) else { - return - } - foldersCreated.insert(serverUrl + "/" + fileName) - - if let metadata { - metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder - metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload - metadata.sessionDate = Date() - metadatas.append(tableMetadata(value: metadata)) - } else { - let metadata = NCManageDatabase.shared.createMetadata(fileName: fileName, - fileNameView: fileName, - ocId: NSUUID().uuidString, - serverUrl: serverUrl, - url: "", - contentType: "httpd/unix-directory", - directory: true, - session: session, - sceneIdentifier: nil) - metadata.status = NCGlobal.shared.metadataStatusWaitCreateFolder - metadata.sessionSelector = NCGlobal.shared.selectorUploadAutoUpload - metadata.sessionDate = Date() - metadatas.append(metadata) - } + func getMetadataDirectoryAsync(serverUrl: String, account: String) async -> tableMetadata? { + guard let url = URL(string: serverUrl) else { + return nil + } + let fileName = url.lastPathComponent + var baseUrl = url.deletingLastPathComponent().absoluteString + if baseUrl.hasSuffix("/") { + baseUrl.removeLast() + } + guard let decodedBaseUrl = baseUrl.removingPercentEncoding else { + return nil } return await core.performRealmReadAsync { realm in @@ -1491,7 +1450,7 @@ extension NCManageDatabase { return object?.detachedCopy() } } - + func getMetadataDirectory(serverUrl: String, account: String) -> tableMetadata? { guard let url = URL(string: serverUrl) else { return nil diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index 42e2f62040..5733a680ee 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -4,6 +4,7 @@ + @@ -17,7 +18,7 @@ - + @@ -30,73 +31,41 @@ - + - + - - - - - - - + + + + - - - - - - - - + + + - @@ -108,4 +77,9 @@ + + + + + diff --git a/iOSClient/Settings/NCPreferences.swift b/iOSClient/Settings/NCPreferences.swift index 81b18e71ad..96ce2c79ee 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -1,72 +1,44 @@ -// -// NCKeychain.swift -// Nextcloud -// -// Created by Marino Faggiana on 23/10/23. -// Copyright © 2023 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 . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2023 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit import KeychainAccess +import NextcloudKit -@objc class NCKeychain: NSObject { - +final class NCPreferences: NSObject { let keychain = Keychain(service: "com.nextcloud.keychain") var showDescription: Bool { get { - if let value = try? keychain.get("showDescription"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "showDescription", defaultValue: true) } set { - keychain["showDescription"] = String(newValue) + setUserDefaults(newValue, forKey: "showDescription") } } var showRecommendedFiles: Bool { get { - if let value = try? keychain.get("showRecommendedFiles"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "showRecommendedFiles", defaultValue: true) } set { - keychain["showRecommendedFiles"] = String(newValue) + setUserDefaults(newValue, forKey: "showRecommendedFiles") } } var typeFilterScanDocument: NCGlobal.TypeFilterScanDocument { get { - if let rawValue = try? keychain.get("ScanDocumentTypeFilter"), let value = NCGlobal.TypeFilterScanDocument(rawValue: rawValue) { - return value - } else { - return .original - } + let rawValue = getStringPreference(key: "ScanDocumentTypeFilter", defaultValue: NCGlobal.TypeFilterScanDocument.original.rawValue) + return NCGlobal.TypeFilterScanDocument(rawValue: rawValue) ?? .original } set { - keychain["ScanDocumentTypeFilter"] = newValue.rawValue + setUserDefaults(newValue.rawValue, forKey: "ScanDocumentTypeFilter") } } - @objc var passcode: String? { + var passcode: String? { get { migrate(key: "passcodeBlock") if let value = try? keychain.get("passcodeBlock"), !value.isEmpty { @@ -79,7 +51,7 @@ import KeychainAccess } } - @objc var resetAppCounterFail: Bool { + var resetAppCounterFail: Bool { get { if let value = try? keychain.get("resetAppCounterFail"), let result = Bool(value) { return result @@ -115,7 +87,7 @@ import KeychainAccess } } - @objc var requestPasscodeAtStart: Bool { + var requestPasscodeAtStart: Bool { get { let keychainOLD = Keychain(service: "Crypto Cloud") if let value = keychainOLD["notPasscodeAtStart"], !value.isEmpty { @@ -138,7 +110,7 @@ import KeychainAccess } } - @objc var touchFaceID: Bool { + var touchFaceID: Bool { get { migrate(key: "enableTouchFaceID") if let value = try? keychain.get("enableTouchFaceID"), let result = Bool(value) { @@ -155,144 +127,101 @@ import KeychainAccess return passcode != nil && requestPasscodeAtStart } - @objc var incrementalNumber: String { - migrate(key: "incrementalnumber") + var incrementalNumber: String { var incrementalString = String(format: "%04ld", 0) - if let value = try? keychain.get("incrementalnumber"), var result = Int(value) { - result += 1 - incrementalString = String(format: "%04ld", result) + let value = getStringPreference(key: "incrementalnumber", defaultValue: incrementalString) + if var intValue = Int(value) { + intValue += 1 + incrementalString = String(format: "%04ld", intValue) } - keychain["incrementalnumber"] = incrementalString + setUserDefaults(incrementalString, forKey: "incrementalnumber") return incrementalString } - @objc var showHiddenFiles: Bool { + var formatCompatibility: Bool { get { - migrate(key: "showHiddenFiles") - if let value = try? keychain.get("showHiddenFiles"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "formatCompatibility", defaultValue: true) } set { - keychain["showHiddenFiles"] = String(newValue) + setUserDefaults(newValue, forKey: "formatCompatibility") } } - @objc var formatCompatibility: Bool { + var disableFilesApp: Bool { get { - migrate(key: "formatCompatibility") - if let value = try? keychain.get("formatCompatibility"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "disablefilesapp", defaultValue: false) } set { - keychain["formatCompatibility"] = String(newValue) + setUserDefaults(newValue, forKey: "disablefilesapp") } } - @objc var disableFilesApp: Bool { + var livePhoto: Bool { get { - migrate(key: "disablefilesapp") - if let value = try? keychain.get("disablefilesapp"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "livePhoto", defaultValue: true) } set { - keychain["disablefilesapp"] = String(newValue) + setUserDefaults(newValue, forKey: "livePhoto") } } - @objc var livePhoto: Bool { + var disableCrashservice: Bool { get { - migrate(key: "livePhoto") - if let value = try? keychain.get("livePhoto"), let result = Bool(value) { - return result - } - return true + return getBoolPreference(key: "crashservice", defaultValue: false) } set { - keychain["livePhoto"] = String(newValue) + setUserDefaults(newValue, forKey: "crashservice") } } - @objc var disableCrashservice: Bool { + /// Stores and retrieves the current log level from the keychain. + var log: NKLogLevel { get { - migrate(key: "crashservice") - if let value = try? keychain.get("crashservice"), let result = Bool(value) { - return result - } - return false + let value = getIntPreference(key: "logLevel", defaultValue: NKLogLevel.normal.rawValue) + return NKLogLevel(rawValue: value) ?? NKLogLevel.normal } set { - keychain["crashservice"] = String(newValue) + setUserDefaults(newValue.rawValue, forKey: "logLevel") } } - @objc var logLevel: Int { + var accountRequest: Bool { get { - migrate(key: "logLevel") - if let value = try? keychain.get("logLevel"), let result = Int(value) { - return result - } - return 1 + return getBoolPreference(key: "accountRequest", defaultValue: false) } set { - keychain["logLevel"] = String(newValue) + setUserDefaults(newValue, forKey: "accountRequest") } } - @objc var accountRequest: Bool { + var removePhotoCameraRoll: Bool { get { - migrate(key: "accountRequest") - if let value = try? keychain.get("accountRequest"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "removePhotoCameraRoll", defaultValue: false) } set { - keychain["accountRequest"] = String(newValue) + setUserDefaults(newValue, forKey: "removePhotoCameraRoll") } } - @objc var removePhotoCameraRoll: Bool { + var privacyScreenEnabled: Bool { get { - migrate(key: "removePhotoCameraRoll") - if let value = try? keychain.get("removePhotoCameraRoll"), let result = Bool(value) { - return result + if NCBrandOptions.shared.enforce_privacyScreenEnabled { + return true } - return false + return getBoolPreference(key: "privacyScreen", defaultValue: false) } set { - keychain["removePhotoCameraRoll"] = String(newValue) + setUserDefaults(newValue, forKey: "privacyScreen") } } - @objc var privacyScreenEnabled: Bool { + var cleanUpDay: Int { get { - migrate(key: "privacyScreen") - if let value = try? keychain.get("privacyScreen"), let result = Bool(value) { - return result - } - return false - } - set { - keychain["privacyScreen"] = String(newValue) - } - } - - @objc var cleanUpDay: Int { - get { - migrate(key: "cleanUpDay") - if let value = try? keychain.get("cleanUpDay"), let result = Int(value) { - return result - } - return NCBrandOptions.shared.cleanUpDay + let value = getIntPreference(key: "cleanUpDay", defaultValue: NCBrandOptions.shared.cleanUpDay) + return value } set { - keychain["cleanUpDay"] = String(newValue) + setUserDefaults(newValue, forKey: "cleanUpDay") } } @@ -328,141 +257,134 @@ import KeychainAccess var textRecognitionStatus: Bool { get { - migrate(key: "textRecognitionStatus") - if let value = try? keychain.get("textRecognitionStatus"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) } set { - keychain["textRecognitionStatus"] = String(newValue) + setUserDefaults(newValue, forKey: "textRecognitionStatus") } } var deleteAllScanImages: Bool { get { - migrate(key: "deleteAllScanImages") - if let value = try? keychain.get("deleteAllScanImages"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "deleteAllScanImages", defaultValue: false) } set { - keychain["deleteAllScanImages"] = String(newValue) + setUserDefaults(newValue, forKey: "deleteAllScanImages") } } var qualityScanDocument: Double { get { - migrate(key: "qualityScanDocument") - if let value = try? keychain.get("qualityScanDocument"), let result = Double(value) { - return result - } - return 2 + let value = getIntPreference(key: "qualityScanDocument", defaultValue: 2) + return Double(value) } set { - keychain["qualityScanDocument"] = String(newValue) + setUserDefaults(newValue, forKey: "qualityScanDocument") } } var appearanceAutomatic: Bool { get { - if let value = try? keychain.get("appearanceAutomatic"), let result = Bool(value) { - return result - } - return true + let value = getBoolPreference(key: "appearanceAutomatic", defaultValue: true) + return value } set { - keychain["appearanceAutomatic"] = String(newValue) + setUserDefaults(newValue, forKey: "appearanceAutomatic") } } var appearanceInterfaceStyle: UIUserInterfaceStyle { get { - if let value = try? keychain.get("appearanceInterfaceStyle") { - if value == "light" { - return .light - } else { - return .dark - } + let value = getStringPreference(key: "appearanceInterfaceStyle", defaultValue: "light") + if value == "light" { + return .light + } else { + return .dark } - return .light } set { if newValue == .light { - keychain["appearanceInterfaceStyle"] = "light" + setUserDefaults("light", forKey: "appearanceInterfaceStyle") } else { - keychain["appearanceInterfaceStyle"] = "dark" + setUserDefaults("dark", forKey: "appearanceInterfaceStyle") } } } var screenAwakeMode: AwakeMode { get { - if let value = try? keychain.get("screenAwakeMode") { - if value == "off" { - return .off - } else if value == "on" { - return .on - } else { - return .whileCharging - } + let value = getStringPreference(key: "screenAwakeMode", defaultValue: "off") + if value == "off" { + return .off + } else if value == "on" { + return .on + } else { + return .whileCharging } - return .off } set { if newValue == .off { - keychain["screenAwakeMode"] = "off" + setUserDefaults("off", forKey: "screenAwakeMode") } else if newValue == .on { - keychain["screenAwakeMode"] = "on" + setUserDefaults("on", forKey: "screenAwakeMode") } else { - keychain["screenAwakeMode"] = "whileCharging" + setUserDefaults("whileCharging", forKey: "screenAwakeMode") } } } var fileNameType: Bool { get { - if let value = try? keychain.get("fileNameType"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "fileNameType", defaultValue: false) } set { - keychain["fileNameType"] = String(newValue) + setUserDefaults(newValue, forKey: "fileNameType") } } var fileNameOriginal: Bool { get { - if let value = try? keychain.get("fileNameOriginal"), let result = Bool(value) { - return result - } - return false + return getBoolPreference(key: "fileNameOriginal", defaultValue: false) } set { - keychain["fileNameOriginal"] = String(newValue) + setUserDefaults(newValue, forKey: "fileNameOriginal") } } var fileNameMask: String { get { - if let value = try? keychain.get("fileNameMask") { - return value - } - return "" + return getStringPreference(key: "fileNameMask", defaultValue: "") } set { - keychain["fileNameMask"] = String(newValue) + setUserDefaults(newValue, forKey: "fileNameMask") + } + } + + var location: Bool { + get { + return getBoolPreference(key: "location", defaultValue: false) + } + set { + setUserDefaults(newValue, forKey: "location") + } + } + + var deviceTokenPushNotification: String { + get { + return getStringPreference(key: "deviceTokenPushNotification", defaultValue: "") + } + set { + setUserDefaults(newValue, forKey: "deviceTokenPushNotification") } } // MARK: - - @objc func getPassword(account: String) -> String { + func getPassword(account: String) -> String { let key = "password" + account migrate(key: key) - return (try? keychain.get(key)) ?? "" + let password = (try? keychain.get(key)) ?? "" + return password } func setPassword(account: String, password: String?) { @@ -471,83 +393,41 @@ import KeychainAccess } func setPersonalFilesOnly(account: String, value: Bool) { - let key = "personalFilesOnly" + account - keychain[key] = String(value) + let userDefaultsKey = "personalfilesonly" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } func getPersonalFilesOnly(account: String) -> Bool { - let key = "personalFilesOnly" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return false - } + return getBoolPreference(key: "personalfilesonly", account: account, defaultValue: false) } - /* OBSOLETE - func setDirectoryOnTop(account: String, value: Bool) { - let key = "directoryOnTop" + account - keychain[key] = String(value) - } - - func getDirectoryOnTop(account: String) -> Bool { - let key = "directoryOnTop" + account - if let value = try? keychain.get(key), let result = Bool(value) { - return result - } else { - return true - } + func setFavoriteOnTop(account: String, value: Bool) { + let userDefaultsKey = "favoriteOnTop" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - */ - func setTitleButtonHeader(account: String, value: String?) { - let key = "titleButtonHeader" + account - keychain[key] = value + func getFavoriteOnTop(account: String) -> Bool { + return getBoolPreference(key: "favoriteOnTop", account: account, defaultValue: true) } - 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) + func setDirectoryOnTop(account: String, value: Bool) { + let userDefaultsKey = "directoryOnTop" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - @objc func getFileNameMask(key: String) -> String { - migrate(key: key) - if let value = try? keychain.get(key) { - return value - } else { - return "" - } + func getDirectoryOnTop(account: String) -> Bool { + return getBoolPreference(key: "directoryOnTop", account: account, defaultValue: true) } - @objc func setFileNameMask(key: String, mask: String?) { - keychain[key] = mask + func setShowHiddenFiles(account: String, value: Bool) { + let userDefaultsKey = "showHiddenFiles" + "_\(account)" + setUserDefaults(value, forKey: userDefaultsKey) } - @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 - } + func getShowHiddenFiles(account: String) -> Bool { + return getBoolPreference(key: "showHiddenFiles", account: account, defaultValue: false) } - @objc func setFileNameType(key: String, prefix: Bool) { - keychain[key] = String(prefix) - } - // MARK: - E2EE func getEndToEndCertificate(account: String) -> String? { @@ -613,73 +493,62 @@ import KeychainAccess setEndToEndPassphrase(account: account, passphrase: nil) } - // MARK: - PUSHNOTIFICATION + // MARK: - PUSH NOTIFICATION - @objc func getPushNotificationPublicKey(account: String) -> Data? { - let key = "PNPublicKey" + account + func getPushNotificationPrivateKey(account: String) -> Data? { + let key = "PushPrivateKey" + account return try? keychain.getData(key) } - @objc func setPushNotificationPublicKey(account: String, data: Data?) { - let key = "PNPublicKey" + account + func setPushNotificationPrivateKey(account: String, data: Data?) { + let key = "PushPrivateKey" + account keychain[data: key] = data } - @objc func getPushNotificationPrivateKey(account: String) -> Data? { - let key = "PNPrivateKey" + account + func getPushNotificationPublicKey(account: String) -> Data? { + let key = "PushPublicKey" + account return try? keychain.getData(key) } - @objc func setPushNotificationPrivateKey(account: String, data: Data?) { - let key = "PNPrivateKey" + account + func setPushNotificationPublicKey(account: String, data: Data?) { + let key = "PushPublicKey" + account keychain[data: key] = data } - @objc func getPushNotificationSubscribingPublicKey(account: String) -> String? { - let key = "PNSubscribingPublicKey" + account + func getPushNotificationSubscribingPublicKey(account: String) -> String? { + let key = "PushSubscribingPublicKey" + account return try? keychain.get(key) } - @objc func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { - let key = "PNSubscribingPublicKey" + account + func setPushNotificationSubscribingPublicKey(account: String, publicKey: String?) { + let key = "PushSubscribingPublicKey" + account keychain[key] = publicKey } - @objc func getPushNotificationToken(account: String) -> String? { - let key = "PNToken" + account - return try? keychain.get(key) - } - - @objc func setPushNotificationToken(account: String, token: String?) { - let key = "PNToken" + account - keychain[key] = token - } - - @objc func getPushNotificationDeviceIdentifier(account: String) -> String? { - let key = "PNDeviceIdentifier" + account - return try? keychain.get(key) + func getPushNotificationDeviceIdentifier(account: String) -> String? { + let value = getStringPreference(key: "PushDeviceIdentifier", account: account, defaultValue: "") + return value } - @objc func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { - let key = "PNDeviceIdentifier" + account - keychain[key] = deviceIdentifier + func setPushNotificationDeviceIdentifier(account: String, deviceIdentifier: String?) { + let userDefaultsKey = "PushDeviceIdentifier" + "_\(account)" + setUserDefaults(deviceIdentifier, forKey: userDefaultsKey) } - @objc func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { - let key = "PNDeviceIdentifierSignature" + account + func getPushNotificationDeviceIdentifierSignature(account: String) -> String? { + let key = "PushDeviceIdentifierSignature" + account return try? keychain.get(key) } - @objc func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { - let key = "PNDeviceIdentifierSignature" + account + func setPushNotificationDeviceIdentifierSignature(account: String, deviceIdentifierSignature: String?) { + let key = "PushDeviceIdentifierSignature" + account keychain[key] = deviceIdentifierSignature } - @objc func clearAllKeysPushNotification(account: String) { + func clearAllKeysPushNotification(account: String) { + setPushNotificationPrivateKey(account: account, data: nil) setPushNotificationPublicKey(account: account, data: nil) setPushNotificationSubscribingPublicKey(account: account, publicKey: nil) - setPushNotificationPrivateKey(account: account, data: nil) - setPushNotificationToken(account: account, token: nil) setPushNotificationDeviceIdentifier(account: account, deviceIdentifier: nil) setPushNotificationDeviceIdentifierSignature(account: account, deviceIdentifierSignature: nil) } @@ -703,22 +572,13 @@ import KeychainAccess return (data, password) } - - @objc func setAccountName(account: String) { - let key = "AccountName" - keychain[key] = account - } - - @objc func getAccountName() -> String? { - let key = "AccountName" - return try? keychain.get(key) - } // MARK: - Albums func setAutoUploadAlbumIds(account: String, albumIds: [String]) { - let key = "AlbumIds" + account - keychain[key] = albumIds.joined(separator: ",") + let userDefaultsKey = "AlbumIds" + "_\(account)" + let value = albumIds.joined(separator: ",") + setUserDefaults(value, forKey: userDefaultsKey) } func getAutoUploadAlbumIds(account: String) -> [String] { @@ -776,7 +636,69 @@ import KeychainAccess } } - @objc func removeAll() { + func removeAll() { try? keychain.removeAll() } + + private func setUserDefaults(_ value: Any?, forKey key: String) { + let keyPreferences = "Preferences_\(key)" + UserDefaults.standard.set(value, forKey: keyPreferences) + } + + private func getBoolPreference(key: String, account: String? = nil, defaultValue: Bool) -> Bool { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Bool { + return value + } + + if let value = try? keychain.get(keychainKey), let boolValue = Bool(value) { + UserDefaults.standard.set(boolValue, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return boolValue + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } + + private func getStringPreference(key: String, account: String? = nil, defaultValue: String) -> String { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? String { + return value + } + + if let value = try? keychain.get(keychainKey) { + UserDefaults.standard.set(value, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return value + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } + + private func getIntPreference(key: String, account: String? = nil, defaultValue: Int) -> Int { + let suffix = account ?? "" + let userDefaultsKey = account != nil ? "Preferences_\(key)_\(suffix)" : "Preferences_\(key)" + let keychainKey = account != nil ? "\(key)\(suffix)" : key + + if let value = UserDefaults.standard.object(forKey: userDefaultsKey) as? Int { + return value + } + + if let value = try? keychain.get(keychainKey), let intValue = Int(value) { + UserDefaults.standard.set(intValue, forKey: userDefaultsKey) + try? keychain.remove(keychainKey) + return intValue + } + + UserDefaults.standard.set(defaultValue, forKey: userDefaultsKey) + return defaultValue + } } diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift index 2615c5cad7..0cf1201651 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaDetailView.swift @@ -5,7 +5,6 @@ import UIKit import MapKit import NextcloudKit -import Alamofire public protocol NCViewerMediaDetailViewDelegate: AnyObject { func downloadFullResolution() @@ -189,7 +188,6 @@ 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 e84a751ef5..71d7f2768c 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -168,7 +168,7 @@ private var hasChangesQuickLook: Bool = false toolbarConfig.optionButtonFontSize = 16 toolbarConfig.optionButtonFontSizeForPad = 21 toolbarConfig.backgroundColor = .systemGray6 - toolbarConfig.foregroundColor = NCBrandColor.shared.customer + toolbarConfig.foregroundColor = .systemBlue var viewConfig = CropViewConfig() viewConfig.cropMaskVisualEffectType = .none From 911a7e74c2472aa6ba006411784871c1693b8933 Mon Sep 17 00:00:00 2001 From: harshada-15-tsys Date: Tue, 16 Jun 2026 19:06:59 +0530 Subject: [PATCH 9/9] NMC 2169 - Media theming customisation changes --- iOSClient/Media/Cell/NCMediaCell.swift | 25 +- iOSClient/Media/Cell/NCMediaCell.xib | 12 +- iOSClient/Media/NCMedia.storyboard | 11 +- iOSClient/Media/NCMedia.swift | 383 +++++++++++++++++- iOSClient/Menu/NCMedia+Menu.swift | 1 + .../NCPlayer/NCPlayerToolBar.swift | 193 +++++++-- .../NCPlayer/NCPlayerToolBar.xib | 19 +- .../Viewer/NCViewerMedia/NCViewerMedia.swift | 55 ++- .../NCViewerMedia/NCViewerMediaPage.swift | 47 +-- .../NCViewerQuickLook/NCViewerQuickLook.swift | 31 +- .../NCViewerQuickLookView.swift | 8 +- 11 files changed, 618 insertions(+), 167 deletions(-) diff --git a/iOSClient/Media/Cell/NCMediaCell.swift b/iOSClient/Media/Cell/NCMediaCell.swift index dc9ea1f615..f2568ad922 100644 --- a/iOSClient/Media/Cell/NCMediaCell.swift +++ b/iOSClient/Media/Cell/NCMediaCell.swift @@ -24,6 +24,7 @@ import UIKit class NCMediaCell: UICollectionViewCell { + @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageVisualEffect: UIVisualEffectView! @IBOutlet weak var imageSelect: UIImageView! @@ -45,17 +46,21 @@ class NCMediaCell: UICollectionViewCell { func initCell() { imageStatus.image = nil imageItem.image = nil - - imageVisualEffect.isHidden = false - imageVisualEffect.effect = nil - imageVisualEffect.alpha = 0 - imageVisualEffect.isUserInteractionEnabled = false - imageVisualEffect.backgroundColor = UIColor.white.withAlphaComponent(0.2) + imageVisualEffect.alpha = 0.4 + imageSelect.image = NCImageCache.shared.getImageCheckedYes() + imageVisualEffect.isHidden = true + imageSelect.isHidden = true } - func selected(_ status: Bool, color: UIColor) { - imageVisualEffect.alpha = status ? 1 : 0 - imageSelect.alpha = status ? 1 : 0 - imageSelect.image = NCImageCache.shared.getImageCheckedYes(color: color) + func selected(_ status: Bool) { + 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() + } } } diff --git a/iOSClient/Media/Cell/NCMediaCell.xib b/iOSClient/Media/Cell/NCMediaCell.xib index 55163a7052..56d8a386e5 100644 --- a/iOSClient/Media/Cell/NCMediaCell.xib +++ b/iOSClient/Media/Cell/NCMediaCell.xib @@ -1,8 +1,9 @@ - + - + + @@ -26,15 +27,16 @@ - + - + diff --git a/iOSClient/Media/NCMedia.storyboard b/iOSClient/Media/NCMedia.storyboard index 5733a680ee..52dc584423 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -1,8 +1,8 @@ - + - + @@ -39,13 +39,13 @@ @@ -68,7 +68,8 @@ - + + diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 7399eb86ce..eef19314cd 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -7,11 +7,23 @@ import UIKit import NextcloudKit import RealmSwift +protocol NCMediaSelectionDelegate: AnyObject { + func didUpdateSelection(files: [String]) +} + class NCMedia: UIViewController { @IBOutlet weak var collectionView: UICollectionView! @IBOutlet weak var titleDate: UILabel! @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var titleConstraint: NSLayoutConstraint! @IBOutlet weak var gradientView: UIView! + @IBOutlet weak var gradientViewHeightContsraint: NSLayoutConstraint! + + // Called when initial media data has finished loading + var onInitialLoadCompleted: (() -> Void)? + + let semaphoreSearchMedia = DispatchSemaphore(value: 1) + let semaphoreNotificationCenter = DispatchSemaphore(value: 1) let layout = NCMediaLayout() let gradientLayer = CAGradientLayer() @@ -25,20 +37,39 @@ class NCMedia: UIViewController { let imageCache = NCImageCache.shared let networking = NCNetworking.shared var dataSource = NCMediaDataSource() + let refreshControl = UIRefreshControl() + var isTop: Bool = true var isEditMode = false - var fileSelect: [String] = [] +// var fileSelect: [String] = [] + // 1. Add this property here (NOT in an extension) + weak var selectionDelegate: NCMediaSelectionDelegate? + + // 2. Find your existing fileSelect array and add the didSet + var fileSelect: [String] = [] { + didSet { + selectionDelegate?.didUpdateSelection(files: fileSelect) + } + } + var filesExists: ThreadSafeArray = ThreadSafeArray() + var ocIdDoNotExists: ThreadSafeArray = ThreadSafeArray() var searchMediaInProgress: Bool = false + // Tracks whether we have completed an explicit preload before presentation + private var didCompleteInitialPreload = false + private var explicitPreloadTask: Task? + var attributesZoomIn: UIMenuElement.Attributes = [] var attributesZoomOut: UIMenuElement.Attributes = [] var showOnlyImages = false var showOnlyVideos = false var timeIntervalSearchNewMedia: TimeInterval = 2.0 var timerSearchNewMedia: Timer? + let insetsTop: CGFloat = 0//75//65 let livePhotoImage = NCUtility().loadImage(named: "livephoto", colors: [.white]) let playImage = NCUtility().loadImage(named: "play.fill", colors: [.white]) var photoImage = UIImage() var videoImage = UIImage() var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer() + var metadatas: ThreadSafeArray? var lastScale: CGFloat = 1.0 var currentScale: CGFloat = 1.0 @@ -51,6 +82,14 @@ class NCMedia: UIViewController { var transitionColumns = false var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 + var loadingTask: Task? + var mediaCommandView: NCMediaCommandView? + var activeAccount = tableAccount() + var lastContentOffsetY: CGFloat = 0 + let maxImageGrid: CGFloat = 7 + var hiddenCellMetadats: ThreadSafeArray = ThreadSafeArray() + + var isInGeneralPhotosSelectionContext: Bool = false let debouncerLoadDataSource = NCDebouncer(maxEventCount: 10) let debouncerSearch = NCDebouncer(maxEventCount: 10) @@ -60,7 +99,6 @@ class NCMedia: UIViewController { NCSession.shared.getSession(controller: tabBarController) } - @MainActor var controller: NCMainTabBarController? { self.tabBarController as? NCMainTabBarController } @@ -73,14 +111,33 @@ class NCMedia: UIViewController { return pinchGesture.state == .began || pinchGesture.state == .changed } - @MainActor var sceneIdentifier: String { (self.tabBarController as? NCMainTabBarController)?.sceneIdentifier ?? "" } +// var isInGeneralPhotosSelectionContext: Bool = false + + // MARK: - Programmatic Preload API + /// Preloads the media data (data source and initial search) so that the controller is ready when presented. + /// Safe to call while the media tab hasn't been opened yet. Idempotent across multiple calls. @MainActor - internal var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) + func preloadIfNeeded() { + // Avoid re-running if already completed + if didCompleteInitialPreload { return } + // Cancel any previous explicit preload + explicitPreloadTask?.cancel() + explicitPreloadTask = Task { [weak self] in + guard let self else { return } + // Ensure view is loaded to set up collectionView/layout safely + _ = self.view + // Run the same loading sequence used in view lifecycle, but explicitly + await self.loadDataSource() + await self.searchMediaUI(true) + self.didCompleteInitialPreload = true + await MainActor.run { + self.onInitialLoadCompleted?() + } + } } // MARK: - View Life Cycle @@ -89,12 +146,14 @@ class NCMedia: UIViewController { super.viewDidLoad() view.backgroundColor = .systemBackground + navigationController?.setNavigationBarAppearance() collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData") collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: mediaSectionFooter, withReuseIdentifier: "sectionFooter") collectionView.register(UINib(nibName: "NCMediaCell", bundle: nil), forCellWithReuseIdentifier: "mediaCell") collectionView.alwaysBounceVertical = true collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + collectionView.contentInset = isInGeneralPhotosSelectionContext ? UIEdgeInsets(top: 10, left: 0, bottom: 50, right: 0) : UIEdgeInsets(top: insetsTop, left: 0, bottom: 50, right: 0) collectionView.backgroundColor = .systemBackground collectionView.prefetchDataSource = self collectionView.dragInteractionEnabled = true @@ -105,7 +164,14 @@ class NCMedia: UIViewController { layout.sectionInset = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2) collectionView.collectionViewLayout = layout - layoutType = database.getLayoutForView(account: session.account, key: global.layoutViewMedia, serverUrl: "", layoutType: global.mediaLayoutRatio).layout + layoutType = database.getLayoutForView(account: session.account, key: global.layoutViewMedia, serverUrl: "", layout: global.mediaLayoutRatio).layout + +// tabBarSelect = NCMediaSelectTabBar(controller: self.tabBarController, viewController: self, delegate: self) + + titleDate.text = "" + titleDate.isHidden = true + + isEditMode = isInGeneralPhotosSelectionContext ? true : false // Gradient Layer gradientLayer.startPoint = CGPoint(x: 0, y: 0) @@ -125,6 +191,30 @@ class NCMedia: UIViewController { gradientLayer.locations = [0.0, 0.20, 0.40, 0.60, 0.75, 0.85, 0.95, 1.0] gradientView.layer.insertSublayer(gradientLayer, at: 0) + activeAccount = NCManageDatabase.shared.getActiveTableAccount() ?? tableAccount() + + collectionView.refreshControl = refreshControl + refreshControl.action(for: .valueChanged) { _ in + DispatchQueue.global().async { + Task { + await self.loadDataSource() + await self.searchMediaUI(true) + } + } + self.refreshControl.endRefreshing() + } + + // Title + Activity indicator + if UIDevice.current.userInterfaceIdiom == .pad { + titleConstraint.constant = 0 + } else { + if #available(iOS 26.0, *) { + titleConstraint.constant = -44 + } else { + titleConstraint.constant = -34 + } + } + titleDate.text = "" titleDate?.textColor = .white activityIndicator.color = .white @@ -154,6 +244,12 @@ class NCMedia: UIViewController { } } + NotificationCenter.default.addObserver(self, selector: #selector(fileExists(_:)), name: NSNotification.Name(rawValue: global.notificationCenterFileExists), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(deleteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(reloadDataSource(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadDataSource), object: nil) + NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { _ in Task { await self.networkRemoveAll() @@ -167,6 +263,7 @@ class NCMedia: UIViewController { if tabBarSelect == nil { tabBarSelect = NCMediaSelectTabBar(controller: self.tabBarController, viewController: self, delegate: self) } +// navigationController?.setMediaAppreance() Task { await (self.navigationController as? NCMediaNavigationController)?.setNavigationRightItems() @@ -180,17 +277,33 @@ class NCMedia: UIViewController { await loadDataSource() } } + Task { + if !self.didCompleteInitialPreload { + await self.loadDataSource() + await self.searchMediaUI(true) + } + } + AnalyticsHelper.shared.trackEvent(eventName: .SCREEN_EVENT__MEDIA) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") + Task { await networking.transferDispatcher.addDelegate(self) } + NotificationCenter.default.addObserver(self, selector: #selector(copyMoveFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(enterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) + if !didCompleteInitialPreload { + onInitialLoadCompleted?() + } + searchNewMedia() } @@ -202,15 +315,30 @@ class NCMedia: UIViewController { await networkRemoveAll() } + NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() +// if let frame = tabBarController?.tabBar.frame { +// tabBarSelect.hostingController?.view.frame = frame +// } gradientLayer.frame = gradientView.bounds } + override var preferredStatusBarStyle: UIStatusBarStyle { + if self.traitCollection.userInterfaceStyle == .dark { + return .lightContent + } else if isTop { + return .darkContent + } else { + return .lightContent + } + } + func searchNewMedia() { timerSearchNewMedia?.invalidate() timerSearchNewMedia = Timer.scheduledTimer(withTimeInterval: timeIntervalSearchNewMedia, repeats: false) { [weak self] _ in @@ -226,7 +354,9 @@ class NCMedia: UIViewController { func networkRemoveAll() async { timerSearchNewMedia?.invalidate() timerSearchNewMedia = nil + filesExists.removeAll() + NCNetworking.shared.fileExistsQueue.cancelAll() networking.downloadThumbnailQueue.cancelAll() let tasks = await networking.getAllDataTask() @@ -235,10 +365,89 @@ class NCMedia: UIViewController { } } + @objc func reloadDataSource(_ notification: NSNotification) { + Task { + await self.loadDataSource() + } + } + + @objc func deleteFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let error = userInfo["error"] as? NKError + else { + return + } + + // This is only a fail safe "dead lock", I don't think the timeout will ever be called but at least nothing gets stuck, if after 5 sec. (which is a long time in this routine), the semaphore is still locked + // +// if self.semaphoreNotificationCenter.wait(timeout: .now() + 5) == .timedOut { +// self.semaphoreNotificationCenter.signal() +// } + + if error.errorCode == self.global.errorResourceNotFound, + let ocIds = userInfo["ocId"] as? [String], + let ocId = ocIds.first { + Task { + await NCManageDatabase.shared.deleteMetadataAsync(ocId: ocId) + await self.loadDataSource() +// { +// self.semaphoreNotificationCenter.signal() +// } + } + } else if error != .success { + Task { + await self.loadDataSource() + } +// self.loadDataSource { +// self.semaphoreNotificationCenter.signal() +// } + } else { +// semaphoreNotificationCenter.signal() + } + } + @objc func enterForeground(_ notification: NSNotification) { searchNewMedia() } + @objc func fileExists(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let ocId = userInfo["ocId"] as? String, + let fileExists = userInfo["fileExists"] as? Bool + else { + return + } + + filesExists.append(ocId) + if !fileExists { + ocIdDoNotExists.append(ocId) + } + + if NCNetworking.shared.fileExistsQueue.operationCount == 0, + !ocIdDoNotExists.isEmpty, + let ocIdDoNotExists = self.ocIdDoNotExists.getArray() { + dataSource.removeMetadata(ocIdDoNotExists) + database.deleteMetadataOcIds(ocIdDoNotExists) + self.ocIdDoNotExists.removeAll() + collectionViewReloadData() + } + } + + @objc func copyMoveFile(_ notification: NSNotification) { + guard let userInfo = notification.userInfo as NSDictionary?, + let dragDrop = userInfo["dragdrop"] as? Bool, + dragDrop else { return } + + setEditMode(false) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + Task { + await self.loadDataSource() + await self.searchMediaUI() + } + } + } + func buildMediaPhotoVideo(columnCount: Int) { var pointSize: CGFloat = 0 @@ -256,6 +465,48 @@ class NCMedia: UIViewController { videoImage = image } } + + // MARK: - Command + + func setupMediaCommandView() { + mediaCommandView?.title.text = "" + + mediaCommandView = Bundle.main.loadNibNamed("NCMediaCommandView", owner: self, options: nil)?.first as? NCMediaCommandView + self.view.addSubview(mediaCommandView!) + mediaCommandView?.mediaView = self +// updateZoomButton() + mediaCommandView?.collapseControlButtonView(true) + mediaCommandView?.translatesAutoresizingMaskIntoConstraints = false + mediaCommandView?.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true + mediaCommandView?.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true + mediaCommandView?.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true + mediaCommandView?.heightAnchor.constraint(equalToConstant: 150).isActive = true + self.updateMediaControlVisibility() + } + + private func setupForGeneralPhotosSelection() { + if isInGeneralPhotosSelectionContext { + gradientViewHeightContsraint.constant = 0 + mediaCommandView?.setupForGeneralPhotosSelection() + isEditMode = true + } + } + + func updateMediaControlVisibility() { + + if let metadatas = self.metadatas, metadatas.isEmpty { + if !self.showOnlyImages && !self.showOnlyVideos { + self.mediaCommandView?.toggleEmptyView(isEmpty: true) + self.mediaCommandView?.isHidden = false + } else { + self.mediaCommandView?.toggleEmptyView(isEmpty: true) + self.mediaCommandView?.isHidden = false + } + } else { + self.mediaCommandView?.toggleEmptyView(isEmpty: false) + self.mediaCommandView?.isHidden = false + } + } } // MARK: - @@ -263,7 +514,12 @@ class NCMedia: UIViewController { extension NCMedia: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { if !dataSource.metadatas.isEmpty { - setTitleDate() + isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25) +// setTitleDate() + if lastContentOffsetY == 0 || lastContentOffsetY / 2 <= scrollView.contentOffset.y || lastContentOffsetY / 2 >= scrollView.contentOffset.y { + setTitleDate() + lastContentOffsetY = scrollView.contentOffset.y + } setNeedsStatusBarAppearanceUpdate() } setElements() @@ -284,13 +540,16 @@ extension NCMedia: UIScrollViewDelegate { searchNewMedia() } - func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { } + func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { + let y = view.safeAreaInsets.top + scrollView.contentOffset.y = -(insetsTop + y) + } } // MARK: - extension NCMedia: NCSelectDelegate { - func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session, controller: NCMainTabBarController?) { + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) { guard let serverUrl else { return } Task { @@ -305,3 +564,109 @@ extension NCMedia: NCSelectDelegate { } } } + +// MARK: - Media Command View + +class NCMediaCommandView: UIView { + + @IBOutlet weak var moreView: UIVisualEffectView! + @IBOutlet weak var gridSwitchButton: UIButton! + @IBOutlet weak var separatorView: UIView! + @IBOutlet weak var buttonControlWidthConstraint: NSLayoutConstraint! + @IBOutlet weak var zoomInButton: UIButton! + @IBOutlet weak var zoomOutButton: UIButton! + @IBOutlet weak var moreButton: UIButton! + @IBOutlet weak var controlButtonView: UIVisualEffectView! + @IBOutlet weak var title: UILabel! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + + var mediaView: NCMedia? + private let gradient: CAGradientLayer = CAGradientLayer() + + override func awakeFromNib() { + moreView.layer.cornerRadius = 20 + moreView.layer.masksToBounds = true + controlButtonView.layer.cornerRadius = 20 + controlButtonView.layer.masksToBounds = true + controlButtonView.effect = UIBlurEffect(style: .dark) + gradient.frame = bounds + gradient.startPoint = CGPoint(x: 0, y: 0.5) + gradient.endPoint = CGPoint(x: 0, y: 1) + gradient.colors = [UIColor.black.withAlphaComponent(UIAccessibility.isReduceTransparencyEnabled ? 0.8 : 0.4).cgColor, UIColor.clear.cgColor] + layer.insertSublayer(gradient, at: 0) + moreButton.setImage(UIImage(named: "more")!.image(color: .white, size: 25), for: .normal) + title.text = "" + } + + func setupForGeneralPhotosSelection() { + gridSwitchButton.isHidden = true + moreView.isHidden = true + title.isHidden = true + controlButtonView.isHidden = true + gradient.isHidden = true + } + + func toggleEmptyView(isEmpty: Bool) { + if isEmpty { + UIView.animate(withDuration: 0.3) { + self.moreView.effect = UIBlurEffect(style: .dark) + self.gradient.isHidden = true + self.controlButtonView.isHidden = true + } + } else { + UIView.animate(withDuration: 0.3) { + self.moreView.effect = UIBlurEffect(style: .dark) + self.gradient.isHidden = false + self.controlButtonView.isHidden = false + } + } + } + + @IBAction func moreButtonPressed(_ sender: UIButton) { +// mediaView?.openMenuButtonMore(sender) + } + + @IBAction func zoomInPressed(_ sender: UIButton) { +// mediaView?.zoomInGrid() + } + + @IBAction func zoomOutPressed(_ sender: UIButton) { +// mediaView?.zoomOutGrid() + } + + @IBAction func gridSwitchButtonPressed(_ sender: Any) { + self.collapseControlButtonView(false) + } + + func collapseControlButtonView(_ collapse: Bool) { + if collapse { + self.buttonControlWidthConstraint.constant = 40 + UIView.animate(withDuration: 0.25) { + self.zoomOutButton.isHidden = true + self.zoomInButton.isHidden = true + self.separatorView.isHidden = true + self.gridSwitchButton.isHidden = false + self.layoutIfNeeded() + } + } else { + self.buttonControlWidthConstraint.constant = 80 + UIView.animate(withDuration: 0.25) { + self.zoomOutButton.isHidden = false + self.zoomInButton.isHidden = false + self.separatorView.isHidden = false + self.gridSwitchButton.isHidden = true + self.layoutIfNeeded() + } + } + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return moreView.frame.contains(point) || controlButtonView.frame.contains(point) + } + + override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) + gradient.frame = bounds + } +} + diff --git a/iOSClient/Menu/NCMedia+Menu.swift b/iOSClient/Menu/NCMedia+Menu.swift index 495fd11d09..74a3e33179 100644 --- a/iOSClient/Menu/NCMedia+Menu.swift +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -182,6 +182,7 @@ extension NCMedia { // } // guard !fileSelect.isEmpty else { return } // +// // actions.append(.seperator(order: 0)) // // let selectedMetadatas = fileSelect.compactMap(NCManageDatabase.shared.getMetadataFromOcId) diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index 7304ddafbb..6d5e5411d2 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -9,6 +9,7 @@ import UIKit import AVKit import MediaPlayer import MobileVLCKit +import FloatingPanel import Alamofire import LucidBanner @@ -62,11 +63,9 @@ class NCPlayerToolBar: UIView { subtitleButton.setImage(utility.loadImage(named: "captions.bubble", colors: [.white]), for: .normal) subtitleButton.isEnabled = false - subtitleButton.showsMenuAsPrimaryAction = true audioButton.setImage(utility.loadImage(named: "speaker.zzz", colors: [.white]), for: .normal) audioButton.isEnabled = false - audioButton.showsMenuAsPrimaryAction = true if UIDevice.current.userInterfaceIdiom == .pad { pointSize = 60 @@ -144,9 +143,6 @@ class NCPlayerToolBar: UIView { } MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] = position - - setupSubtitleButton() - setupAudioButton() } public func updatePlaybackPosition() { @@ -257,46 +253,20 @@ class NCPlayerToolBar: UIView { viewerMediaPage?.changeScreenMode(mode: viewerMediaScreenMode) } - private func setupSubtitleButton() { + @IBAction func tapSubTitle(_ sender: Any) { guard let player = ncplayer?.player else { return } + let spuTracks = player.videoSubTitlesNames + let spuTrackIndexes = player.videoSubTitlesIndexes - var currentIndex: Int? - if let data = database.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex { - currentIndex = idx - } else { - currentIndex = Int(player.currentVideoSubTitleIndex) - } - - subtitleButton.menu = NCContextMenuPlayerTracks( - trackType: .subtitle, - tracks: player.videoSubTitlesNames, - trackIndexes: player.videoSubTitlesIndexes, - currentIndex: currentIndex, - ncplayer: ncplayer, - metadata: metadata, - viewerMediaPage: viewerMediaPage - ).viewMenu() + toggleMenuSubTitle(spuTracks: spuTracks, spuTrackIndexes: spuTrackIndexes, sender: sender) } - private func setupAudioButton() { + @IBAction func tapAudio(_ sender: Any) { guard let player = ncplayer?.player else { return } + let audioTracks = player.audioTrackNames + let audioTrackIndexes = player.audioTrackIndexes - var currentIndex: Int? - if let data = database.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex { - currentIndex = idx - } else { - currentIndex = Int(player.currentAudioTrackIndex) - } - - audioButton.menu = NCContextMenuPlayerTracks( - trackType: .audio, - tracks: player.audioTrackNames, - trackIndexes: player.audioTrackIndexes, - currentIndex: currentIndex, - ncplayer: ncplayer, - metadata: metadata, - viewerMediaPage: viewerMediaPage - ).viewMenu() + toggleMenuAudio(audioTracks: audioTracks, audioTrackIndexes: audioTrackIndexes, sender: sender) } @IBAction func tapPlayerPause(_ sender: Any) { @@ -336,8 +306,145 @@ class NCPlayerToolBar: UIView { } } +extension NCPlayerToolBar { + func toggleMenuSubTitle(spuTracks: [Any], spuTrackIndexes: [Any], sender: Any?) { + var actions = [NCMenuAction]() + var subTitleIndex: Int? + + if let data = self.database.getVideo(metadata: metadata), let idx = data.currentVideoSubTitleIndex { + subTitleIndex = idx + } else if let idx = ncplayer?.player.currentVideoSubTitleIndex { + subTitleIndex = Int(idx) + } + + if !spuTracks.isEmpty { + for index in 0...spuTracks.count - 1 { + + guard let title = spuTracks[index] as? String, let idx = spuTrackIndexes[index] as? Int32, let metadata = self.metadata else { return } + + actions.append( + NCMenuAction( + title: title, + icon: UIImage(), + onTitle: title, + onIcon: UIImage(), + selected: (subTitleIndex ?? -9999) == idx, + on: (subTitleIndex ?? -9999) == idx, + sender: sender, + action: { _ in + self.ncplayer?.player.currentVideoSubTitleIndex = idx + self.database.addVideo(metadata: metadata, currentVideoSubTitleIndex: Int(idx)) + } + ) + ) + } + + actions.append(.seperator(order: 0, sender: sender)) + } + + actions.append( + NCMenuAction( + title: NSLocalizedString("_add_subtitle_", comment: ""), + icon: UIImage(), + onTitle: NSLocalizedString("_add_subtitle_", comment: ""), + onIcon: UIImage(), + selected: false, + on: false, + sender: sender, + action: { _ in + + guard let metadata = self.metadata else { return } + let storyboard = UIStoryboard(name: "NCSelect", bundle: nil) + if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController, + let viewController = navigationController.topViewController as? NCSelect { + + viewController.delegate = self + viewController.typeOfCommandView = .nothing + viewController.includeDirectoryE2EEncryption = false + viewController.enableSelectFile = true + viewController.type = "subtitle" + viewController.serverUrl = metadata.serverUrl + viewController.session = NCSession.shared.getSession(account: metadata.account) + viewController.controller = nil + + self.viewerMediaPage?.present(navigationController, animated: true, completion: nil) + } + } + ) + ) + + viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white, sender: sender) + } + + func toggleMenuAudio(audioTracks: [Any], audioTrackIndexes: [Any], sender: Any?) { + var actions = [NCMenuAction]() + var audioIndex: Int? + + if let data = self.database.getVideo(metadata: metadata), let idx = data.currentAudioTrackIndex { + audioIndex = idx + } else if let idx = ncplayer?.player.currentAudioTrackIndex { + audioIndex = Int(idx) + } + + if !audioTracks.isEmpty { + for index in 0...audioTracks.count - 1 { + guard let title = audioTracks[index] as? String, let idx = audioTrackIndexes[index] as? Int32, let metadata = self.metadata else { return } + actions.append( + NCMenuAction( + title: title, + icon: UIImage(), + onTitle: title, + onIcon: UIImage(), + selected: (audioIndex ?? -9999) == idx, + on: (audioIndex ?? -9999) == idx, + sender: sender, + action: { _ in + self.ncplayer?.player.currentAudioTrackIndex = idx + self.database.addVideo(metadata: metadata, currentAudioTrackIndex: Int(idx)) + } + ) + ) + } + + actions.append(.seperator(order: 0, sender: sender)) + } + + actions.append( + NCMenuAction( + title: NSLocalizedString("_add_audio_", comment: ""), + icon: UIImage(), + onTitle: NSLocalizedString("_add_audio_", comment: ""), + onIcon: UIImage(), + selected: false, + on: false, + sender: sender, + action: { _ in + guard let metadata = self.metadata else { return } + let storyboard = UIStoryboard(name: "NCSelect", bundle: nil) + if let navigationController = storyboard.instantiateInitialViewController() as? UINavigationController, + let viewController = navigationController.topViewController as? NCSelect { + + viewController.delegate = self + viewController.typeOfCommandView = .nothing + viewController.includeDirectoryE2EEncryption = false + viewController.enableSelectFile = true + viewController.type = "audio" + viewController.serverUrl = metadata.serverUrl + viewController.session = NCSession.shared.getSession(account: metadata.account) + viewController.controller = nil + + self.viewerMediaPage?.present(navigationController, animated: true, completion: nil) + } + } + ) + ) + + viewerMediaPage?.presentMenu(with: actions, menuColor: UIColor(hexString: "#1C1C1EFF"), textColor: .white, sender: sender) + } +} + extension NCPlayerToolBar: NCSelectDelegate { - func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session, controller: NCMainTabBarController?) { + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) { if let metadata = metadata, let viewerMediaPage = viewerMediaPage { let fileNameLocalPath = NCUtilityFileSystem().getDirectoryProviderStorageOcId(metadata.ocId, fileName: metadata.fileNameView, userId: metadata.userId, urlBase: metadata.urlBase) let windowScene = SceneManager.shared.getWindowScene(controller: viewerMediaPage.tabBarController) @@ -373,17 +480,13 @@ extension NCPlayerToolBar: NCSelectDelegate { banner?.update(payload: LucidBannerPayload.Update(progress: Double(progress.fractionCompleted)), for: token) } - }) { _, response, error in + }) { _, etag, _, _, _, _, error in Task { if let banner { banner.dismiss() } let ocId = metadata.ocId - let allHeaderFields = response?.response?.allHeaderFields - let nkComm = NextcloudKit.shared.nkCommonInstance - let etag = nkComm.normalizedETag(nkComm.findHeader("oc-etag", allHeaderFields: allHeaderFields)) - await self.database.setMetadataSessionAsync(ocId: ocId, session: "", sessionTaskIdentifier: 0, diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib index deaf0d7558..172f9daf67 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.xib @@ -1,8 +1,9 @@ - + - + + @@ -20,11 +21,17 @@ + + +