diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index a3788d3efc..ac9f66bc5f 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -85,6 +85,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 */; }; @@ -1270,6 +1275,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 = ""; }; @@ -2064,6 +2072,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 = ""; @@ -3223,6 +3243,7 @@ F7EC9CB921185F2000F1C5CE /* Media */ = { isa = PBXGroup; children = ( + B5C980172DACD51C0041B146 /* NCMediaCommandView.xib */, F720B5B72507B9A5008C94E5 /* Cell */, F7501C302212E57400FB1415 /* NCMedia.storyboard */, F7501C312212E57400FB1415 /* NCMedia.swift */, @@ -4056,6 +4077,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 */, @@ -4886,6 +4908,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/AppDelegate.swift b/iOSClient/AppDelegate.swift index e37f4f8cc2..996667d51a 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,7 +35,16 @@ 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 { @@ -44,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() @@ -114,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 } @@ -145,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) { @@ -245,7 +470,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // MARK: - + // MARK: - Trust Certificate Error func trustCertificateError(host: String) { guard let activeTblAccount = NCManageDatabase.shared.getActiveTableAccount(), @@ -297,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) } @@ -305,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 011ddbf681..f85ab01a9b 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -151,10 +151,37 @@ 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 == NKTypeClassFile.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 == NKTypeClassFile.document.rawValue + } + var isAudioOrVideo: Bool { return classFile == NKTypeClassFile.audio.rawValue || classFile == NKTypeClassFile.video.rawValue } @@ -227,15 +254,44 @@ extension tableMetadata { return session.isEmpty && !isDirectoryE2EE && !e2eEncrypted } - // 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 true + var canShare: Bool { + return session.isEmpty && !directory && !NCBrandOptions.shared.disable_openin_file + } + + var canSetDirectoryAsE2EE: Bool { + return directory && size == 0 && !e2eEncrypted && NCPreferences().isEndToEndEnabled(account: account) + } + + var canUnsetDirectoryAsE2EE: Bool { + return !isDirectoryE2EE && directory && size == 0 && e2eEncrypted && NCPreferences().isEndToEndEnabled(account: account) + } + + 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 { @@ -344,6 +400,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), !e2eEncrypted { + 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`) @@ -918,6 +985,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, @@ -1195,6 +1290,24 @@ 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 ?? [:] + } + func getAssetLocalIdentifiersUploadedAsync() async -> [String]? { return await core.performRealmReadAsync { realm in let results = realm.objects(tableMetadata.self).filter("assetLocalIdentifier != ''") @@ -1343,6 +1456,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..5733a680ee 100644 --- a/iOSClient/Media/NCMedia.storyboard +++ b/iOSClient/Media/NCMedia.storyboard @@ -1,8 +1,8 @@ - + - + @@ -68,6 +68,7 @@ + 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..495fd11d09 --- /dev/null +++ b/iOSClient/Menu/NCMedia+Menu.swift @@ -0,0 +1,217 @@ +// +// 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.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] = [] +// +// 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 eb40357d0f..ba4daa6fad 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,6 +27,8 @@ final class NCImageCache: @unchecked Sendable { public var isLoadingCache: Bool = false public var controller: UITabBarController? + 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 self.cache.removeAll() @@ -36,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) } @@ -63,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, @@ -88,10 +89,28 @@ final class NCImageCache: @unchecked Sendable { } deinit { - if let token = observerToken { - NotificationCenter.default.removeObserver(token) - } + NotificationCenter.default.removeObserver(self, name: LRUCacheMemoryWarningNotification, object: nil) + } + + 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 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) @@ -129,7 +148,8 @@ final class NCImageCache: @unchecked Sendable { } func removeAll() { - cache.removeAll() +// cache.removeAll() + self.cache.removeAllValues() } // MARK: - MEDIA - @@ -192,31 +212,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? { @@ -231,43 +255,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 387fb10bfd..96ce2c79ee 100644 --- a/iOSClient/Settings/NCPreferences.swift +++ b/iOSClient/Settings/NCPreferences.swift @@ -225,6 +225,36 @@ final class NCPreferences: NSObject { } } + var mediaColumnCount: Int { + get { + let value = getIntPreference(key: "mediaColumnCount", defaultValue: 3) + return value + } + set { + setUserDefaults(newValue, forKey: "mediaColumnCount") + } + } + + var mediaTypeLayout: String { + get { + let value = getStringPreference(key: "mediaTypeLayout", defaultValue: NCGlobal.shared.mediaLayoutRatio) + return value + } + set { + setUserDefaults(newValue, forKey: "mediaTypeLayout") + } + } + + var mediaSortDate: String { + get { + let value = getStringPreference(key: "mediaSortDate", defaultValue: "date") + return value + } + set { + setUserDefaults(newValue, forKey: "mediaSortDate") + } + } + var textRecognitionStatus: Bool { get { return getBoolPreference(key: "textRecognitionStatus", defaultValue: false) @@ -445,10 +475,12 @@ final class NCPreferences: NSObject { } func isEndToEndEnabled(account: String) -> Bool { - 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 else { + let passphrase = getEndToEndPassphrase(account: account), !passphrase.isEmpty, + NCGlobal.shared.e2eeVersions.contains(capabilities.e2EEApiVersion) else { return false } return true diff --git a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift index 37dabd4a1a..7eddf30c48 100644 --- a/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift +++ b/iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift @@ -199,6 +199,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 } let session = NCSession.shared.getSession(account: metadata.account) @@ -245,6 +255,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 {