diff --git a/Brand/NCBrand.swift b/Brand/NCBrand.swift index 1cd9756eb9..c53e2e82a5 100755 --- a/Brand/NCBrand.swift +++ b/Brand/NCBrand.swift @@ -25,7 +25,7 @@ final class NCBrandOptions: @unchecked Sendable { var brand: String = "Nextcloud" var brandUserAgent: String = "" - var textCopyrightNextcloudiOS: String = "Nextcloud Matheria for iOS %@ © 2025" + var textCopyrightNextcloudiOS: String = "Nextcloud Matheria for iOS %@ © 2026" var textCopyrightNextcloudServer: String = "Nextcloud Server %@" var loginBaseUrl: String = "https://cloud.nextcloud.com" var pushNotificationServerProxy: String = "" diff --git a/File Provider Extension/FileProviderExtension+Actions.swift b/File Provider Extension/FileProviderExtension+Actions.swift index dd8a03b7b5..e8d37c14dc 100644 --- a/File Provider Extension/FileProviderExtension+Actions.swift +++ b/File Provider Extension/FileProviderExtension+Actions.swift @@ -207,7 +207,7 @@ extension FileProviderExtension { } if (favorite == true && !metadata.favorite) || (!favorite && metadata.favorite) { - let fileNamePath = NCUtilityFileSystem().getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) + let fileNamePath = NCUtilityFileSystem().getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId) let resultsFavorite = await NextcloudKit.shared.setFavoriteAsync(fileName: fileNamePath, favorite: favorite, account: metadata.account) if resultsFavorite.error == .success { diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 44bad97001..cc39c7ec55 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.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 */; }; + F317C82E2E844C5300761AEA /* ClientIntegrationUIViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F317C82D2E844C5300761AEA /* ClientIntegrationUIViewer.swift */; }; F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */; }; F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */; }; F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3374A802D64AB9E002A38F9 /* StatusInfo.swift */; }; @@ -544,7 +545,6 @@ F77444F622281649000D5EB0 /* NCMediaCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F77444F422281649000D5EB0 /* NCMediaCell.xib */; }; F77444F8222816D5000D5EB0 /* NCPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */; }; F778231E2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */; }; - F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */; }; F77B0F631D118A16002130FE /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F7E70DE91A24DE4100E1B66A /* Localizable.strings */; }; F77B0F7D1D118A16002130FE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7F67BB81A24D27800EE80DA /* Images.xcassets */; }; F77BB746289984CA0090FC19 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77BB745289984CA0090FC19 /* UIViewController+Extension.swift */; }; @@ -752,6 +752,16 @@ F7C9B9202B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */; }; F7C9B9232B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */; }; F7CADEFD2EA159210057849E /* NCMetadataTranfersSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CADEFA2EA1591D0057849E /* NCMetadataTranfersSuccess.swift */; }; + F7CAFE182F164B9500DB35A5 /* NCCollectionViewCommon+CellDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE172F164B9200DB35A5 /* NCCollectionViewCommon+CellDelegate.swift */; }; + F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A3DB8F2DDE238C008F7EC8 /* NCDebouncer.swift */; }; + F7CAFE1B2F16AA8D00DB35A5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1A2F16AA8600DB35A5 /* main.swift */; }; + F7CAFE1D2F17A35F00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE1E2F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE1F2F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE202F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE212F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE222F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; + F7CAFE232F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */; }; F7CB689A2541676B0050EC94 /* NCMore.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7CB68992541676B0050EC94 /* NCMore.storyboard */; }; F7CBC1232BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CBC1212BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib */; }; F7CBC1242BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7CBC1212BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib */; }; @@ -1225,6 +1235,7 @@ 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 = ""; }; F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCViewerMedia+VisionKit.swift"; sourceTree = ""; }; + F317C82D2E844C5300761AEA /* ClientIntegrationUIViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientIntegrationUIViewer.swift; sourceTree = ""; }; F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = ""; }; F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = ""; }; F3374A802D64AB9E002A38F9 /* StatusInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusInfo.swift; sourceTree = ""; }; @@ -1498,7 +1509,6 @@ F77444F422281649000D5EB0 /* NCMediaCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCMediaCell.xib; sourceTree = ""; }; F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPickerViewController.swift; sourceTree = ""; }; F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+MediaLayout.swift"; sourceTree = ""; }; - F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+Menu.swift"; sourceTree = ""; }; F77BB745289984CA0090FC19 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; F77BB747289985270090FC19 /* UITabBarController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITabBarController+Extension.swift"; sourceTree = ""; }; F77BB7492899857B0090FC19 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; @@ -1686,6 +1696,9 @@ F7C9739428F17131002C43E2 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; F7C9B91C2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+SecurityGuard.swift"; sourceTree = ""; }; F7CADEFA2EA1591D0057849E /* NCMetadataTranfersSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMetadataTranfersSuccess.swift; sourceTree = ""; }; + F7CAFE172F164B9200DB35A5 /* NCCollectionViewCommon+CellDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+CellDelegate.swift"; sourceTree = ""; }; + F7CAFE1A2F16AA8600DB35A5 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCNetworking+Actor.swift"; sourceTree = ""; }; F7CB68992541676B0050EC94 /* NCMore.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCMore.storyboard; sourceTree = ""; }; F7CBC1212BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionFirstHeaderEmptyData.xib; sourceTree = ""; }; F7CBC1222BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSectionFirstHeaderEmptyData.swift; sourceTree = ""; }; @@ -1984,7 +1997,6 @@ isa = PBXGroup; children = ( F376A3732E5CC5FF0067EE25 /* ContextMenuActions.swift */, - F77A697C250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift */, F78C6FDD296D677300C952C3 /* NCContextMenu.swift */, 3704EB2923D5A58400455C5B /* NCMenu.storyboard */, 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */, @@ -2096,6 +2108,14 @@ path = Tests; sourceTree = ""; }; + F317C82C2E844C3C00761AEA /* Client Integration */ = { + isa = PBXGroup; + children = ( + F317C82D2E844C5300761AEA /* ClientIntegrationUIViewer.swift */, + ); + path = "Client Integration"; + sourceTree = ""; + }; F3374A7F2D64AB40002A38F9 /* Components */ = { isa = PBXGroup; children = ( @@ -2398,6 +2418,7 @@ F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */, F77BC3EC293E528A005F2B08 /* NCConfigServer.swift */, F75A9EE523796C6F0044CFCE /* NCNetworking.swift */, + F7CAFE1C2F17A34F00DB35A5 /* NCNetworking+Actor.swift */, F70898662EDDB39300EF85BD /* NCNetworking+TransferDelegate.swift */, F76341172EBE0BB80056F538 /* NCNetworking+NextcloudKitDelegate.swift */, F7327E1F2B73A42F00A462C7 /* NCNetworking+Download.swift */, @@ -2467,6 +2488,7 @@ F75FE06B2BB01D0D00A0EFEF /* Cell */, F78ACD50219046AC0088454D /* Section Header Footer */, F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */, + F7CAFE172F164B9200DB35A5 /* NCCollectionViewCommon+CellDelegate.swift */, F7743A132C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift */, F74D50342C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift */, F7743A112C33F0A20034F670 /* NCCollectionViewCommon+CollectionViewDelegate.swift */, @@ -3227,6 +3249,7 @@ children = ( AA517BB42D66149900F8D37C /* .tx */, F702F2CC25EE5B4F008F8E80 /* AppDelegate.swift */, + F7CAFE1A2F16AA8600DB35A5 /* main.swift */, F794E13E2BBC0F70003693D7 /* SceneDelegate.swift */, F7CF067A2E0FF38F0063AD04 /* NCAppStateManager.swift */, F77DD6A72C5CC093009448FB /* NCSession.swift */, @@ -3238,6 +3261,7 @@ F70F96AF2874394B006C8379 /* Nextcloud-Bridging-Header.h */, F7F67BB81A24D27800EE80DA /* Images.xcassets */, F769CA1B2966EF4F00039397 /* GUI */, + F317C82C2E844C3C00761AEA /* Client Integration */, F70211F31BAC56E9003FC03E /* Main */, F7FDFF5A2E437E55000D7688 /* Account */, F7A321621E9E37960069AD1B /* Activity */, @@ -4069,6 +4093,7 @@ F798F0EC2588060A000DAFFD /* UIColor+Extension.swift in Sources */, F76882372C0DD22F001CF441 /* NCPreferences.swift in Sources */, F73EF7E52B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, + F7CAFE232F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F71F6D0D2B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, F763D2A32A249C4500A3C901 /* NCManageDatabase+Capabilities.swift in Sources */, F749B656297B0F2400087535 /* NCManageDatabase+Avatar.swift in Sources */, @@ -4136,6 +4161,7 @@ F7F1FB9E2E27CE7200C79E20 /* NCNetworking.swift in Sources */, F77DD6AD2C5CC093009448FB /* NCSession.swift in Sources */, F76340F92EBDE9760056F538 /* NCManageDatabaseCore.swift in Sources */, + F7CAFE222F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F7E742F42EC0A10C00E2362A /* NCManageDatabase+Account.swift in Sources */, F763410B2EBDFCB10056F538 /* NCManageDatabase+CreateMetadata.swift in Sources */, F7490E6B29882A92009DCE94 /* NCGlobal.swift in Sources */, @@ -4174,6 +4200,7 @@ F74B6D982A7E239A00F03C5F /* NCManageDatabase+Chunk.swift in Sources */, F7CF06872E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */, F7FDFF722E437E55000D7688 /* NCAccountRequest.swift in Sources */, + F7CAFE202F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F7707687263A853700A1BA94 /* NCContentPresenter.swift in Sources */, F343A4B62A1E084200DDA874 /* PHAsset+Extension.swift in Sources */, F70460532499095400BB98A7 /* NotificationCenter+MainThread.swift in Sources */, @@ -4205,6 +4232,7 @@ F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, F71916122E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */, F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, + F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */, F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, F77E8C252E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift in Sources */, F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, @@ -4283,6 +4311,7 @@ F78302F928B4C3E600B84583 /* NCManageDatabase+Account.swift in Sources */, F7E0710128B13BB00001B882 /* DashboardData.swift in Sources */, F783030328B4C4DD00B84583 /* ThreadSafeDictionary.swift in Sources */, + F7CAFE1E2F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F77ED59128C9CE9D00E24ED0 /* ToolbarData.swift in Sources */, F78302F728B4C3C900B84583 /* NCManageDatabase.swift in Sources */, F7346E1628B0EF5C006CE2D2 /* Widget.swift in Sources */, @@ -4364,6 +4393,7 @@ F3E173C42C9B1067006D177A /* AwakeMode.swift in Sources */, F7D61E932EBF1366007F865B /* UIColor+Extension.swift in Sources */, F76340F42EBDE9760056F538 /* NCManageDatabaseCore.swift in Sources */, + F7CAFE212F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F76340EE2EBDE74C0056F538 /* NCManageDatabase.swift in Sources */, F763410A2EBDFCB10056F538 /* NCManageDatabase+CreateMetadata.swift in Sources */, F7E742F72EC0A4CD00E2362A /* NCManageDatabase+LocalFile.swift in Sources */, @@ -4402,6 +4432,7 @@ 370D26AF248A3D7A00121797 /* NCCellProtocol.swift in Sources */, F32FADA92D1176E3007035E2 /* UIButton+Extension.swift in Sources */, F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */, + F7CAFE1D2F17A35F00DB35A5 /* NCNetworking+Actor.swift in Sources */, F71CD6CA2930D7B1006C95C1 /* NCApplicationHandle.swift in Sources */, F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, @@ -4471,7 +4502,6 @@ F710D2022405826100A6033D /* NCViewerContextMenu.swift in Sources */, F765E9CD295C585800A09ED8 /* NCUploadScanDocument.swift in Sources */, F741C2242B6B9FD600E849BB /* NCMediaSelectTabBar.swift in Sources */, - F77A697D250A0FBC00FF1708 /* NCCollectionViewCommon+Menu.swift in Sources */, F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */, AA8D31662D411FA100FE2775 /* NCShareDateCell.swift in Sources */, F3F442EE2DDE292D00FD701F /* NCMetadataPermissions.swift in Sources */, @@ -4493,6 +4523,7 @@ F76882362C0DD1E7001CF441 /* NCAcknowledgementsView.swift in Sources */, F785EE9D246196DF00B3F945 /* NCNetworkingE2EE.swift in Sources */, F724377B2C10B83E00C7C68D /* NCSharePermissions.swift in Sources */, + F317C82E2E844C5300761AEA /* ClientIntegrationUIViewer.swift in Sources */, F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */, F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, @@ -4592,6 +4623,7 @@ F73EF7DF2B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, F79B646026CA661600838ACA /* UIControl+Extension.swift in Sources */, F768823E2C0DD305001CF441 /* LazyView.swift in Sources */, + F7CAFE1B2F16AA8D00DB35A5 /* main.swift in Sources */, F3E173B02C9AF637006D177A /* ScreenAwakeManager.swift in Sources */, F747EB0D2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift in Sources */, F765F73125237E3F00391DBE /* NCRecent.swift in Sources */, @@ -4722,6 +4754,7 @@ AF4BF614275629E20081CEEF /* NCManageDatabase+Account.swift in Sources */, F76340FA2EBDE9760056F538 /* NCManageDatabaseCore.swift in Sources */, F3E173C02C9B1067006D177A /* AwakeMode.swift in Sources */, + F7CAFE182F164B9500DB35A5 /* NCCollectionViewCommon+CellDelegate.swift in Sources */, F711A4DC2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, AF4BF61E27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, ); @@ -4760,6 +4793,7 @@ AA8D31532D41052300FE2775 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7A8D74228F18261008BBE1C /* NCUtility.swift in Sources */, F7A8D73A28F17E28008BBE1C /* NCManageDatabase+Video.swift in Sources */, + F7CAFE1F2F17A37C00DB35A5 /* NCNetworking+Actor.swift in Sources */, F7D61EA72EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, F7A8D73828F17E21008BBE1C /* NCManageDatabase+DashboardWidget.swift in Sources */, F7CF06852E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */, @@ -5728,7 +5762,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -5748,7 +5782,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Nextcloud. All rights reserved."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Nextcloud. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -5794,7 +5828,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = NKUJUXUJ3B; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -5811,7 +5845,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025 Nextcloud. All rights reserved."; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2026 Nextcloud. All rights reserved."; IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -6099,7 +6133,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = main; + branch = "declarative-ui"; kind = branch; }; }; diff --git a/Share/NCShareExtension+DataSource.swift b/Share/NCShareExtension+DataSource.swift index f6ac57ec99..5b1b3cbb83 100644 --- a/Share/NCShareExtension+DataSource.swift +++ b/Share/NCShareExtension+DataSource.swift @@ -76,14 +76,12 @@ extension NCShareExtension: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! + var cell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { return cell } - cell.fileOcId = metadata.ocId - cell.fileOcIdTransfer = metadata.ocIdTransfer - cell.fileUser = metadata.ownerId + cell.metadata = metadata cell.labelTitle.text = metadata.fileNameView cell.labelTitle.textColor = NCBrandColor.shared.textColor cell.imageSelect.image = nil diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 6098a49209..adf92a6dd3 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -84,9 +84,7 @@ class NCActivity: UIViewController, NCSharePagingContent { self.loadComments() } else { Task {@MainActor in - await showErrorBanner(controller: self.tabBarController, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription) } } } @@ -225,9 +223,9 @@ extension NCActivity: UITableViewDataSource { let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) if results.image == nil { - cell.fileAvatarImageView?.image = utility.loadUserImage(for: comment.actorId, displayName: comment.actorDisplayName, urlBase: NCSession.shared.getSession(account: account).urlBase) + cell.avatarImageView?.image = utility.loadUserImage(for: comment.actorId, displayName: comment.actorDisplayName, urlBase: NCSession.shared.getSession(account: account).urlBase) } else { - cell.fileAvatarImageView?.image = results.image + cell.avatarImageView?.image = results.image } if let tblAvatar = results.tblAvatar, @@ -313,9 +311,9 @@ extension NCActivity: UITableViewDataSource { let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) if results.image == nil { - cell.fileAvatarImageView?.image = utility.loadUserImage(for: activity.user, displayName: nil, urlBase: session.urlBase) + cell.avatarImageView?.image = utility.loadUserImage(for: activity.user, displayName: nil, urlBase: session.urlBase) } else { - cell.fileAvatarImageView?.image = results.image + cell.avatarImageView?.image = results.image } if !(results.tblAvatar?.loaded ?? false), @@ -441,9 +439,7 @@ extension NCActivity { self.database.addComments(comments, account: metadata.account, objectId: metadata.fileId) } else if error.errorCode != NCGlobal.shared.errorResourceNotFound { Task {@MainActor in - await showErrorBanner(controller: self.tabBarController, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription) } } @@ -583,9 +579,7 @@ extension NCActivity: NCShareCommentsCellDelegate { self.loadComments() } else { Task {@MainActor in - await showErrorBanner(controller: self.tabBarController, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription) } } } @@ -617,9 +611,7 @@ extension NCActivity: NCShareCommentsCellDelegate { self.loadComments() } else { Task {@MainActor in - await showErrorBanner(controller: self.tabBarController, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.tabBarController, errorDescription: error.errorDescription) } } } diff --git a/iOSClient/Activity/NCActivityTableViewCell.swift b/iOSClient/Activity/NCActivityTableViewCell.swift index 2b0db6b02c..fae6553e84 100644 --- a/iOSClient/Activity/NCActivityTableViewCell.swift +++ b/iOSClient/Activity/NCActivityTableViewCell.swift @@ -37,7 +37,7 @@ class NCActivityTableViewCell: UITableViewCell, NCCellProtocol { get { return index } set { index = newValue } } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return avatar } var fileUser: String? { @@ -86,9 +86,7 @@ extension NCActivityTableViewCell: UICollectionViewDelegate { } else { let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_trash_file_not_found_") Task {@MainActor in - await showErrorBanner(controller: viewController.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: viewController.controller, errorDescription: error.errorDescription) } } } diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 7fcf867ea8..226fd9162a 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -14,7 +14,6 @@ import EasyTipView import SwiftUI import RealmSwift -@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var backgroundSessionCompletionHandler: (() -> Void)? var isUiTestingEnabled: Bool { @@ -408,7 +407,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegatesAsync { delegate in try? await Task.sleep(nanoseconds: 500_000_000) - delegate.transferReloadData(serverUrl: nil, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: nil, requestData: true, status: nil) } } } else if let navigationController = UIStoryboard(name: "NCNotification", bundle: nil).instantiateInitialViewController() as? UINavigationController, diff --git a/iOSClient/Client Integration/ClientIntegrationUIViewer.swift b/iOSClient/Client Integration/ClientIntegrationUIViewer.swift new file mode 100644 index 0000000000..9371236c5e --- /dev/null +++ b/iOSClient/Client Integration/ClientIntegrationUIViewer.swift @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +struct ClientIntegrationUIViewer: View { + @Environment(\.openURL) private var openURL + + struct Row: Identifiable { + let id = UUID() + let element: String + let title: String? + let urlString: String + } + + // Configurable inputs + let rows: [Row] + let baseURL: String + + var body: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(rows) { row in + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(row.element) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + Spacer() + } + + if let title = row.title { + Text(title).font(.headline) + } + + let finalUrl = baseURL + row.urlString + + Text(finalUrl) + .font(.footnote) + .foregroundStyle(.secondary) + .textSelection(.enabled) + + HStack(spacing: 12) { + Button { + openURL(URL(string: finalUrl)!) + } label: { + Label("Open", systemImage: "safari") + } + + Button { + UIPasteboard.general.string = row.urlString + } label: { + Label("Copy", systemImage: "doc.on.doc") + } + } + .buttonStyle(.borderedProminent) + } + .padding() + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12)) + } + } + .padding() + } + } + + // private func resolvedURL(for row: Row) -> URL? { + // // If the string is already an absolute URL with a scheme, use it. + // if let absolute = URL(string: row.urlString), absolute.scheme != nil { + // return absolute + // } + // // Otherwise, resolve relative to the provided base URL if available. + // if let baseURL { + // return URL(string: row.urlString, relativeTo: baseURL)?.absoluteURL + // } + // return nil + // } +} + +#Preview { + ClientIntegrationUIViewer(rows: [.init(element: "URL", title: "Test", urlString: "/test")], baseURL: "test.com") +} diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index f03ef6cf8d..b4b8615114 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -125,6 +125,13 @@ class tableMetadata: Object { @objc dynamic var autoUploadServerUrlBase: String? @objc dynamic var typeIdentifier: String = "" + // ========================= + // UI / transient properties + // ========================= + + /// Used only for UI state (not persisted, not observed by Realm) + var isOffline: Bool = false + override static func primaryKey() -> String { return "ocId" } diff --git a/iOSClient/DeepLink/NCDeepLinkHandler.swift b/iOSClient/DeepLink/NCDeepLinkHandler.swift index 66a34d9234..49dff00367 100644 --- a/iOSClient/DeepLink/NCDeepLinkHandler.swift +++ b/iOSClient/DeepLink/NCDeepLinkHandler.swift @@ -116,7 +116,7 @@ class NCDeepLinkHandler { DispatchQueue.main.asyncAfter(deadline: .now() + 4) { let serverUrl = controller.currentServerUrl() let session = NCSession.shared.getSession(controller: controller) - let fileFolderPath = NCUtilityFileSystem().getFileNamePath("", serverUrl: serverUrl, session: session) + let fileFolderPath = NCUtilityFileSystem().getRelativeFilePath("", serverUrl: serverUrl, session: session) let fileFolderName = (serverUrl as NSString).lastPathComponent let capabilities = NCNetworking.shared.capabilities[controller.account] ?? NKCapabilities.Capabilities() diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index 2a443ab17d..fdfafa3923 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -321,16 +321,12 @@ class NCFiles: NCCollectionViewCommon { NCContentPresenter().showInfo(description: "Metadata not found") let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, account: account) if error != .success { - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } else { // show error Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } @@ -350,9 +346,7 @@ class NCFiles: NCCollectionViewCommon { let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: serverUrl, updateVersionV1V2: true, account: account) if error != .success { Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } NCActivityIndicator.shared.stop() @@ -361,9 +355,7 @@ class NCFiles: NCCollectionViewCommon { // Client Diagnostic await self.database.addDiagnosticAsync(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors) Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } diff --git a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift index 9cf26ee1de..a22fc906d6 100644 --- a/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/ErrorBannerView.swift @@ -6,13 +6,13 @@ import SwiftUI import LucidBanner @MainActor -func showErrorBanner(controller: UITabBarController?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { +func showErrorBanner(controller: UITabBarController?, errorDescription: String, footnote: String? = nil, sleepBefore: Double = 1) async { let scene = SceneManager.shared.getWindow(controller: controller)?.windowScene - await showErrorBanner(scene: scene, errorDescription: errorDescription, errorCode: errorCode, sleepBefore: sleepBefore) + await showErrorBanner(scene: scene, errorDescription: errorDescription, footnote: footnote, sleepBefore: sleepBefore) } @MainActor -func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: Int, sleepBefore: Double = 1) async { +func showErrorBanner(scene: UIWindowScene?, errorDescription: String, footnote: String? = nil, sleepBefore: Double = 1) async { try? await Task.sleep(nanoseconds: UInt64(sleepBefore * 1e9)) var scene = scene if scene == nil { @@ -22,7 +22,7 @@ func showErrorBanner(scene: UIWindowScene?, errorDescription: String, errorCode: LucidBanner.shared.show( scene: scene, subtitle: errorDescription, - footnote: "(Code: \(errorCode))", + footnote: footnote, vPosition: .top, autoDismissAfter: NCGlobal.shared.dismissAfterSecond, swipeToDismiss: true, diff --git a/iOSClient/GUI/Lucid Banner/HudBannerView.swift b/iOSClient/GUI/Lucid Banner/HudBannerView.swift index 34b9236cec..6d1802c1f0 100644 --- a/iOSClient/GUI/Lucid Banner/HudBannerView.swift +++ b/iOSClient/GUI/Lucid Banner/HudBannerView.swift @@ -208,7 +208,6 @@ struct HudBannerView: View { @ViewBuilder func containerView(@ViewBuilder _ content: () -> Content) -> some View { let cornerRadius: CGFloat = 22 - let opacity = 0.65 let backgroundColor = Color(.systemBackground).opacity(0.65) if #available(iOS 26, *) { diff --git a/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift index b861d54f00..99ca24292c 100644 --- a/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift +++ b/iOSClient/Main/Collection Common/Cell/NCCellProtocol.swift @@ -1,46 +1,23 @@ -// -// NCCellProtocol.swift -// Nextcloud -// -// Created by Philippe Weidmann on 05.06.20. -// Copyright © 2020 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit protocol NCCellProtocol { - var fileAvatarImageView: UIImageView? { get } - var fileAccount: String? { get set } - var fileOcId: String? { get set } - var fileOcIdTransfer: String? { get set } - var filePreviewImageView: UIImageView? { get set } - var fileUser: String? { get set } - var fileTitleLabel: UILabel? { get set } - var fileInfoLabel: UILabel? { get set } - var fileSubinfoLabel: UILabel? { get set } - var fileStatusImage: UIImageView? { get set } - var fileLocalImage: UIImageView? { get set } - var fileFavoriteImage: UIImageView? { get set } - var fileSharedImage: UIImageView? { get set } - var fileMoreImage: UIImageView? { get set } - var cellSeparatorView: UIView? { get set } + var metadata: tableMetadata? {get set } + + var avatarImageView: UIImageView? { get } + var previewImageView: UIImageView? { get set } + var title: UILabel? { get set } + var info: UILabel? { get set } + var subInfo: UILabel? { get set } + var statusImageView: UIImageView? { get set } + var localImageView: UIImageView? { get set } + var favoriteImageView: UIImageView? { get set } + var shareImageView: UIImageView? { get set } + var separatorView: UIView? { get set } - func titleInfoTrailingDefault() func titleInfoTrailingFull() func writeInfoDateSize(date: NSDate, size: Int64) func setButtonMore(image: UIImage) @@ -48,7 +25,6 @@ protocol NCCellProtocol { func hideImageFavorite(_ status: Bool) func hideImageStatus(_ status: Bool) func hideImageLocal(_ status: Bool) - func hideLabelTitle(_ status: Bool) func hideLabelInfo(_ status: Bool) func hideLabelSubinfo(_ status: Bool) func hideButtonShare(_ status: Bool) @@ -60,63 +36,51 @@ protocol NCCellProtocol { } extension NCCellProtocol { - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return nil } - var fileAccount: String? { - get { return nil } - set {} - } - var fileOcId: String? { + var metadata: tableMetadata? { get { return nil } set {} } - var fileOcIdTransfer: String? { + var previewImageView: UIImageView? { get { return nil } set {} } - var filePreviewImageView: UIImageView? { + var title: UILabel? { get { return nil } set {} } - var fileTitleLabel: UILabel? { - get { return nil } - set {} - } - var fileInfoLabel: UILabel? { + var info: UILabel? { get { return nil } set { } } - var fileSubinfoLabel: UILabel? { + var subInfo: UILabel? { get { return nil } set { } } - var fileStatusImage: UIImageView? { + var statusImageView: UIImageView? { get { return nil } set {} } - var fileLocalImage: UIImageView? { + var localImageView: UIImageView? { get { return nil } set {} } - var fileFavoriteImage: UIImageView? { + var favoriteImageView: UIImageView? { get { return nil } set {} } - var fileSharedImage: UIImageView? { + var shareImageView: UIImageView? { get { return nil } set {} } - var fileMoreImage: UIImageView? { - get { return nil } - set {} - } - var cellSeparatorView: UIView? { + + var separatorView: UIView? { get { return nil } set {} } - func titleInfoTrailingDefault() {} func titleInfoTrailingFull() {} func writeInfoDateSize(date: NSDate, size: Int64) {} func setButtonMore(image: UIImage) {} @@ -124,7 +88,6 @@ extension NCCellProtocol { func hideImageFavorite(_ status: Bool) {} func hideImageStatus(_ status: Bool) {} func hideImageLocal(_ status: Bool) {} - func hideLabelTitle(_ status: Bool) {} func hideLabelInfo(_ status: Bool) {} func hideLabelSubinfo(_ status: Bool) {} func hideButtonShare(_ status: Bool) {} diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift index 582ee2dd6d..0bb73f7aea 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.swift @@ -1,28 +1,14 @@ -// -// NCGridCell.swift -// Nextcloud -// -// Created by Marino Faggiana on 08/10/2018. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit +protocol NCGridCellDelegate: AnyObject { + func onMenuIntent(with metadata: tableMetadata?) + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) +} + class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageSelect: UIImageView! @@ -36,56 +22,51 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto @IBOutlet weak var imageVisualEffect: UIVisualEffectView! @IBOutlet weak var iconsStackView: UIStackView! - var ocId = "" - var ocIdTransfer = "" - var account = "" - var user = "" + weak var delegate: NCGridCellDelegate? - weak var gridCellDelegate: NCGridCellDelegate? - - var fileOcId: String? { - get { return ocId } - set { ocId = newValue ?? "" } - } - var fileOcIdTransfer: String? { - get { return ocIdTransfer } - set { ocIdTransfer = newValue ?? "" } + var metadata: tableMetadata? { + didSet { + delegate?.contextMenu(with: metadata, button: buttonMore, sender: self) /* preconfigure UIMenu with each metadata */ + } } - var filePreviewImageView: UIImageView? { + + var previewImageView: UIImageView? { get { return imageItem } set { imageItem = newValue } } - var fileUser: String? { - get { return user } - set { user = newValue ?? "" } - } - var fileTitleLabel: UILabel? { + var title: UILabel? { get { return labelTitle } set { labelTitle = newValue } } - var fileInfoLabel: UILabel? { + var info: UILabel? { get { return labelInfo } set { labelInfo = newValue } } - var fileSubinfoLabel: UILabel? { + var subInfo: UILabel? { get { return labelSubinfo } set { labelSubinfo = newValue } } - var fileStatusImage: UIImageView? { + var statusImageView: UIImageView? { get { return imageStatus } set { imageStatus = newValue } } - var fileLocalImage: UIImageView? { + var localImageView: UIImageView? { get { return imageLocal } set { imageLocal = newValue } } - var fileFavoriteImage: UIImageView? { + var favoriteImageView: UIImageView? { get { return imageFavorite } set { imageFavorite = newValue } } override func awakeFromNib() { super.awakeFromNib() + + let tapObserver = UITapGestureRecognizer(target: self, action: #selector(handleTapObserver(_:))) + tapObserver.cancelsTouchesInView = false + tapObserver.delegate = self + contentView.addGestureRecognizer(tapObserver) + initCell() } @@ -119,37 +100,26 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto iconsStackView.layer.cornerRadius = 8 iconsStackView.clipsToBounds = true - let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) - longPressedGesture.minimumPressDuration = 0.5 - longPressedGesture.delegate = self - longPressedGesture.delaysTouchesBegan = true - self.addGestureRecognizer(longPressedGesture) + buttonMore.menu = nil + buttonMore.showsMenuAsPrimaryAction = true + + contentView.bringSubviewToFront(buttonMore) } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { return nil } - @IBAction func touchUpInsideMore(_ sender: Any) { - gridCellDelegate?.tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) - } + @objc private func handleTapObserver(_ g: UITapGestureRecognizer) { + let location = g.location(in: contentView) - @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - gridCellDelegate?.longPressGridItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) - } - - fileprivate func setA11yActions() { - self.accessibilityCustomActions = [ - UIAccessibilityCustomAction( - name: NSLocalizedString("_more_", comment: ""), - target: self, - selector: #selector(touchUpInsideMore(_:))) - ] + if buttonMore.frame.contains(location) { + delegate?.onMenuIntent(with: metadata) + } } func setButtonMore(image: UIImage) { buttonMore.setImage(image, for: .normal) - setA11yActions() } func hideImageItem(_ status: Bool) { @@ -168,10 +138,6 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto imageLocal.isHidden = status } - func hideLabelTitle(_ status: Bool) { - labelTitle.isHidden = status - } - func hideLabelInfo(_ status: Bool) { labelInfo.isHidden = status } @@ -190,7 +156,6 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto accessibilityCustomActions = nil } else { buttonMore.isHidden = false - setA11yActions() } if status { imageSelect.isHidden = false @@ -220,11 +185,6 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto func setIconOutlines() {} } -protocol NCGridCellDelegate: AnyObject { - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) - func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) -} - // MARK: - Grid Layout class NCGridLayout: UICollectionViewFlowLayout { diff --git a/iOSClient/Main/Collection Common/Cell/NCGridCell.xib b/iOSClient/Main/Collection Common/Cell/NCGridCell.xib index acf2c9b713..b5cacdc258 100644 --- a/iOSClient/Main/Collection Common/Cell/NCGridCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCGridCell.xib @@ -1,8 +1,8 @@ - + - + @@ -71,9 +71,6 @@ - - - diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 8a7f0e3b74..df72a63889 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -1,28 +1,15 @@ -// -// NCListCell.swift -// Nextcloud -// -// Created by Marino Faggiana on 24/10/2018. -// Copyright © 2018 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2018 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit +protocol NCListCellDelegate: AnyObject { + func onMenuIntent(with metadata: tableMetadata?) + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) + func tapShareListItem(with metadata: tableMetadata?, button: UIButton, sender: Any) +} + class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageSelect: UIImageView! @@ -45,64 +32,50 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto @IBOutlet weak var separatorHeightConstraint: NSLayoutConstraint! @IBOutlet weak var titleTrailingConstraint: NSLayoutConstraint! - var ocId = "" - var ocIdTransfer = "" - var user = "" + weak var delegate: NCListCellDelegate? - weak var listCellDelegate: NCListCellDelegate? + var metadata: tableMetadata? { + didSet { + delegate?.contextMenu(with: metadata, button: buttonMore, sender: self) /* preconfigure UIMenu with each metadata */ + } + } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return imageShared } - var fileOcId: String? { - get { return ocId } - set { ocId = newValue ?? "" } - } - var fileOcIdTransfer: String? { - get { return ocIdTransfer } - set { ocIdTransfer = newValue ?? "" } - } - var filePreviewImageView: UIImageView? { + var previewImageView: UIImageView? { get { return imageItem } set { imageItem = newValue } } - var fileUser: String? { - get { return user } - set { user = newValue ?? "" } - } - var fileTitleLabel: UILabel? { + var title: UILabel? { get { return labelTitle } set { labelTitle = newValue } } - var fileInfoLabel: UILabel? { + var info: UILabel? { get { return labelInfo } set { labelInfo = newValue } } - var fileSubinfoLabel: UILabel? { + var subInfo: UILabel? { get { return labelSubinfo } set { labelSubinfo = newValue } } - var fileStatusImage: UIImageView? { + var statusImageView: UIImageView? { get { return imageStatus } set { imageStatus = newValue } } - var fileLocalImage: UIImageView? { + var localImageView: UIImageView? { get { return imageLocal } set { imageLocal = newValue } } - var fileFavoriteImage: UIImageView? { + var favoriteImageView: UIImageView? { get { return imageFavorite } set { imageFavorite = newValue } } - var fileSharedImage: UIImageView? { + var shareImageView: UIImageView? { get { return imageShared } set { imageShared = newValue } } - var fileMoreImage: UIImageView? { - get { return imageMore } - set { imageMore = newValue } - } - var cellSeparatorView: UIView? { + var separatorView: UIView? { get { return separator } set { separator = newValue } } @@ -122,6 +95,12 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto override func awakeFromNib() { super.awakeFromNib() + + let tapObserver = UITapGestureRecognizer(target: self, action: #selector(handleTapObserver(_:))) + tapObserver.cancelsTouchesInView = false + tapObserver.delegate = self + contentView.addGestureRecognizer(tapObserver) + initCell() } @@ -150,13 +129,11 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto separatorHeightConstraint.constant = 0.5 tag0.text = "" tag1.text = "" - titleInfoTrailingDefault() + titleTrailingConstraint.constant = 90 - let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) - longPressedGesture.minimumPressDuration = 0.5 - longPressedGesture.delegate = self - longPressedGesture.delaysTouchesBegan = true - self.addGestureRecognizer(longPressedGesture) + contentView.bringSubviewToFront(buttonMore) + buttonMore.menu = nil + buttonMore.showsMenuAsPrimaryAction = true } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { @@ -164,41 +141,29 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } @IBAction func touchUpInsideShare(_ sender: Any) { - listCellDelegate?.tapShareListItem(with: ocId, ocIdTransfer: ocIdTransfer, sender: sender) + delegate?.tapShareListItem(with: metadata, button: buttonShared, sender: sender) } - @IBAction func touchUpInsideMore(_ sender: Any) { - listCellDelegate?.tapMoreListItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) - } + @objc private func handleTapObserver(_ g: UITapGestureRecognizer) { + let location = g.location(in: contentView) - @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - listCellDelegate?.longPressListItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) + if buttonMore.frame.contains(location) { + delegate?.onMenuIntent(with: metadata) + } } - fileprivate func setA11yActions() { - self.accessibilityCustomActions = [ - UIAccessibilityCustomAction( - name: NSLocalizedString("_share_", comment: ""), - target: self, - selector: #selector(touchUpInsideShare(_:))), - UIAccessibilityCustomAction( - name: NSLocalizedString("_more_", comment: ""), - target: self, - selector: #selector(touchUpInsideMore(_:))) - ] + // Allow the button to receive taps even with the long press gesture + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + let location = touch.location(in: contentView) + return buttonMore.frame.contains(location) } func titleInfoTrailingFull() { titleTrailingConstraint.constant = 10 } - func titleInfoTrailingDefault() { - titleTrailingConstraint.constant = 90 - } - func setButtonMore(image: UIImage) { imageMore.image = image - setA11yActions() } func hideButtonMore(_ status: Bool) { @@ -228,7 +193,6 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto buttonShared.isHidden = false buttonMore.isHidden = false backgroundView = nil - setA11yActions() } if status { var blurEffectView: UIView? @@ -321,12 +285,6 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto } } -protocol NCListCellDelegate: AnyObject { - func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) -} - // MARK: - List Layout class NCListLayout: UICollectionViewFlowLayout { diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.xib b/iOSClient/Main/Collection Common/Cell/NCListCell.xib index 5aea605add..be2b91f06c 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.xib @@ -103,11 +103,8 @@ - - - - + diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift index b6be78f651..6580a1f640 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.swift @@ -1,65 +1,46 @@ -// -// NCPhotoCell.swift -// Nextcloud -// -// Created by Marino Faggiana on 13/07/2024. -// Copyright © 2024 Marino Faggiana. All rights reserved. -// -// Author Marino Faggiana -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit -class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProtocol { +protocol NCPhotoCellDelegate: AnyObject { + func onMenuIntent(with metadata: tableMetadata?) + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) +} +class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageSelect: UIImageView! @IBOutlet weak var imageStatus: UIImageView! @IBOutlet weak var buttonMore: UIButton! @IBOutlet weak var imageVisualEffect: UIVisualEffectView! - var ocId = "" - var ocIdTransfer = "" - var user = "" - - weak var photoCellDelegate: NCPhotoCellDelegate? + weak var delegate: NCPhotoCellDelegate? - var fileOcId: String? { - get { return ocId } - set { ocId = newValue ?? "" } - } - var fileOcIdTransfer: String? { - get { return ocIdTransfer } - set { ocIdTransfer = newValue ?? "" } + var metadata: tableMetadata? { + didSet { + delegate?.contextMenu(with: metadata, button: buttonMore, sender: self) /* preconfigure UIMenu with each metadata */ + } } - var filePreviewImageView: UIImageView? { + + var previewImageView: UIImageView? { get { return imageItem } set { imageItem = newValue } } - var fileUser: String? { - get { return user } - set { user = newValue ?? "" } - } - var fileStatusImage: UIImageView? { + var statusImageView: UIImageView? { get { return imageStatus } set { imageStatus = newValue } } override func awakeFromNib() { super.awakeFromNib() + + let tapObserver = UITapGestureRecognizer(target: self, action: #selector(handleTapObserver(_:))) + tapObserver.cancelsTouchesInView = false + tapObserver.delegate = self + contentView.addGestureRecognizer(tapObserver) + initCell() } @@ -80,41 +61,30 @@ class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProt imageVisualEffect.clipsToBounds = true imageVisualEffect.alpha = 0.5 - let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:))) - longPressedGesture.minimumPressDuration = 0.5 - longPressedGesture.delegate = self - longPressedGesture.delaysTouchesBegan = true - self.addGestureRecognizer(longPressedGesture) + buttonMore.isHidden = true + buttonMore.menu = nil + buttonMore.showsMenuAsPrimaryAction = true + contentView.bringSubviewToFront(buttonMore) } override func snapshotView(afterScreenUpdates afterUpdates: Bool) -> UIView? { return nil } - @IBAction func touchUpInsideMore(_ sender: Any) { - photoCellDelegate?.tapMorePhotoItem(with: ocId, ocIdTransfer: ocIdTransfer, image: imageItem.image, sender: sender) - } - - @objc func longPress(gestureRecognizer: UILongPressGestureRecognizer) { - photoCellDelegate?.longPressPhotoItem(with: ocId, ocIdTransfer: ocIdTransfer, gestureRecognizer: gestureRecognizer) - } + @objc private func handleTapObserver(_ g: UITapGestureRecognizer) { + let location = g.location(in: contentView) - fileprivate func setA11yActions() { - self.accessibilityCustomActions = [ - UIAccessibilityCustomAction( - name: NSLocalizedString("_more_", comment: ""), - target: self, - selector: #selector(touchUpInsideMore(_:))) - ] + if buttonMore.frame.contains(location) { + delegate?.onMenuIntent(with: metadata) + } } func setButtonMore(image: UIImage) { buttonMore.setImage(image, for: .normal) - setA11yActions() } func hideButtonMore(_ status: Bool) { - buttonMore.isHidden = status + // buttonMore.isHidden = status NO MORE USED } func hideImageStatus(_ status: Bool) { @@ -137,8 +107,3 @@ class NCPhotoCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProt accessibilityValue = value } } - -protocol NCPhotoCellDelegate: AnyObject { - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) - func longPressPhotoItem(with objectId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) -} diff --git a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.xib b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.xib index bf456974fe..1e47d12444 100644 --- a/iOSClient/Main/Collection Common/Cell/NCPhotoCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCPhotoCell.xib @@ -1,9 +1,8 @@ - + - - + @@ -52,9 +51,6 @@ - - - @@ -89,10 +85,10 @@ - + - + diff --git a/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.swift b/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.swift index d6c8987a05..e23fb58b88 100644 --- a/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.swift @@ -1,30 +1,37 @@ -// -// NCRecommendationsCell.swift -// Nextcloud -// -// Created by Marino Faggiana on 06/01/25. -// Copyright © 2025 Marino Faggiana. All rights reserved. -// +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2025 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later import UIKit protocol NCRecommendationsCellDelegate: AnyObject { - func touchUpInsideButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) + func onMenuIntent(with metadata: tableMetadata?) + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) } class NCRecommendationsCell: UICollectionViewCell, UIGestureRecognizerDelegate { @IBOutlet weak var image: UIImageView! @IBOutlet weak var labelFilename: UILabel! @IBOutlet weak var labelInfo: UILabel! - @IBOutlet weak var buttonMenu: UIButton! + @IBOutlet weak var buttonMore: UIButton! var delegate: NCRecommendationsCellDelegate? - var metadata: tableMetadata = tableMetadata() var recommendedFiles: tableRecommendedFiles = tableRecommendedFiles() - var id: String = "" + + var metadata: tableMetadata? { + didSet { + delegate?.contextMenu(with: metadata, button: buttonMore, sender: self) /* preconfigure UIMenu with each metadata */ + } + } override func awakeFromNib() { super.awakeFromNib() + + let tapObserver = UITapGestureRecognizer(target: self, action: #selector(handleTapObserver(_:))) + tapObserver.cancelsTouchesInView = false + tapObserver.delegate = self + contentView.addGestureRecognizer(tapObserver) + initCell() } @@ -36,15 +43,27 @@ class NCRecommendationsCell: UICollectionViewCell, UIGestureRecognizerDelegate { func initCell() { let imageButton = UIImage(systemName: "ellipsis.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .thin))?.applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [.black, .white])) - buttonMenu.setImage(imageButton, for: .normal) - buttonMenu.layer.shadowColor = UIColor.black.cgColor - buttonMenu.layer.shadowOpacity = 0.2 - buttonMenu.layer.shadowOffset = CGSize(width: 2, height: 2) - buttonMenu.layer.shadowRadius = 4 + buttonMore.setImage(imageButton, for: .normal) + buttonMore.layer.shadowColor = UIColor.black.cgColor + buttonMore.layer.shadowOpacity = 0.2 + buttonMore.layer.shadowOffset = CGSize(width: 2, height: 2) + buttonMore.layer.shadowRadius = 4 image.image = nil labelFilename.text = "" labelInfo.text = "" + + buttonMore.menu = nil + buttonMore.showsMenuAsPrimaryAction = true + contentView.bringSubviewToFront(buttonMore) + } + + @objc private func handleTapObserver(_ g: UITapGestureRecognizer) { + let location = g.location(in: contentView) + + if buttonMore.frame.contains(location) { + delegate?.onMenuIntent(with: metadata) + } } func setImageCorner(withBorder: Bool) { @@ -58,8 +77,4 @@ class NCRecommendationsCell: UICollectionViewCell, UIGestureRecognizerDelegate { image.layer.borderColor = UIColor.clear.cgColor } } - - @IBAction func touchUpInsideButtonMenu(_ sender: Any) { - self.delegate?.touchUpInsideButtonMenu(with: self.metadata, image: image.image, sender: sender) - } } diff --git a/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.xib b/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.xib index 9f5c0a17b3..610a832dc1 100644 --- a/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.xib +++ b/iOSClient/Main/Collection Common/Cell/NCRecommendationsCell.xib @@ -1,9 +1,8 @@ - + - - + @@ -43,9 +42,6 @@ - - - @@ -65,7 +61,7 @@ - + @@ -76,7 +72,7 @@ - + diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CellDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CellDelegate.swift new file mode 100644 index 0000000000..4aabde59f7 --- /dev/null +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CellDelegate.swift @@ -0,0 +1,21 @@ +extension NCCollectionViewCommon: NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate { + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) { + Task { + guard let metadata else { return } + button.menu = NCContextMenu(metadata: metadata, viewController: self, sceneIdentifier: self.sceneIdentifier, sender: sender).viewMenu() + } + } + + func onMenuIntent(with metadata: tableMetadata?) { + Task { + await self.debouncerReloadData.pause() + } + } + + func tapShareListItem(with metadata: tableMetadata?, button: UIButton, sender: Any) { + Task { + guard let metadata else { return } + NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) + } + } +} diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index e8080bb079..be4595a357 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -57,22 +57,21 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { private func photoCell(cell: NCPhotoCell, indexPath: IndexPath, metadata: tableMetadata, ext: String) -> NCPhotoCell { let width = UIScreen.main.bounds.width / CGFloat(self.numberOfColumns) - cell.ocId = metadata.ocId - cell.ocIdTransfer = metadata.ocIdTransfer - cell.hideButtonMore(true) + cell.metadata = metadata + // cell.hideButtonMore(true) NO MORE USED cell.hideImageStatus(true) // Image // if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - cell.filePreviewImageView?.image = image - cell.filePreviewImageView?.contentMode = .scaleAspectFill + cell.previewImageView?.image = image + cell.previewImageView?.contentMode = .scaleAspectFill } else { if isPinchGestureActive || ext == global.previewExt512 || ext == global.previewExt1024 { - cell.filePreviewImageView?.image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) + cell.previewImageView?.image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) } DispatchQueue.global(qos: .userInteractive).async { @@ -80,16 +79,16 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { if let image { self.imageCache.addImageCache(ocId: metadata.ocId, etag: metadata.etag, image: image, ext: ext, cost: indexPath.row) DispatchQueue.main.async { - cell.filePreviewImageView?.image = image - cell.filePreviewImageView?.contentMode = .scaleAspectFill + cell.previewImageView?.image = image + cell.previewImageView?.contentMode = .scaleAspectFill } } else { DispatchQueue.main.async { - cell.filePreviewImageView?.contentMode = .scaleAspectFit + cell.previewImageView?.contentMode = .scaleAspectFit if metadata.iconName.isEmpty { - cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + cell.previewImageView?.image = NCImageCache.shared.getImageFile() } else { - cell.filePreviewImageView?.image = self.utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + cell.previewImageView?.image = self.utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } } } @@ -105,7 +104,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } if width > 100 { - cell.hideButtonMore(false) + // cell.hideButtonMore(false) NO MORE USED cell.hideImageStatus(false) } @@ -139,34 +138,34 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { if isLayoutPhoto { if metadata.isImageOrVideo { let photoCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? NCPhotoCell)! - photoCell.photoCellDelegate = self + photoCell.delegate = self cell = photoCell return self.photoCell(cell: photoCell, indexPath: indexPath, metadata: metadata, ext: ext) } else { let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! - gridCell.gridCellDelegate = self + gridCell.delegate = self cell = gridCell } } else if isLayoutGrid { // LAYOUT GRID let gridCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridCell)! - gridCell.gridCellDelegate = self + gridCell.delegate = self cell = gridCell } else { // LAYOUT LIST let listCell = (collectionView.dequeueReusableCell(withReuseIdentifier: "listCell", for: indexPath) as? NCListCell)! - listCell.listCellDelegate = self + listCell.delegate = self cell = listCell } // CONTENT MODE - cell.fileAvatarImageView?.contentMode = .center - cell.filePreviewImageView?.layer.borderWidth = 0 + cell.avatarImageView?.contentMode = .center + cell.previewImageView?.layer.borderWidth = 0 if existsImagePreview && layoutForView?.layout != global.layoutPhotoRatio { - cell.filePreviewImageView?.contentMode = .scaleAspectFill + cell.previewImageView?.contentMode = .scaleAspectFill } else { - cell.filePreviewImageView?.contentMode = .scaleAspectFit + cell.previewImageView?.contentMode = .scaleAspectFit } guard let metadata = self.dataSource.getMetadata(indexPath: indexPath) else { @@ -178,35 +177,23 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { isMounted = metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !metadataFolder!.permissions.contains(NCMetadataPermissions.permissionMounted) } - cell.fileAccount = metadata.account - cell.fileOcId = metadata.ocId - cell.fileOcIdTransfer = metadata.ocIdTransfer - cell.fileUser = metadata.ownerId - if isSearchingMode { if metadata.name == global.appName { - cell.fileInfoLabel?.text = NSLocalizedString("_in_", comment: "") + " " + utilityFileSystem.getPath(path: metadata.path, user: metadata.user) + cell.info?.text = NSLocalizedString("_in_", comment: "") + " " + utilityFileSystem.getPath(path: metadata.path, user: metadata.user) } else { - cell.fileInfoLabel?.text = metadata.subline + cell.info?.text = metadata.subline } - cell.fileSubinfoLabel?.isHidden = true + cell.subInfo?.isHidden = true } else if !metadata.sessionError.isEmpty, metadata.status != global.metadataStatusNormal { - cell.fileSubinfoLabel?.isHidden = false - cell.fileInfoLabel?.text = metadata.sessionError + cell.subInfo?.isHidden = false + cell.info?.text = metadata.sessionError } else { - cell.fileSubinfoLabel?.isHidden = false + cell.subInfo?.isHidden = false cell.writeInfoDateSize(date: metadata.date, size: metadata.size) } -// if cell is NCListCell && cell.fileTitleLabel is BidiFilenameLabel { -// (cell.fileTitleLabel as? BidiFilenameLabel)?.fullFilename = metadata.fileNameView -// (cell.fileTitleLabel as? BidiFilenameLabel)?.isFolder = metadata.directory -// (cell.fileTitleLabel as? BidiFilenameLabel)?.numberOfLines = 1 -// -// } else { - cell.fileTitleLabel?.text = metadata.fileNameView -// } + cell.title?.text = metadata.fileNameView // Accessibility [shared] if metadata.ownerId != appDelegate.userId, appDelegate.account == metadata.account { if metadata.ownerId != metadata.userId { @@ -217,89 +204,91 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { let tblDirectory = database.getTableDirectory(ocId: metadata.ocId) if metadata.e2eEncrypted { - cell.filePreviewImageView?.image = imageCache.getFolderEncrypted(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderEncrypted(account: metadata.account) } else if isShare { - cell.filePreviewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account) } else if !metadata.shareType.isEmpty { metadata.shareType.contains(NKShare.ShareType.publicLink.rawValue) ? - (cell.filePreviewImageView?.image = imageCache.getFolderPublic(account: metadata.account)) : - (cell.filePreviewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account)) + (cell.previewImageView?.image = imageCache.getFolderPublic(account: metadata.account)) : + (cell.previewImageView?.image = imageCache.getFolderSharedWithMe(account: metadata.account)) } else if !metadata.shareType.isEmpty && metadata.shareType.contains(NKShare.ShareType.publicLink.rawValue) { - cell.filePreviewImageView?.image = imageCache.getFolderPublic(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderPublic(account: metadata.account) } else if metadata.mountType == "group" { - cell.filePreviewImageView?.image = imageCache.getFolderGroup(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderGroup(account: metadata.account) } else if isMounted { - cell.filePreviewImageView?.image = imageCache.getFolderExternal(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderExternal(account: metadata.account) } else if metadata.fileName == autoUploadFileName && metadata.serverUrl == autoUploadDirectory { - cell.filePreviewImageView?.image = imageCache.getFolderAutomaticUpload(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolderAutomaticUpload(account: metadata.account) } else { - cell.filePreviewImageView?.image = imageCache.getFolder(account: metadata.account) + cell.previewImageView?.image = imageCache.getFolder(account: metadata.account) } // Local image: offline - if let tblDirectory, tblDirectory.offline { - cell.fileLocalImage?.image = imageCache.getImageOfflineFlag(colors: [.systemBackground, .systemGreen]) + metadata.isOffline = tblDirectory?.offline ?? false + + if metadata.isOffline { + cell.localImageView?.image = imageCache.getImageOfflineFlag(colors: [.systemBackground, .systemGreen]) } // color folder - cell.filePreviewImageView?.image = cell.filePreviewImageView?.image?.colorizeFolder(metadata: metadata, tblDirectory: tblDirectory) + cell.previewImageView?.image = cell.previewImageView?.image?.colorizeFolder(metadata: metadata, tblDirectory: tblDirectory) } else { let tableLocalFile = database.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) if metadata.hasPreviewBorder { - cell.filePreviewImageView?.layer.borderWidth = 0.2 - cell.filePreviewImageView?.layer.borderColor = UIColor.lightGray.cgColor + cell.previewImageView?.layer.borderWidth = 0.2 + cell.previewImageView?.layer.borderColor = UIColor.lightGray.cgColor } if metadata.name == global.appName { if let image = NCImageCache.shared.getImageCache(ocId: metadata.ocId, etag: metadata.etag, ext: ext) { - cell.filePreviewImageView?.image = image + cell.previewImageView?.image = image } else if let image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: metadata.userId, urlBase: metadata.urlBase) { - cell.filePreviewImageView?.image = image + cell.previewImageView?.image = image } - if cell.filePreviewImageView?.image == nil { + if cell.previewImageView?.image == nil { if metadata.iconName.isEmpty { - cell.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + cell.previewImageView?.image = NCImageCache.shared.getImageFile() } else { - cell.filePreviewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + cell.previewImageView?.image = utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } } } else { // APP NAME - UNIFIED SEARCH switch metadata.iconName { case let str where str.contains("contacts"): - cell.filePreviewImageView?.image = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor]) case let str where str.contains("conversation"): - cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + cell.previewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) case let str where str.contains("calendar"): - cell.filePreviewImageView?.image = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor]) case let str where str.contains("deck"): - cell.filePreviewImageView?.image = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor]) case let str where str.contains("mail"): - cell.filePreviewImageView?.image = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor]) case let str where str.contains("talk"): - cell.filePreviewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) + cell.previewImageView?.image = UIImage(named: "talk-template")!.image(color: NCBrandColor.shared.getElement(account: metadata.account)) case let str where str.contains("confirm"): - cell.filePreviewImageView?.image = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor]) case let str where str.contains("pages"): - cell.filePreviewImageView?.image = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor]) default: - cell.filePreviewImageView?.image = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor]) + cell.previewImageView?.image = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor]) } if !metadata.iconUrl.isEmpty { if let ownerId = getAvatarFromIconUrl(metadata: metadata) { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: ownerId) if let image = NCImageCache.shared.getImageCache(key: fileName) { - cell.filePreviewImageView?.image = image + cell.previewImageView?.image = image } else { self.database.getImageAvatarLoaded(fileName: fileName) { image, tblAvatar in if let image { - cell.filePreviewImageView?.image = image + cell.previewImageView?.image = image NCImageCache.shared.addImageCache(image: image, key: fileName) } else { - cell.filePreviewImageView?.image = self.utility.loadUserImage(for: ownerId, displayName: nil, urlBase: metadata.urlBase) + cell.previewImageView?.image = self.utility.loadUserImage(for: ownerId, displayName: nil, urlBase: metadata.urlBase) } if !(tblAvatar?.loaded ?? false), @@ -312,29 +301,32 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } } - if let tableLocalFile, tableLocalFile.offline { + // Local image: offline + metadata.isOffline = tableLocalFile?.offline ?? false + + if metadata.isOffline { a11yValues.append(NSLocalizedString("_offline_", comment: "")) - cell.fileLocalImage?.image = imageCache.getImageOfflineFlag(colors: [.systemBackground, .systemGreen]) + cell.localImageView?.image = imageCache.getImageOfflineFlag(colors: [.systemBackground, .systemGreen]) } else if utilityFileSystem.fileProviderStorageExists(metadata) { - cell.fileLocalImage?.image = imageCache.getImageLocal(colors: [.systemBackground, .systemGreen]) + cell.localImageView?.image = imageCache.getImageLocal(colors: [.systemBackground, .systemGreen]) } } // image Favorite if metadata.favorite { - cell.fileFavoriteImage?.image = imageCache.getImageFavorite() + cell.favoriteImageView?.image = imageCache.getImageFavorite() a11yValues.append(NSLocalizedString("_favorite_short_", comment: "")) } // Share image if isShare { - cell.fileSharedImage?.image = imageCache.getImageShared() + cell.shareImageView?.image = imageCache.getImageShared() } else if !metadata.shareType.isEmpty { metadata.shareType.contains(NKShare.ShareType.publicLink.rawValue) ? - (cell.fileSharedImage?.image = imageCache.getImageShareByLink()) : - (cell.fileSharedImage?.image = imageCache.getImageShared()) + (cell.shareImageView?.image = imageCache.getImageShareByLink()) : + (cell.shareImageView?.image = imageCache.getImageShared()) } else { - cell.fileSharedImage?.image = imageCache.getImageCanShare() + cell.shareImageView?.image = imageCache.getImageCanShare() } // Button More @@ -347,34 +339,34 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { // Status if metadata.isLivePhoto { - cell.fileStatusImage?.image = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor]) + cell.statusImageView?.image = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor]) a11yValues.append(NSLocalizedString("_upload_mov_livephoto_", comment: "")) } else if metadata.isVideo { - cell.fileStatusImage?.image = utility.loadImage(named: "play.circle.fill", colors: [.systemBackgroundInverted, .systemGray5]) + cell.statusImageView?.image = utility.loadImage(named: "play.circle.fill", colors: [.systemBackgroundInverted, .systemGray5]) } switch metadata.status { case global.metadataStatusWaitCreateFolder: - cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_create_folder_", comment: "") + cell.statusImageView?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.info?.text = NSLocalizedString("_status_wait_create_folder_", comment: "") case global.metadataStatusWaitFavorite: - cell.fileStatusImage?.image = utility.loadImage(named: "star.circle", colors: NCBrandColor.shared.iconImageMultiColors) - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_favorite_", comment: "") + cell.statusImageView?.image = utility.loadImage(named: "star.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.info?.text = NSLocalizedString("_status_wait_favorite_", comment: "") case global.metadataStatusWaitCopy: - cell.fileStatusImage?.image = utility.loadImage(named: "c.circle", colors: NCBrandColor.shared.iconImageMultiColors) - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_copy_", comment: "") + cell.statusImageView?.image = utility.loadImage(named: "c.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.info?.text = NSLocalizedString("_status_wait_copy_", comment: "") case global.metadataStatusWaitMove: - cell.fileStatusImage?.image = utility.loadImage(named: "m.circle", colors: NCBrandColor.shared.iconImageMultiColors) - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_move_", comment: "") + cell.statusImageView?.image = utility.loadImage(named: "m.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.info?.text = NSLocalizedString("_status_wait_move_", comment: "") case global.metadataStatusWaitRename: - cell.fileStatusImage?.image = utility.loadImage(named: "a.circle", colors: NCBrandColor.shared.iconImageMultiColors) - cell.fileInfoLabel?.text = NSLocalizedString("_status_wait_rename_", comment: "") + cell.statusImageView?.image = utility.loadImage(named: "a.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.info?.text = NSLocalizedString("_status_wait_rename_", comment: "") case global.metadataStatusWaitDownload: - cell.fileStatusImage?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) + cell.statusImageView?.image = utility.loadImage(named: "arrow.triangle.2.circlepath", colors: NCBrandColor.shared.iconImageMultiColors) case global.metadataStatusDownloading: - cell.fileStatusImage?.image = utility.loadImage(named: "arrowshape.down.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.statusImageView?.image = utility.loadImage(named: "arrowshape.down.circle", colors: NCBrandColor.shared.iconImageMultiColors) case global.metadataStatusDownloadError, global.metadataStatusUploadError: - cell.fileStatusImage?.image = utility.loadImage(named: "exclamationmark.circle", colors: NCBrandColor.shared.iconImageMultiColors) + cell.statusImageView?.image = utility.loadImage(named: "exclamationmark.circle", colors: NCBrandColor.shared.iconImageMultiColors) default: break } @@ -383,17 +375,17 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) if let image = NCImageCache.shared.getImageCache(key: fileName) { - cell.fileAvatarImageView?.contentMode = .scaleAspectFill - cell.fileAvatarImageView?.image = image + cell.avatarImageView?.contentMode = .scaleAspectFill + cell.avatarImageView?.image = image } else { self.database.getImageAvatarLoaded(fileName: fileName) { image, tblAvatar in if let image { - cell.fileAvatarImageView?.contentMode = .scaleAspectFill - cell.fileAvatarImageView?.image = image + cell.avatarImageView?.contentMode = .scaleAspectFill + cell.avatarImageView?.image = image NCImageCache.shared.addImageCache(image: image, key: fileName) } else { - cell.fileAvatarImageView?.contentMode = .scaleAspectFill - cell.fileAvatarImageView?.image = self.utility.loadUserImage(for: metadata.ownerId, displayName: metadata.ownerDisplayName, urlBase: metadata.urlBase) + cell.avatarImageView?.contentMode = .scaleAspectFill + cell.avatarImageView?.image = self.utility.loadUserImage(for: metadata.ownerId, displayName: metadata.ownerDisplayName, urlBase: metadata.urlBase) } if !(tblAvatar?.loaded ?? false), @@ -406,19 +398,16 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { // URL if metadata.classFile == NKTypeClassFile.url.rawValue { - cell.fileLocalImage?.image = nil + cell.localImageView?.image = nil cell.hideButtonShare(true) cell.hideButtonMore(true) - if let ownerId = getAvatarFromIconUrl(metadata: metadata) { - cell.fileUser = ownerId - } } // Separator if collectionView.numberOfItems(inSection: indexPath.section) == indexPath.row + 1 || isSearchingMode { - cell.cellSeparatorView?.isHidden = true + cell.separatorView?.isHidden = true } else { - cell.cellSeparatorView?.isHidden = false + cell.separatorView?.isHidden = false } // Edit mode @@ -430,17 +419,17 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } // Accessibility - cell.setAccessibility(label: metadata.fileNameView + ", " + (cell.fileInfoLabel?.text ?? "") + (cell.fileSubinfoLabel?.text ?? ""), value: a11yValues.joined(separator: ", ")) + cell.setAccessibility(label: metadata.fileNameView + ", " + (cell.info?.text ?? "") + (cell.subInfo?.text ?? ""), value: a11yValues.joined(separator: ", ")) // Color string find in search - cell.fileTitleLabel?.textColor = NCBrandColor.shared.textColor - cell.fileTitleLabel?.font = .systemFont(ofSize: 15) + cell.title?.textColor = NCBrandColor.shared.textColor + cell.title?.font = .systemFont(ofSize: 15) - if isSearchingMode, let literalSearch = self.literalSearch, let title = cell.fileTitleLabel?.text { + if isSearchingMode, let literalSearch = self.literalSearch, let title = cell.title?.text { let longestWordRange = (title.lowercased() as NSString).range(of: literalSearch) let attributedString = NSMutableAttributedString(string: title, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]) attributedString.setAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 15), NSAttributedString.Key.foregroundColor: UIColor.systemBlue], range: longestWordRange) - cell.fileTitleLabel?.attributedText = attributedString + cell.title?.attributedText = attributedString } // TAGS @@ -457,12 +446,12 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { cell.hideLabelInfo(false) cell.hideLabelSubinfo(false) cell.hideImageStatus(false) - cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 15) + cell.title?.font = UIFont.systemFont(ofSize: 15) if width < 120 { cell.hideImageFavorite(true) cell.hideImageLocal(true) - cell.fileTitleLabel?.font = UIFont.systemFont(ofSize: 10) + cell.title?.font = UIFont.systemFont(ofSize: 10) if width < 100 { cell.hideImageItem(true) cell.hideButtonMore(true) @@ -482,6 +471,9 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { cell.setIconOutlines() + // Obligatory here, at the end !! + cell.metadata = metadata + return cell } @@ -553,8 +545,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { header.setContent(emptyImage: emptyImage, emptyTitle: emptyTitle, - emptyDescription: emptyDescription, - delegate: self) + emptyDescription: emptyDescription) } else if let header = header as? NCSectionHeader { let text = self.dataSource.getSectionValueLocalization(indexPath: indexPath) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 3f2d134ddc..6c2499114f 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -61,9 +61,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { if results.nkError == .success || results.afError?.isExplicitlyCancelledError ?? false { print("ok") } else { - await showErrorBanner(scene: scene, - errorDescription: results.nkError.errorDescription, - errorCode: results.nkError.errorCode) + await showErrorBanner(scene: scene, errorDescription: results.nkError.errorDescription) } } @@ -166,9 +164,9 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { } return UIContextMenuConfiguration(identifier: identifier, previewProvider: { - return NCViewerProviderContextMenu(metadata: metadata, image: image, sceneIdentifier: self.sceneIdentifier) + return nil }, actionProvider: { _ in - let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: self, sceneIdentifier: self.sceneIdentifier, image: image, sender: cell) + let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: self, sceneIdentifier: self.sceneIdentifier, sender: cell) return contextMenu.viewMenu() }) } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index ad09d5b6f2..db710613e8 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -9,7 +9,7 @@ import NextcloudKit import EasyTipView import LucidBanner -class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, NCListCellDelegate, NCGridCellDelegate, NCPhotoCellDelegate, NCSectionFirstHeaderDelegate, NCSectionFooterDelegate, NCSectionFirstHeaderEmptyDataDelegate, NCAccountSettingsModelDelegate, NCTransferDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { +class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, UIGestureRecognizerDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate, UIAdaptivePresentationControllerDelegate, UIContextMenuInteractionDelegate { @IBOutlet weak var collectionView: UICollectionView! @@ -151,7 +151,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return self.serverUrl == self.utilityFileSystem.getHomeServer(session: self.session) && capabilities.recommendations } - internal let debouncer = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadDataSource = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerReloadData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerGetServerData = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) + internal let debouncerNetworkSearch = NCDebouncer(maxEventCount: NCBrandOptions.shared.numMaximumProcess) // MARK: - View Life Cycle @@ -233,9 +236,19 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.sectionFirstHeader?.setRichWorkspaceColor(style: view.traitCollection.userInterfaceStyle) } - NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.collectionView.reloadData() + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterChangeTheming), object: nil, queue: .main) { _ in + let serverUrl = self.serverUrl + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } + } + } + + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: self.global.notificationCenterUserInteractionMonitor), object: nil, queue: .main) { _ in + Task { + await self.debouncerReloadData.resume() + } } DispatchQueue.main.async { @@ -258,6 +271,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS isEditMode = false Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + await (self.navigationController as? NCMainNavigationController)?.setNavigationLeftItems() await (self.navigationController as? NCMainNavigationController)?.setNavigationRightItems() } @@ -283,10 +298,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - Task { - await NCNetworking.shared.transferDispatcher.addDelegate(self) - } - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(closeRichWorkspaceWebView), name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) } @@ -341,110 +352,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return true } - // MARK: - Transfer Delegate - - func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } - - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) { - Task { - if error != .success, - error.errorCode != global.errorResourceNotFound { - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) - } - guard session.account == account else { - return - } - - await self.debouncer.call { - switch status { - // UPLOADED, UPLOADED LIVEPHOTO, DELETE - case self.global.networkingStatusUploaded, - self.global.networkingStatusDelete, - self.global.networkingStatusCopyMove: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl || destination == self.serverUrl { - await self.reloadDataSource() - } - // DOWNLOAD - case self.global.networkingStatusDownloaded: - if serverUrl == self.serverUrl || self.serverUrl.isEmpty { - await self.reloadDataSource() - } - case self.global.networkingStatusDownloadCancel: - if serverUrl == self.serverUrl { - await self.reloadDataSource() - } - // CREATE FOLDER - case self.global.networkingStatusCreateFolder: - if serverUrl == self.serverUrl, - selector != self.global.selectorUploadAutoUpload, - let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { - self.pushMetadata(metadata) - } - // RENAME - case self.global.networkingStatusRename: - if self.isSearchingMode { - self.networkSearch() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - // FAVORITE - case self.global.networkingStatusFavorite: - if self.isSearchingMode { - self.networkSearch() - } else if self is NCFavorite { - await self.reloadDataSource() - } else if self.serverUrl == serverUrl { - await self.reloadDataSource() - } - default: - break - } - } - } - } - - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { - Task { - await self.debouncer.call { - if requestData { - if self.isSearchingMode { - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.getServerData() - } - } - } else { - if self.isSearchingMode { - guard status != self.global.metadataStatusWaitDelete, - status != self.global.metadataStatusWaitRename, - status != self.global.metadataStatusWaitMove, - status != self.global.metadataStatusWaitCopy, - status != self.global.metadataStatusWaitFavorite else { - return - } - self.networkSearch() - } else if ( self.serverUrl == serverUrl) || serverUrl == nil { - Task { - await self.reloadDataSource() - } - } - } - } - } - } - // MARK: - NotificationCenter @objc func applicationWillResignActive(_ notification: NSNotification) { @@ -521,12 +428,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return NCBrandOptions.shared.brand } - func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } - @MainActor func startGUIGetServerData() { self.dataSource.setGetServerData(false) - self.collectionView.reloadData() // Don't show spinner on iPad root folder if UIDevice.current.userInterfaceIdiom == .pad, @@ -597,6 +501,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS self.networking.cancelUnifiedSearchFiles() self.isSearchingMode = false + self.networkSearchInProgress = false self.literalSearch = "" self.providers?.removeAll() self.dataSource.removeAll() @@ -609,60 +514,6 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS // MARK: - TAP EVENT - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) - } - - func tapMorePhotoItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - tapMoreGridItem(with: ocId, ocIdTransfer: ocIdTransfer, image: image, sender: sender) - } - - func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - - NCCreate().createShare(viewController: self, metadata: metadata, page: .sharing) - } - - func tapMoreGridItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { - guard let metadata = self.database.getMetadataFromOcId(ocId) else { return } - toggleMenu(metadata: metadata, image: image, sender: sender) - } - - func tapRichWorkspace(_ sender: Any) { - if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { - if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { - viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" - viewerRichWorkspace.serverUrl = serverUrl - viewerRichWorkspace.delegate = self - - navigationController.modalPresentationStyle = .fullScreen - self.present(navigationController, animated: true, completion: nil) - } - } - } - - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - toggleMenu(metadata: metadata, image: image, sender: sender) - } - - func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { - unifiedSearchMore(metadataForSection: metadataForSection) - } - - func tapRecommendations(with metadata: tableMetadata) { - didSelectMetadata(metadata, withOcIds: false) - } - - func longPressListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreListItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressPhotoItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - - func longPressMoreGridItem(with ocId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - @objc func longPressCollecationView(_ gestureRecognizer: UILongPressGestureRecognizer) { openMenuItems(with: nil, gestureRecognizer: gestureRecognizer) } @@ -768,14 +619,12 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS fileName: fileName) Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.serverUrl, requestData: true, status: nil) } } } else { Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: resultsUpload.error.errorDescription, - errorCode: resultsUpload.error.errorCode) + await showErrorBanner(scene: scene, errorDescription: resultsUpload.error.errorDescription) } } } @@ -796,11 +645,9 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } } - UIView.transition(with: self.collectionView, - duration: 0.20, - options: .transitionCrossDissolve, - animations: { self.collectionView.reloadData() }, - completion: nil) + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: self.serverUrl) + } await (self.navigationController as? NCMainNavigationController)?.updateRightMenu() } @@ -856,8 +703,8 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { metadatasSearch, error in Task { guard let metadatasSearch, - error == .success, - self.isSearchingMode + error == .success, + self.isSearchingMode else { self.networkSearchInProgress = false await self.reloadDataSource() @@ -884,7 +731,11 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let metadataForSection = metadataForSection, let lastSearchResult = metadataForSection.lastSearchResult, let cursor = lastSearchResult.cursor, let term = literalSearch else { return } metadataForSection.unifiedSearchInProgress = true - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } + } self.networking.unifiedSearchFilesProvider(id: lastSearchResult.id, term: term, limit: 5, cursor: cursor, account: session.account) { task in self.searchDataSourceTask = task @@ -894,11 +745,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS } completion: { _, searchResult, metadatas, error in if error != .success { Task {@MainActor in - await showErrorBanner( - controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } @@ -906,8 +753,10 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS guard let searchResult = searchResult, let metadatas = metadatas else { return } self.dataSource.appendMetadatasToSection(metadatas, metadataForSection: metadataForSection, lastSearchResult: searchResult) - DispatchQueue.main.async { - self.collectionView?.reloadData() + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } @@ -1016,4 +865,105 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS return CGSize(width: collectionView.frame.width, height: 0) } } + + func accountSettingsDidDismiss(tblAccount: tableAccount?, controller: NCMainTabBarController?) { } +} + +extension NCCollectionViewCommon: NCSectionFirstHeaderDelegate { + func tapRichWorkspace(_ sender: Any) { + if let navigationController = UIStoryboard(name: "NCViewerRichWorkspace", bundle: nil).instantiateInitialViewController() as? UINavigationController { + if let viewerRichWorkspace = navigationController.topViewController as? NCViewerRichWorkspace { + viewerRichWorkspace.richWorkspaceText = richWorkspaceText ?? "" + viewerRichWorkspace.serverUrl = serverUrl + viewerRichWorkspace.delegate = self + + navigationController.modalPresentationStyle = .fullScreen + self.present(navigationController, animated: true, completion: nil) + } + } + } + + func tapRecommendations(with metadata: tableMetadata) { + didSelectMetadata(metadata, withOcIds: false) + } +} + +extension NCCollectionViewCommon: NCSectionFooterDelegate { + func tapButtonSection(_ sender: Any, metadataForSection: NCMetadataForSection?) { + unifiedSearchMore(metadataForSection: metadataForSection) + } +} + +extension NCCollectionViewCommon: NCTransferDelegate { + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + + func transferReloadData(serverUrl: String?) { + Task { + await self.debouncerReloadData.call({ + self.collectionView.reloadData() + }, immediate: true) + } + } + + func transferChange(status: String, + account: String, + fileName: String, + serverUrl: String, + selector: String?, + ocId: String, + destination: String?, + error: NKError) { + Task { + if error != .success, + error.errorCode != global.errorResourceNotFound { + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) + } + guard session.account == account else { + return + } + + if status == self.global.networkingStatusCreateFolder { + if serverUrl == self.serverUrl, + selector != self.global.selectorUploadAutoUpload, + let metadata = await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", account, serverUrl, fileName)) { + self.pushMetadata(metadata) + } + return + } + + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + } else if self.serverUrl == serverUrl || destination == self.serverUrl || self.serverUrl.isEmpty { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { + Task { + if self.isSearchingMode { + await self.debouncerNetworkSearch.call { + self.networkSearch() + } + return + } + + if requestData && (self.serverUrl == serverUrl || serverUrl == nil) { + await self.debouncerGetServerData.call { + await self.getServerData() + } + return + } + + if self.serverUrl == serverUrl || serverUrl == nil { + await self.debouncerReloadDataSource.call { + await self.reloadDataSource() + } + } + } + } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift index eefbcc7d3b..a9651883d1 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewDownloadThumbnail.swift @@ -42,13 +42,13 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation, @unchecked Sendabl let image = self.utility.getImage(ocId: self.metadata.ocId, etag: self.metadata.etag, ext: self.ext, userId: self.metadata.userId, urlBase: self.metadata.urlBase) Task { @MainActor in - for case let cell as NCCellProtocol in collectionView.visibleCells where cell.fileOcId == self.metadata.ocId { - if let filePreviewImageView = cell.filePreviewImageView { - filePreviewImageView.contentMode = .scaleAspectFill + for case let cell as NCCellProtocol in collectionView.visibleCells where cell.metadata?.ocId == self.metadata.ocId { + if let previewImageView = cell.previewImageView { + previewImageView.contentMode = .scaleAspectFill if self.metadata.hasPreviewBorder { - filePreviewImageView.layer.borderWidth = 0.2 - filePreviewImageView.layer.borderColor = UIColor.systemGray3.cgColor + previewImageView.layer.borderWidth = 0.2 + previewImageView.layer.borderColor = UIColor.systemGray3.cgColor } if let photoCell = (cell as? NCPhotoCell), @@ -57,11 +57,15 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation, @unchecked Sendabl cell.hideImageStatus(false) } - UIView.transition(with: filePreviewImageView, - duration: 0.75, - options: .transitionCrossDissolve, - animations: { filePreviewImageView.image = image }, - completion: nil) + UIView.transition( + with: previewImageView, + duration: 0.75, + options: .transitionCrossDissolve, + animations: { + previewImageView.image = image + }, + completion: nil + ) break } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift index 1bcb3c6317..3a926c9c9b 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewUnifiedSearch.swift @@ -19,22 +19,17 @@ class NCCollectionViewUnifiedSearch: ConcurrentOperation, @unchecked Sendable { self.searchResult = searchResult } - func reloadDataThenPerform(_ closure: @escaping (() -> Void)) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - CATransaction.begin() - CATransaction.setCompletionBlock(closure) - self.collectionViewCommon.collectionView.reloadData() - CATransaction.commit() - } - } - override func start() { guard !isCancelled else { return self.finish() } self.collectionViewCommon.dataSource.addSection(metadatas: metadatas, searchResult: searchResult) self.collectionViewCommon.searchResults?.append(self.searchResult) - reloadDataThenPerform { - self.finish() + self.finish() + + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: nil) + } } } } diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift index 3a3f982288..bedfb4a1e1 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeader.swift @@ -9,7 +9,6 @@ import NextcloudKit protocol NCSectionFirstHeaderDelegate: AnyObject { func tapRichWorkspace(_ sender: Any) func tapRecommendations(with metadata: tableMetadata) - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) } class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegate { @@ -124,7 +123,16 @@ class NCSectionFirstHeader: UICollectionReusableView, UIGestureRecognizerDelegat viewSection.isHidden = false } +#if EXTENSION self.collectionViewRecommendations.reloadData() +#else + Task { + let isPause = await (viewController as? NCCollectionViewCommon)?.debouncerReloadDataSource.isPausedNow() ?? false + if !isPause { + self.collectionViewRecommendations.reloadData() + } + } +#endif } // MARK: - RichWorkspace @@ -178,7 +186,7 @@ extension NCSectionFirstHeader: UICollectionViewDataSource { if let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: self.global.previewExt512, userId: metadata.userId, urlBase: metadata.urlBase) { Task { @MainActor in for case let cell as NCRecommendationsCell in self.collectionViewRecommendations.visibleCells { - if cell.id == recommendedFiles.id { + if cell.metadata?.fileId == recommendedFiles.id { cell.image.contentMode = .scaleAspectFill if metadata.classFile == NKTypeClassFile.document.rawValue { cell.setImageCorner(withBorder: true) @@ -208,7 +216,6 @@ extension NCSectionFirstHeader: UICollectionViewDataSource { cell.delegate = self cell.metadata = metadata cell.recommendedFiles = recommendedFiles - cell.id = recommendedFiles.id } return cell @@ -242,7 +249,7 @@ extension NCSectionFirstHeader: UICollectionViewDelegate { return NCViewerProviderContextMenu(metadata: metadata, image: image, sceneIdentifier: self.sceneIdentifier) }, actionProvider: { _ in let cell = collectionView.cellForItem(at: indexPath) - let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: viewController, sceneIdentifier: self.sceneIdentifier, image: image, sender: cell) + let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: viewController, sceneIdentifier: self.sceneIdentifier, sender: cell) return contextMenu.viewMenu() }) #endif @@ -258,7 +265,23 @@ extension NCSectionFirstHeader: UICollectionViewDelegateFlowLayout { } extension NCSectionFirstHeader: NCRecommendationsCellDelegate { - func touchUpInsideButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { - self.delegate?.tapRecommendationsButtonMenu(with: metadata, image: image, sender: sender) + func contextMenu(with metadata: tableMetadata?, button: UIButton, sender: Any) { +#if !EXTENSION + Task { + guard let viewController = self.viewController, let metadata else { + return + } + button.menu = NCContextMenu(metadata: metadata, viewController: viewController, sceneIdentifier: self.sceneIdentifier, sender: sender).viewMenu() + } +#endif + } + + func onMenuIntent(with metadata: tableMetadata?) { +#if !EXTENSION + Task { + let collectionViewCommon = (self.viewController as? NCCollectionViewCommon) + await collectionViewCommon?.debouncerReloadData.pause() + } +#endif } } diff --git a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeaderEmptyData.swift b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeaderEmptyData.swift index 64649a793c..009e3a5b40 100644 --- a/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeaderEmptyData.swift +++ b/iOSClient/Main/Collection Common/Section Header Footer/NCSectionFirstHeaderEmptyData.swift @@ -6,16 +6,11 @@ import UIKit import MarkdownKit import RealmSwift -protocol NCSectionFirstHeaderEmptyDataDelegate: AnyObject { -} - class NCSectionFirstHeaderEmptyData: UICollectionReusableView { @IBOutlet weak var emptyImage: UIImageView! @IBOutlet weak var emptyTitle: UILabel! @IBOutlet weak var emptyDescription: UILabel! - weak var delegate: NCSectionFirstHeaderEmptyDataDelegate? - override func awakeFromNib() { super.awakeFromNib() initHeader() @@ -36,9 +31,7 @@ class NCSectionFirstHeaderEmptyData: UICollectionReusableView { func setContent(emptyImage: UIImage?, emptyTitle: String?, - emptyDescription: String?, - delegate: NCSectionFirstHeaderEmptyDataDelegate?) { - self.delegate = delegate + emptyDescription: String?) { self.emptyImage.image = emptyImage self.emptyTitle.text = emptyTitle self.emptyDescription.text = emptyDescription diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index 0348071348..26b6de23a6 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -41,11 +41,7 @@ class NCCreate: NSObject { } guard results.error == .success, let url = results.url else { Task {@MainActor in - await showErrorBanner( - controller: controller, - errorDescription: results.error.errorDescription, - errorCode: results.error.errorCode - ) + await showErrorBanner(controller: controller, errorDescription: results.error.errorDescription) } return } @@ -72,11 +68,7 @@ class NCCreate: NSObject { } guard results.error == .success, let url = results.url else { Task {@MainActor in - await showErrorBanner( - controller: controller, - errorDescription: results.error.errorDescription, - errorCode: results.error.errorCode - ) + await showErrorBanner(controller: controller, errorDescription: results.error.errorDescription) } return } diff --git a/iOSClient/Main/NCDragDrop.swift b/iOSClient/Main/NCDragDrop.swift index 24f0545624..9fe6217710 100644 --- a/iOSClient/Main/NCDragDrop.swift +++ b/iOSClient/Main/NCDragDrop.swift @@ -145,11 +145,7 @@ class NCDragDrop: NSObject { } catch { Task {@MainActor in let error = NKError(error: error) - await showErrorBanner( - controller: controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode - ) + await showErrorBanner(controller: controller, errorDescription: error.errorDescription) } return } @@ -230,7 +226,7 @@ class NCDragDrop: NSObject { downloadRequest = request } guard results.nkError == .success else { - await showErrorBanner(scene: scene, errorDescription: results.nkError.errorDescription, errorCode: results.nkError.errorCode) + await showErrorBanner(scene: scene, errorDescription: results.nkError.errorDescription) break } } @@ -252,7 +248,7 @@ class NCDragDrop: NSObject { uploadRequest = request } guard results.error == .success else { - await showErrorBanner(scene: scene, errorDescription: results.error.errorDescription, errorCode: results.error.errorCode) + await showErrorBanner(scene: scene, errorDescription: results.error.errorDescription) break } diff --git a/iOSClient/Main/NCMainNavigationController.swift b/iOSClient/Main/NCMainNavigationController.swift index 86710f684e..c56942c079 100644 --- a/iOSClient/Main/NCMainNavigationController.swift +++ b/iOSClient/Main/NCMainNavigationController.swift @@ -357,7 +357,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController image: utility.loadImage(named: "doc.text", colors: [NCBrandColor.shared.iconImageColor])) { _ in Task { let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + creator.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await NCCreate().createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "text", creatorId: creator.identifier, templateId: "document", account: session.account) } @@ -377,7 +377,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "collabora", templateId: "document", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "collabora", templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -389,7 +389,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "collabora", templateId: "spreadsheet", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "collabora", templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -401,7 +401,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "collabora", templateId: "presentation", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "collabora", templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -417,7 +417,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "document", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -431,7 +431,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "spreadsheet", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -446,7 +446,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController let createDocument = NCCreate() let templates = await createDocument.getTemplate(editorId: "onlyoffice", templateId: "presentation", account: session.account) let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + templates.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = utilityFileSystem.getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await createDocument.createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "onlyoffice", creatorId: creator.identifier, templateId: templates.selectedTemplate.identifier, account: session.account) } @@ -741,7 +741,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController Task { NCPreferences().setFavoriteOnTop(account: self.session.account, value: !favoriteOnTop) await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) } await self.updateRightMenu() } @@ -753,7 +753,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController Task { NCPreferences().setDirectoryOnTop(account: self.session.account, value: !directoryOnTop) await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) } await self.updateRightMenu() } @@ -776,7 +776,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController Task { NCPreferences().setPersonalFilesOnly(account: self.session.account, value: !personalFilesOnly) await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) } await self.updateRightMenu() } @@ -788,7 +788,7 @@ class NCMainNavigationController: UINavigationController, UINavigationController NCPreferences().showDescription = !showDescriptionKeychain Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: collectionViewCommon.serverUrl, requestData: false, status: nil) } await self.updateRightMenu() } diff --git a/iOSClient/Main/NCPickerViewController.swift b/iOSClient/Main/NCPickerViewController.swift index a44ccfdd8c..a6e20c3411 100644 --- a/iOSClient/Main/NCPickerViewController.swift +++ b/iOSClient/Main/NCPickerViewController.swift @@ -61,27 +61,21 @@ class NCPhotosPickerViewController: NSObject { pickerVC?.didExceedMaximumNumberOfSelection = { _ in let error = NKError(errorCode: self.global.errorInternalError, errorDescription: "_limited_dimension_") Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } pickerVC?.handleNoAlbumPermissions = { _ in let error = NKError(errorCode: self.global.errorInternalError, errorDescription: "_denied_album_") Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } pickerVC?.handleNoCameraPermissions = { _ in let error = NKError(errorCode: self.global.errorInternalError, errorDescription: "_denied_camera_") Task {@MainActor in - await showErrorBanner(controller: self.controller, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(controller: self.controller, errorDescription: error.errorDescription) } } diff --git a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift index e9ed267832..eee99e776e 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDataSource.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDataSource.swift @@ -84,8 +84,9 @@ extension NCMedia: UICollectionViewDataSource { if isPinchGestureActive || ext == global.previewExt512 || ext == global.previewExt1024 { cell.imageItem.image = utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: self.session.userId, urlBase: self.session.urlBase) } else { + let session = self.session DispatchQueue.global(qos: .userInteractive).async { - let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: self.session.userId, urlBase: self.session.urlBase) + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: ext, userId: session.userId, urlBase: session.urlBase) DispatchQueue.main.async { if let currentCell = collectionView.cellForItem(at: indexPath) as? NCMediaCell, currentCell.ocId == metadata.ocId, let image { diff --git a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift index 8535cad6b4..c5113c7ab4 100644 --- a/iOSClient/Media/NCMedia+CollectionViewDelegate.swift +++ b/iOSClient/Media/NCMedia+CollectionViewDelegate.swift @@ -44,8 +44,7 @@ extension NCMedia: UICollectionViewDelegate { return UIContextMenuConfiguration(identifier: identifier, previewProvider: { return NCViewerProviderContextMenu(metadata: metadata, image: image, sceneIdentifier: self.sceneIdentifier) }, actionProvider: { _ in - let cell = collectionView.cellForItem(at: indexPath) - let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: self, sceneIdentifier: self.sceneIdentifier, image: image, sender: collectionView) + let contextMenu = NCContextMenu(metadata: metadata.detachedCopy(), viewController: self, sceneIdentifier: self.sceneIdentifier, sender: collectionView) return contextMenu.viewMenu() }) } diff --git a/iOSClient/Media/NCMedia+TransferDelegate.swift b/iOSClient/Media/NCMedia+TransferDelegate.swift index dca0d83f6c..439afb8d7a 100644 --- a/iOSClient/Media/NCMedia+TransferDelegate.swift +++ b/iOSClient/Media/NCMedia+TransferDelegate.swift @@ -9,14 +9,18 @@ import NextcloudKit // MARK: - Drag extension NCMedia: NCTransferDelegate { - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { Task { - await self.debouncer.call { + await self.debouncerLoadDataSource.call { await self.loadDataSource() } } } + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, @@ -26,14 +30,9 @@ extension NCMedia: NCTransferDelegate { destination: String?, error: NKError) { Task { - await self.debouncer.call { - switch status { - case self.global.networkingStatusCopyMove: - await self.loadDataSource() - await self.searchMediaUI() - default: - break - } + await self.debouncerSearch.call { + await self.loadDataSource() + await self.searchMediaUI() } } } diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 85b2099ee0..a46652401c 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -52,7 +52,8 @@ class NCMedia: UIViewController { var numberOfColumns: Int = 0 var lastNumberOfColumns: Int = 0 - let debouncer = NCDebouncer(maxEventCount: 10) + let debouncerLoadDataSource = NCDebouncer(maxEventCount: 10) + let debouncerSearch = NCDebouncer(maxEventCount: 10) @MainActor var session: NCSession.Session { diff --git a/iOSClient/Menu/NCCollectionViewCommon+Menu.swift b/iOSClient/Menu/NCCollectionViewCommon+Menu.swift deleted file mode 100644 index 06d7f034b9..0000000000 --- a/iOSClient/Menu/NCCollectionViewCommon+Menu.swift +++ /dev/null @@ -1,429 +0,0 @@ -// -// NCCollectionViewCommon+Menu.swift -// Nextcloud -// -// Created by Philippe Weidmann on 24.01.20. -// Copyright © 2020 Philippe Weidmann. All rights reserved. -// Copyright © 2020 Marino Faggiana All rights reserved. -// Copyright © 2022 Henrik Storch. All rights reserved. -// -// Author Philippe Weidmann -// Author Marino Faggiana -// Author Henrik Storch -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// - -import UIKit -import FloatingPanel -import NextcloudKit -import Queuer - -extension NCCollectionViewCommon { - func toggleMenu(metadata: tableMetadata, image: UIImage?, sender: Any?) { - guard let metadata = database.getMetadataFromOcId(metadata.ocId), - let sceneIdentifier = self.controller?.sceneIdentifier else { - return - } - let tableLocalFile = database.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) - let fileExists = NCUtilityFileSystem().fileProviderStorageExists(metadata) - var actions = [NCMenuAction]() - var isOffline: Bool = false - let applicationHandle = NCApplicationHandle() - var iconHeader: UIImage! - let capabilities = NCNetworking.shared.capabilities[session.account] ?? NKCapabilities.Capabilities() - - if metadata.directory, let directory = database.getTableDirectory(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { - isOffline = directory.offline - } else if let localFile = database.getTableLocalFile(predicate: NSPredicate(format: "ocId == %@", metadata.ocId)) { - isOffline = localFile.offline - } - - if let image { - iconHeader = image - } else { - if metadata.directory { - iconHeader = imageCache.getFolder(account: metadata.account) - } else { - iconHeader = imageCache.getImageFile() - } - } - - actions.append( - NCMenuAction( - title: metadata.fileNameView, - boldTitle: true, - icon: iconHeader, - order: 0, - sender: sender, - action: nil - ) - ) - - actions.append(.seperator(order: 1, sender: sender)) - - // - // DETAILS - // - if NCNetworking.shared.isOnline, - !(!capabilities.fileSharingApiEnabled && !capabilities.filesComments && capabilities.activity.isEmpty) { - actions.append( - NCMenuAction( - title: NSLocalizedString("_details_", comment: ""), - icon: utility.loadImage(named: "info.circle", colors: [NCBrandColor.shared.iconImageColor]), - order: 10, - sender: sender, - action: { _ in - NCCreate().createShare(viewController: self, metadata: metadata, page: .activity) - } - ) - ) - } - - if metadata.lock { - var lockOwnerName = metadata.lockOwnerDisplayName.isEmpty ? metadata.lockOwner : metadata.lockOwnerDisplayName - var lockIcon = utility.loadUserImage(for: metadata.lockOwner, displayName: lockOwnerName, urlBase: metadata.urlBase) - if metadata.lockOwnerType != 0 { - lockOwnerName += " app" - if !metadata.lockOwnerEditor.isEmpty, let appIcon = UIImage(named: metadata.lockOwnerEditor) { - lockIcon = appIcon - } - } - - var lockTimeString: String? - if let lockTime = metadata.lockTimeOut { - if lockTime >= Date().addingTimeInterval(60), - let timeInterval = (lockTime.timeIntervalSince1970 - Date().timeIntervalSince1970).format() { - lockTimeString = String(format: NSLocalizedString("_time_remaining_", comment: ""), timeInterval) - } else if lockTime > Date() { - lockTimeString = NSLocalizedString("_less_a_minute_", comment: "") - } // else: don't show negative time - } - if let lockTime = metadata.lockTime, lockTimeString == nil { - lockTimeString = DateFormatter.localizedString(from: lockTime, dateStyle: .short, timeStyle: .short) - } - - actions.append( - NCMenuAction( - title: String(format: NSLocalizedString("_locked_by_", comment: ""), lockOwnerName), - details: lockTimeString, - icon: lockIcon, - order: 20, - sender: sender, - action: nil) - ) - } - - // - // VIEW IN FOLDER - // - if NCNetworking.shared.isOnline, - layoutKey != NCGlobal.shared.layoutViewFiles { - actions.append( - NCMenuAction( - title: NSLocalizedString("_view_in_folder_", comment: ""), - icon: utility.loadImage(named: "questionmark.folder", colors: [NCBrandColor.shared.iconImageColor]), - order: 21, - sender: sender, - action: { _ in - NCNetworking.shared.openFileViewInFolder(serverUrl: metadata.serverUrl, fileNameBlink: metadata.fileName, fileNameOpen: nil, sceneIdentifier: sceneIdentifier) - } - ) - ) - } - - // - // LOCK / UNLOCK - // - if NCNetworking.shared.isOnline, - !metadata.directory, - metadata.canUnlock(as: metadata.userId), - !capabilities.filesLockVersion.isEmpty { - actions.append(NCMenuAction.lockUnlockFiles(shouldLock: !metadata.lock, metadatas: [metadata], order: 30, sender: sender)) - } - - // - // SET FOLDER E2EE - // - if NCNetworking.shared.isOnline, - metadata.directory, - metadata.size == 0, - !metadata.e2eEncrypted, - NCPreferences().isEndToEndEnabled(account: metadata.account), - metadata.serverUrl == NCUtilityFileSystem().getHomeServer(urlBase: metadata.urlBase, userId: metadata.userId) { - actions.append( - NCMenuAction( - title: NSLocalizedString("_e2e_set_folder_encrypted_", comment: ""), - icon: utility.loadImage(named: "lock", colors: [NCBrandColor.shared.iconImageColor]), - order: 30, - sender: sender, - action: { _ in - Task { - let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee(account: metadata.account, serverUrlFileName: metadata.serverUrlFileName, userId: metadata.userId) - if error != .success { - NCContentPresenter().showError(error: error) - } - } - } - ) - ) - } - - // - // UNSET FOLDER E2EE - // - if NCNetworking.shared.isOnline, - metadata.canUnsetDirectoryAsE2EE { - actions.append( - NCMenuAction( - title: NSLocalizedString("_e2e_remove_folder_encrypted_", comment: ""), - icon: utility.loadImage(named: "lock", colors: [NCBrandColor.shared.iconImageColor]), - order: 30, - sender: sender, - action: { _ in - Task { - let results = await NextcloudKit.shared.markE2EEFolderAsync(fileId: metadata.fileId, delete: true, account: metadata.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, - path: metadata.fileId, - name: "markE2EEFolder") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } - if results.error == .success { - await self.database.deleteE2eEncryptionAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrlFileName)) - await self.database.setMetadataEncryptedAsync(ocId: metadata.ocId, encrypted: false) - await self.reloadDataSource() - } else { - NCContentPresenter().messageNotification(NSLocalizedString("_e2e_error_", comment: ""), error: results.error, delay: NCGlobal.shared.dismissAfterSecond, type: .error) - } - } - } - ) - ) - } - - // - // FAVORITE - if !metadata.lock { - actions.append( - NCMenuAction( - title: metadata.favorite ? NSLocalizedString("_remove_favorites_", comment: "") : NSLocalizedString("_add_favorites_", comment: ""), - icon: utility.loadImage(named: metadata.favorite ? "star.slash" : "star", colors: [NCBrandColor.shared.yellowFavorite]), - order: 50, - sender: sender, - action: { _ in - NCNetworking.shared.setStatusWaitFavorite(metadata) { error in - if error != .success { - NCContentPresenter().showError(error: error) - } - } - } - ) - ) - } - - // - // OFFLINE - // - if NCNetworking.shared.isOnline, - metadata.canSetAsAvailableOffline { - actions.append(.setAvailableOfflineAction(selectedMetadatas: [metadata], isAnyOffline: isOffline, viewController: self, order: 60, sender: sender, completion: { - Task { - await self.reloadDataSource() - } - })) - } - - // - // SHARE - // - if (NCNetworking.shared.isOnline || (tableLocalFile != nil && fileExists)) && metadata.canShare { - actions.append(.share(selectedMetadatas: [metadata], controller: self.controller, order: 80, sender: sender)) - } - - // - // SAVE LIVE PHOTO - // - if NCNetworking.shared.isOnline, - let metadataMOV = database.getMetadataLivePhoto(metadata: metadata) { - actions.append( - NCMenuAction( - title: NSLocalizedString("_livephoto_save_", comment: ""), - icon: NCUtility().loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor]), - order: 100, - sender: sender, - action: { _ in - NCNetworking.shared.saveLivePhotoQueue.addOperation(NCOperationSaveLivePhoto(metadata: metadata, metadataMOV: metadataMOV, controller: self.tabBarController)) - } - ) - ) - } - - // - // SAVE AS SCAN - // - if NCNetworking.shared.isOnline, - metadata.isSavebleAsImage { - actions.append( - NCMenuAction( - title: NSLocalizedString("_save_as_scan_", comment: ""), - icon: utility.loadImage(named: "doc.viewfinder", colors: [NCBrandColor.shared.iconImageColor]), - order: 110, - sender: sender, - action: { _ in - Task { - if self.utilityFileSystem.fileProviderStorageExists(metadata) { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: NCGlobal.shared.networkingStatusDownloaded, - account: metadata.account, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl, - selector: NCGlobal.shared.selectorSaveAsScan, - ocId: metadata.ocId, - destination: nil, - error: .success) - } - } else { - if let metadata = await self.database.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, - session: NCNetworking.shared.sessionDownload, - selector: NCGlobal.shared.selectorSaveAsScan, - sceneIdentifier: sceneIdentifier) { - await NCNetworking.shared.downloadFile(metadata: metadata) - } - } - } - } - ) - ) - } - - // - // RENAME - // - if metadata.isRenameable { - actions.append( - NCMenuAction( - title: NSLocalizedString("_rename_", comment: ""), - icon: utility.loadImage(named: "text.cursor", colors: [NCBrandColor.shared.iconImageColor]), - order: 120, - sender: sender, - action: { _ in - Task { @MainActor in - let capabilities = await NKCapabilities.shared.getCapabilities(for: metadata.account) - let fileNameNew = await UIAlertController.renameFileAsync(fileName: metadata.fileNameView, isDirectory: metadata.directory, capabilities: capabilities, account: metadata.account, presenter: self) - - if await NCManageDatabase.shared.getMetadataAsync(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, fileNameNew)) != nil { - NCContentPresenter().showError(error: NKError(errorCode: 0, errorDescription: "_rename_already_exists_")) - return - } - - NCNetworking.shared.setStatusWaitRename(metadata, fileNameNew: fileNameNew) - } - } - ) - ) - } - - // - // COPY - MOVE - // - if metadata.isCopyableMovable { - actions.append(.moveOrCopyAction(selectedMetadatas: [metadata], account: metadata.account, viewController: self, order: 130, sender: sender)) - } - - // - // MODIFY WITH QUICK LOOK - // - if NCNetworking.shared.isOnline, - metadata.isModifiableWithQuickLook { - actions.append( - NCMenuAction( - title: NSLocalizedString("_modify_", comment: ""), - icon: utility.loadImage(named: "pencil.tip.crop.circle", colors: [NCBrandColor.shared.iconImageColor]), - order: 150, - sender: sender, - action: { _ in - Task { - if self.utilityFileSystem.fileProviderStorageExists(metadata) { - await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: NCGlobal.shared.networkingStatusDownloaded, - account: metadata.account, - fileName: metadata.fileName, - serverUrl: metadata.serverUrl, - selector: NCGlobal.shared.selectorLoadFileQuickLook, - ocId: metadata.ocId, - destination: nil, - error: .success) - } - } else { - if let metadata = await self.database.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, - session: NCNetworking.shared.sessionDownload, - selector: NCGlobal.shared.selectorLoadFileQuickLook, - sceneIdentifier: sceneIdentifier) { - await NCNetworking.shared.downloadFile(metadata: metadata) - } - } - } - } - ) - ) - } - - // - // COLOR FOLDER - // - if self is NCFiles, - metadata.directory { - actions.append( - NCMenuAction( - title: NSLocalizedString("_change_color_", comment: ""), - icon: utility.loadImage(named: "paintpalette", colors: [NCBrandColor.shared.iconImageColor]), - order: 160, - sender: sender, - action: { _ in - if let picker = UIStoryboard(name: "NCColorPicker", bundle: nil).instantiateInitialViewController() as? NCColorPicker { - picker.metadata = metadata - picker.collectionViewCommon = self - let popup = NCPopupViewController(contentController: picker, popupWidth: 200, popupHeight: 320) - popup.backgroundAlpha = 0 - self.present(popup, animated: true) - } - } - ) - ) - } - - // - // DELETE - // - if metadata.isDeletable { - actions.append(.deleteOrUnshareAction(selectedMetadatas: [metadata], metadataFolder: metadataFolder, controller: self.controller, order: 170, sender: sender)) - } - - applicationHandle.addCollectionViewCommonMenu(metadata: metadata, image: image, actions: &actions) - - presentMenu(with: actions, controller: controller, sender: sender) - } -} - -extension TimeInterval { - func format() -> String? { - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour, .minute] - formatter.unitsStyle = .full - formatter.maximumUnitCount = 1 - return formatter.string(from: self) - } -} diff --git a/iOSClient/Menu/NCContextMenu.swift b/iOSClient/Menu/NCContextMenu.swift index b69dd7d94d..698d766d9e 100644 --- a/iOSClient/Menu/NCContextMenu.swift +++ b/iOSClient/Menu/NCContextMenu.swift @@ -1,225 +1,567 @@ // SPDX-FileCopyrightText: Nextcloud GmbH // SPDX-FileCopyrightText: 2023 Marino Faggiana +// SPDX-FileCopyrightText: 2025 Milen Pivchev // SPDX-License-Identifier: GPL-3.0-or-later import Foundation import UIKit +import SwiftUI import Alamofire import NextcloudKit +import SVGKit import LucidBanner class NCContextMenu: NSObject { let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() - let database = NCManageDatabase.shared - let global = NCGlobal.shared - let networking = NCNetworking.shared let metadata: tableMetadata let sceneIdentifier: String let viewController: UIViewController - let image: UIImage? let sender: Any? - init(metadata: tableMetadata, viewController: UIViewController, sceneIdentifier: String, image: UIImage?, sender: Any?) { + init(metadata: tableMetadata, viewController: UIViewController, sceneIdentifier: String, sender: Any?) { self.metadata = metadata self.viewController = viewController self.sceneIdentifier = sceneIdentifier - self.image = image self.sender = sender } func viewMenu() -> UIMenu { - var downloadRequest: DownloadRequest? - var titleDeleteConfirmFile = NSLocalizedString("_delete_file_", comment: "") - let metadataMOV = self.database.getMetadataLivePhoto(metadata: metadata) - let scene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene + guard let capabilities = NCNetworking.shared.capabilities[metadata.account] else { + return UIMenu() + } + + // Build top menu items + let detail = makeDetailAction(metadata: metadata) + let favorite = makeFavoriteAction(metadata: metadata) + let share = makeShareAction() + + let mainActionsMenu = buildMainActionsMenu( + metadata: metadata, + capabilities: capabilities + ) + + let clientIntegrationMenu = buildClientIntegrationMenuItems( + capabilities: capabilities, + metadata: metadata + ) + + let deleteMenu = buildDeleteMenu(metadata: metadata) + + // Assemble final menu + if NCNetworking.shared.isOnline { + let baseChildren = [ + UIMenu(title: "", options: .displayInline, children: mainActionsMenu), + UIMenu(title: "", options: .displayInline, children: clientIntegrationMenu), + UIMenu(title: "", options: .displayInline, children: deleteMenu) + ] - if metadata.directory { titleDeleteConfirmFile = NSLocalizedString("_delete_folder_", comment: "") } + let finalMenu = UIMenu(title: "", children: (metadata.lock ? [detail] : [detail, share, favorite]) + baseChildren) + finalMenu.preferredElementSize = .medium - // MENU ITEMS + return finalMenu + } else { + return UIMenu() + } + } - let detail = UIAction(title: NSLocalizedString("_details_", comment: ""), - image: utility.loadImage(named: "info.circle")) { _ in - NCCreate().createShare(viewController: self.viewController, metadata: self.metadata, page: .activity) + // MARK: Basic Actions + + private func makeDetailAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_details_", comment: ""), + image: utility.loadImage(named: "info.circle.fill") + ) { _ in + NCCreate().createShare(viewController: self.viewController, metadata: metadata, page: .activity) } + } - let favorite = UIAction(title: metadata.favorite ? - NSLocalizedString("_remove_favorites_", comment: "") : - NSLocalizedString("_add_favorites_", comment: ""), - image: utility.loadImage(named: self.metadata.favorite ? "star.slash" : "star", colors: [NCBrandColor.shared.yellowFavorite])) { _ in - self.networking.setStatusWaitFavorite(self.metadata) { error in + private func makeFavoriteAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: metadata.favorite ? + NSLocalizedString("_remove_favorites_", comment: "") : + NSLocalizedString("_add_favorites_", comment: ""), + image: utility.loadImage( + named: metadata.favorite ? "star.slash.fill" : "star.fill", + colors: [NCBrandColor.shared.yellowFavorite] + ) + ) { _ in + NCNetworking.shared.setStatusWaitFavorite(metadata) { error in if error != .success { NCContentPresenter().showError(error: error) } } } + } - let share = UIAction(title: NSLocalizedString("_share_", comment: ""), - image: utility.loadImage(named: "square.and.arrow.up") ) { _ in - Task {@MainActor in + private func makeShareAction() -> UIAction { + return UIAction( + title: NSLocalizedString("_share_", comment: ""), + image: utility.loadImage(named: "square.and.arrow.up.fill") + ) { _ in + Task { @MainActor in let controller = self.viewController.tabBarController as? NCMainTabBarController - await NCCreate().createActivityViewController(selectedMetadata: [self.metadata], - controller: controller, - sender: self.sender) + await NCCreate().createActivityViewController( + selectedMetadata: [self.metadata], + controller: controller, + sender: self.sender + ) } } + } + + // MARK: Main Actions Menu + + private func buildMainActionsMenu( + metadata: tableMetadata, + capabilities: NKCapabilities.Capabilities + ) -> [UIMenuElement] { + var mainActionsMenu: [UIMenuElement] = [] + // Lock/Unlock + if NCNetworking.shared.isOnline, + !metadata.directory, + metadata.canUnlock(as: metadata.userId), + !capabilities.filesLockVersion.isEmpty { + mainActionsMenu.append( + ContextMenuActions.lockUnlock(shouldLock: !metadata.lock, metadatas: [metadata]) + ) + } + + // E2EE actions + addE2EEActions(metadata: metadata, capabilities: capabilities, mainActionsMenu: &mainActionsMenu) + + // Offline + if NCNetworking.shared.isOnline, + metadata.canSetAsAvailableOffline { + mainActionsMenu.append( + ContextMenuActions.setAvailableOffline( + selectedMetadatas: [metadata], + isAnyOffline: metadata.isOffline, + viewController: viewController + ) + ) + } + + // Save as scan + if NCNetworking.shared.isOnline, + metadata.isSavebleAsImage { + mainActionsMenu.append(makeSaveAsScanAction(metadata: metadata)) + } + + // Rename + if metadata.isRenameable { + mainActionsMenu.append(makeRenameAction(metadata: metadata)) + } + + // Move/Copy + if metadata.isCopyableMovable { + mainActionsMenu.append( + ContextMenuActions.moveOrCopy( + selectedMetadatas: [metadata], + account: metadata.account, + viewController: viewController + ) + ) + } - let viewInFolder = UIAction(title: NSLocalizedString("_view_in_folder_", comment: ""), - image: utility.loadImage(named: "questionmark.folder")) { _ in - NCNetworking.shared.openFileViewInFolder(serverUrl: self.metadata.serverUrl, fileNameBlink: self.metadata.fileName, fileNameOpen: nil, sceneIdentifier: self.sceneIdentifier) + // Modify with Quick Look + if NCNetworking.shared.isOnline, + metadata.isModifiableWithQuickLook { + mainActionsMenu.append(makeModifyWithQuickLookAction(metadata: metadata)) } - let livePhotoSave = UIAction(title: NSLocalizedString("_livephoto_save_", comment: ""), image: utility.loadImage(named: "livephoto")) { _ in - if let metadataMOV = metadataMOV { - self.networking.saveLivePhotoQueue.addOperation(NCOperationSaveLivePhoto(metadata: self.metadata, metadataMOV: metadataMOV, controller: self.viewController.tabBarController)) + // Color folder + if viewController is NCFiles, + metadata.directory { + mainActionsMenu.append(makeColorFolderAction(metadata: metadata)) + } + + return mainActionsMenu + } + + // MARK: E2EE Actions + + private func addE2EEActions( + metadata: tableMetadata, + capabilities: NKCapabilities.Capabilities, + mainActionsMenu: inout [UIMenuElement] + ) { + // Set folder E2EE + if NCNetworking.shared.isOnline, + metadata.directory, + metadata.size == 0, + !metadata.e2eEncrypted, + NCPreferences().isEndToEndEnabled(account: metadata.account), + metadata.serverUrl == self.utilityFileSystem.getHomeServer(urlBase: metadata.urlBase, userId: metadata.userId) { + mainActionsMenu.append(makeSetFolderE2EEAction(metadata: metadata)) + } + + // Unset folder E2EE + if NCNetworking.shared.isOnline, + metadata.canUnsetDirectoryAsE2EE { + mainActionsMenu.append(makeUnsetFolderE2EEAction(metadata: metadata)) + } + } + + private func makeSetFolderE2EEAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_e2e_set_folder_encrypted_", comment: ""), + image: utility.loadImage(named: "lock", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + Task { + let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee( + account: metadata.account, + serverUrlFileName: metadata.serverUrlFileName, + userId: metadata.userId + ) + if error != .success { + NCContentPresenter().showError(error: error) + } } } + } - let modify = UIAction(title: NSLocalizedString("_modify_", comment: ""), - image: utility.loadImage(named: "pencil.tip.crop.circle")) { _ in - Task { @MainActor in - if self.utilityFileSystem.fileProviderStorageExists(self.metadata) { - await self.networking.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: self.global.networkingStatusDownloaded, - account: self.metadata.account, - fileName: self.metadata.fileName, - serverUrl: self.metadata.serverUrl, - selector: self.global.selectorLoadFileQuickLook, - ocId: self.metadata.ocId, - destination: nil, - error: .success) + private func makeUnsetFolderE2EEAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_e2e_remove_folder_encrypted_", comment: ""), + image: utility.loadImage(named: "lock", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + Task { + let results = await NextcloudKit.shared.markE2EEFolderAsync( + fileId: metadata.fileId, + delete: true, + account: metadata.account + ) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier( + account: metadata.account, + path: metadata.fileId, + name: "markE2EEFolder" + ) + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } + } + + if results.error == .success { + await NCManageDatabase.shared.deleteE2eEncryptionAsync( + predicate: NSPredicate( + format: "account == %@ AND serverUrl == %@", + metadata.account, + metadata.serverUrlFileName + ) + ) + await NCManageDatabase.shared.setMetadataEncryptedAsync(ocId: metadata.ocId, encrypted: false) + await (self.viewController as? NCCollectionViewCommon)?.reloadDataSource() } else { - guard let metadata = await self.database.setMetadataSessionInWaitDownloadAsync(ocId: self.metadata.ocId, - session: self.networking.sessionDownload, - selector: self.global.selectorLoadFileQuickLook, - sceneIdentifier: self.sceneIdentifier) else { - return - } + NCContentPresenter().messageNotification( + NSLocalizedString("_e2e_error_", comment: ""), + error: results.error, + delay: NCGlobal.shared.dismissAfterSecond, + type: .error + ) + } + } + } + } - let token = showHudBanner(scene: scene, - title: NSLocalizedString("_download_in_progress_", comment: ""), - stage: .button) { - if let request = downloadRequest { - request.cancel() - } - } + // MARK: File Actions - let results = await self.networking.downloadFile(metadata: metadata) { request in - downloadRequest = request - } progressHandler: { progress in - Task {@MainActor in - LucidBanner.shared.update(progress: progress.fractionCompleted, for: token) - } + private func makeSaveAsScanAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_save_as_scan_", comment: ""), + image: utility.loadImage(named: "doc.viewfinder", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + Task { + if self.utilityFileSystem.fileProviderStorageExists(metadata) { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferChange( + status: NCGlobal.shared.networkingStatusDownloaded, + account: metadata.account, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl, + selector: NCGlobal.shared.selectorSaveAsScan, + ocId: metadata.ocId, + destination: nil, + error: .success + ) } - LucidBanner.shared.dismiss() - - if results.nkError == .success || results.afError?.isExplicitlyCancelledError ?? false { - // - } else { - await showErrorBanner(scene: scene, errorDescription: results.nkError.errorDescription, errorCode: results.nkError.errorCode) + } else { + if let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync( + ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: NCGlobal.shared.selectorSaveAsScan, + sceneIdentifier: self.sceneIdentifier + ) { + await NCNetworking.shared.downloadFile(metadata: metadata) } } } } + } - let deleteConfirmFile = UIAction(title: titleDeleteConfirmFile, - image: utility.loadImage(named: "trash"), attributes: .destructive) { _ in + private func makeRenameAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_rename_", comment: ""), + image: utility.loadImage(named: "text.cursor", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + Task { @MainActor in + let capabilities = await NKCapabilities.shared.getCapabilities(for: metadata.account) + let fileNameNew = await UIAlertController.renameFileAsync( + fileName: metadata.fileNameView, + isDirectory: metadata.directory, + capabilities: capabilities, + account: metadata.account, + presenter: self.viewController + ) + + if await NCManageDatabase.shared.getMetadataAsync( + predicate: NSPredicate( + format: "account == %@ AND serverUrl == %@ AND fileName == %@", + metadata.account, + metadata.serverUrl, + fileNameNew + ) + ) != nil { + NCContentPresenter().showError( + error: NKError(errorCode: 0, errorDescription: "_rename_already_exists_") + ) + return + } - var alertStyle = UIAlertController.Style.actionSheet - if UIDevice.current.userInterfaceIdiom == .pad { - alertStyle = .alert + NCNetworking.shared.setStatusWaitRename(metadata, fileNameNew: fileNameNew) } - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: alertStyle) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_delete_file_", comment: ""), style: .destructive) { _ in - if let viewController = self.viewController as? NCCollectionViewCommon { - Task { - await self.networking.setStatusWaitDelete(metadatas: [self.metadata], sceneIdentifier: self.sceneIdentifier) - await viewController.reloadDataSource() + } + } + + private func makeModifyWithQuickLookAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_modify_", comment: ""), + image: utility.loadImage(named: "pencil.tip.crop.circle", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + Task { + if self.utilityFileSystem.fileProviderStorageExists(metadata) { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferChange( + status: NCGlobal.shared.networkingStatusDownloaded, + account: metadata.account, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl, + selector: NCGlobal.shared.selectorLoadFileQuickLook, + ocId: metadata.ocId, + destination: nil, + error: .success + ) } - } - if let viewController = self.viewController as? NCMedia { - Task { - await viewController.deleteImage(with: self.metadata.ocId) + } else { + if let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync( + ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: NCGlobal.shared.selectorLoadFileQuickLook, + sceneIdentifier: self.sceneIdentifier + ) { + await NCNetworking.shared.downloadFile(metadata: metadata) } } - }) - alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { _ in }) - self.viewController.present(alertController, animated: true, completion: nil) + } + } + } + + private func makeColorFolderAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_change_color_", comment: ""), + image: utility.loadImage(named: "paintpalette", colors: [NCBrandColor.shared.iconImageColor]) + ) { _ in + if let picker = UIStoryboard(name: "NCColorPicker", bundle: nil) + .instantiateInitialViewController() as? NCColorPicker { + + picker.metadata = metadata + picker.collectionViewCommon = self.viewController as? NCFiles + let popup = NCPopupViewController( + contentController: picker, + popupWidth: 200, + popupHeight: 320 + ) + popup.backgroundAlpha = 0 + self.viewController.present(popup, animated: true) + } } + } + + // MARK: Delete Menu + + private func buildDeleteMenu(metadata: tableMetadata) -> [UIMenuElement] { + var deleteMenu: [UIMenuElement] = [] + let deleteConfirmLocal = makeDeleteLocalAction(metadata: metadata) + let deleteConfirmFile = makeDeleteFileAction(metadata: metadata) + + let deleteSubMenu = UIMenu( + title: NSLocalizedString("_delete_", comment: ""), + image: utility.loadImage(named: "trash"), + options: .destructive, + children: [deleteConfirmLocal, deleteConfirmFile] + ) - let deleteConfirmLocal = UIAction(title: NSLocalizedString("_remove_local_file_", comment: ""), - image: utility.loadImage(named: "trash"), attributes: .destructive) { _ in + if metadata.directory { + if !metadata.isDirectoryE2EE && !metadata.e2eEncrypted { + deleteMenu.append(deleteSubMenu) + } + } else { + if !metadata.lock { + deleteMenu.append(deleteSubMenu) + } + } + + return deleteMenu + } + + private func makeDeleteFileAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString( + metadata.directory ? "_delete_folder_" : "_delete_file_", + comment: "" + ), + image: utility.loadImage(named: "trash"), + attributes: .destructive + ) { _ in + if let viewController = self.viewController as? NCCollectionViewCommon { + Task { + await NCNetworking.shared.setStatusWaitDelete( + metadatas: [metadata], + sceneIdentifier: self.sceneIdentifier + ) + await viewController.reloadDataSource() + } + } else if let viewController = self.viewController as? NCMedia { + Task { + await viewController.deleteImage(with: metadata.ocId) + } + } + } + } + + private func makeDeleteLocalAction(metadata: tableMetadata) -> UIAction { + return UIAction( + title: NSLocalizedString("_remove_local_file_", comment: ""), + image: utility.loadImage(named: "trash"), + attributes: .destructive + ) { _ in Task { - let error = await self.networking.deleteCache(self.metadata, sceneIdentifier: self.sceneIdentifier) - - await self.networking.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: NCGlobal.shared.networkingStatusDelete, - account: self.metadata.account, - fileName: self.metadata.fileName, - serverUrl: self.metadata.serverUrl, - selector: self.metadata.sessionSelector, - ocId: self.metadata.ocId, - destination: nil, - error: error) + let error = await NCNetworking.shared.deleteCache( + metadata, + sceneIdentifier: self.sceneIdentifier + ) + + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferChange( + status: NCGlobal.shared.networkingStatusDelete, + account: metadata.account, + fileName: metadata.fileName, + serverUrl: metadata.serverUrl, + selector: metadata.sessionSelector, + ocId: metadata.ocId, + destination: nil, + error: error + ) } } } + } - let deleteSubMenu = UIMenu(title: NSLocalizedString("_delete_file_", comment: ""), - image: utility.loadImage(named: "trash"), - options: .destructive, - children: [deleteConfirmLocal, deleteConfirmFile]) + // MARK: Client Integration - // ------ MENU ----- + private func buildClientIntegrationMenuItems(capabilities: NKCapabilities.Capabilities, metadata: tableMetadata) -> [UIMenuElement] { + var clientIntegrationMenu: [UIMenuElement] = [] + guard let apps = capabilities.clientIntegration?.apps else { return [] } - var menu: [UIMenuElement] = [] + for (_, context) in apps { + for item in context.contextMenu { + var shouldShowMenu = false - if self.networking.isOnline { - if metadata.directory { - if metadata.isDirectoryE2EE || metadata.e2eEncrypted { - menu.append(favorite) + if let mimetypeFilters = item.mimetypeFilters { + let filters = mimetypeFilters.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } + shouldShowMenu = filters.contains(where: { filter in // If app has specific mimetypes, we should only show the menu if the file/folder matches one of them. + if filter.hasSuffix("/") { + // Handle wildcard MIME types like "audio/", "video/", "image/" + return metadata.contentType.hasPrefix(filter) + } else { + return metadata.contentType == filter + } + }) } else { - menu.append(favorite) - menu.append(deleteConfirmFile) + shouldShowMenu = true // if app has no mimetypes, then menu should be shown for every file/folder } - return UIMenu(title: "", children: [detail, UIMenu(title: "", options: .displayInline, children: menu)]) - } else { - if metadata.lock { - menu.append(favorite) - menu.append(share) - - if self.database.getMetadataLivePhoto(metadata: metadata) != nil { - menu.append(livePhotoSave) - } - } else { - menu.append(favorite) - menu.append(share) - if self.database.getMetadataLivePhoto(metadata: metadata) != nil { - menu.append(livePhotoSave) - } + if shouldShowMenu { + let deferredElement = UIDeferredMenuElement { completion in + Task { + func resizedRasterImage(_ image: UIImage, to size: CGSize) -> UIImage { + let format = UIGraphicsImageRendererFormat.default() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: size)) + }.withRenderingMode(image.renderingMode) + } - if viewController is NCMedia { - menu.append(viewInFolder) - } + var iconImage: UIImage - // MODIFY WITH QUICK LOOK - if metadata.isModifiableWithQuickLook { - menu.append(modify) - } + if let iconUrl = item.icon, + let url = URL(string: metadata.urlBase + iconUrl) { + let (data, _) = try await URLSession.shared.data(from: url) + let svgkImage = SVGKImage(data: data)?.uiImage.withRenderingMode(.alwaysTemplate) + iconImage = resizedRasterImage(svgkImage ?? UIImage(), to: .init(width: 23, height: 23)) + } else { + iconImage = UIImage() + } + + let action = await UIAction( + title: item.name, + image: iconImage + ) { _ in + Task { + let response = await NextcloudKit.shared.sendRequestAsync(account: metadata.account, + fileId: metadata.fileId, + filePath: self.utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId), + url: item.url, + method: item.method, + params: item.params) + + if response.error != .success { + NCContentPresenter().showError(error: response.error) + } else { + if let tooltip = response.uiResponse?.ocs.data.tooltip { + NCContentPresenter().showCustomMessage(message: tooltip, type: .success) + } else { + let baseURL = metadata.urlBase + + await MainActor.run { + guard let ui = response.uiResponse?.ocs.data.root, let firstRow = ui.rows.first, let child = firstRow.children.first else { return } - if viewController is NCMedia { - menu.append(deleteConfirmFile) - } else { - menu.append(deleteSubMenu) + let viewer = ClientIntegrationUIViewer( + rows: [.init(element: child.element, title: child.text, urlString: child.url)], + baseURL: baseURL + ) + let hosting = UIHostingController(rootView: viewer) + hosting.modalPresentationStyle = .pageSheet + self.viewController.present(hosting, animated: true) + } + } + } + } + } + + await MainActor.run { + completion([action]) + } + } } + + clientIntegrationMenu.append(deferredElement) } - return UIMenu(title: "", children: [detail, UIMenu(title: "", options: .displayInline, children: menu)]) } - } else { - return UIMenu() } + + return clientIntegrationMenu } } diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index e257881739..3edfec9c13 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -284,6 +284,8 @@ final class NCGlobal: Sendable { let notificationCenterPlayerIsPlaying = "playerIsPlaying" let notificationCenterPlayerStoppedPlaying = "playerStoppedPlaying" + let notificationCenterUserInteractionMonitor = "serInteractionMonitor" + // Networking Status let networkingStatusCreateFolder = "statusCreateFolder" let networkingStatusDelete = "statusDelete" diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EERename.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EERename.swift index d1e061ba5a..1302e58058 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EERename.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EERename.swift @@ -42,7 +42,7 @@ class NCNetworkingE2EERename: NSObject { // DB RENAME // - let newFileNamePath = utilityFileSystem.getFileNamePath(fileNameNew, serverUrl: metadata.serverUrl, session: session) + let newFileNamePath = utilityFileSystem.getRelativeFilePath(fileNameNew, serverUrl: metadata.serverUrl, session: session) await self.database.renameFileE2eEncryptionAsync(account: metadata.account, serverUrl: metadata.serverUrl, fileNameIdentifier: metadata.fileName, newFileName: fileNameNew, newFileNamePath: newFileNamePath) // UPLOAD METADATA diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift index fc10b27b2a..81f7c5ae75 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EEUpload.swift @@ -200,7 +200,7 @@ class NCNetworkingE2EEUpload: NSObject { utility.createImageFileFrom(metadata: metadata) await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferChange(status: global.networkingStatusUploaded, + delegate.transferChange(status: self.global.networkingStatusUploaded, account: metadata.account, fileName: metadata.fileName, serverUrl: metadata.serverUrl, diff --git a/iOSClient/Networking/NCNetworking+Actor.swift b/iOSClient/Networking/NCNetworking+Actor.swift new file mode 100644 index 0000000000..e262af03a8 --- /dev/null +++ b/iOSClient/Networking/NCNetworking+Actor.swift @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2019 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import Alamofire + +/// Actor-based dispatcher that manages weak NCTransferDelegate references +/// and delivers notifications safely across concurrency domains. +actor NCTransferDelegateDispatcher { + // Weak reference collection of delegates + private var transferDelegates = NSHashTable.weakObjects() + + /// Adds a delegate safely. + func addDelegate(_ delegate: NCTransferDelegate) { + if transferDelegates.contains(delegate) { + return + } + transferDelegates.add(delegate) + } + + /// Remove a delegate safely. + func removeDelegate(_ delegate: NCTransferDelegate) { + transferDelegates.remove(delegate) + } + + /// Returns a strong snapshot of all valid delegates. + private func snapshotDelegates() -> [NCTransferDelegate] { + transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } + } + + /// Notifies all delegates on the main actor. + func notifyAllDelegates(_ block: @MainActor @escaping (NCTransferDelegate) -> Void) async { + let delegates = snapshotDelegates() + await MainActor.run { + for delegate in delegates { + block(delegate) + } + } + } + + /// Notifies only the delegate matching a specific scene identifier. + func notifyDelegate(forScene sceneIdentifier: String, _ block: @MainActor @escaping (NCTransferDelegate) -> Void) async { + let delegates = snapshotDelegates() + await MainActor.run { + for delegate in delegates where delegate.sceneIdentifier == sceneIdentifier { + block(delegate) + } + } + } + + /// Notifies matching and non-matching delegates on the main actor. + func notifyDelegates(forScene sceneIdentifier: String, matching: @MainActor @escaping (NCTransferDelegate) -> Void, others: @MainActor @escaping (NCTransferDelegate) -> Void) async { + let delegates = snapshotDelegates() + await MainActor.run { + for delegate in delegates { + if delegate.sceneIdentifier == sceneIdentifier { + matching(delegate) + } else { + others(delegate) + } + } + } + } + + /// Notifies all delegates concurrently using async/await. + func notifyAllDelegatesAsync(_ block: @escaping @Sendable (NCTransferDelegate) async -> Void) async { + let delegates = snapshotDelegates() + await withTaskGroup(of: Void.self) { group in + for delegate in delegates { + group.addTask { + await block(delegate) + } + } + } + } +} + +/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. +/// +/// Each task is associated with a string identifier (`identifier`) that you define, +/// allowing you to check whether a request is already running, avoid duplicates, +/// and cancel all active tasks at once. The registry automatically removes +/// completed tasks via `cleanupCompleted()` to keep memory usage compact. +/// +/// Typical use cases: +/// - Ensure only one task per identifier is active at a time. +/// - Query whether a specific request is still running (`isReading`). +/// - Forcefully stop a specific request (`cancel`). +/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). +actor NetworkingTasks { + private var active: [(identifier: String, task: URLSessionTask)] = [] + + /// Returns whether there is an in-flight task for the given URL. + /// + /// A task is considered in-flight if its `state` is `.running` or `.suspended`. + /// - Parameter identifier: The identifier to check. + /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. + func isReading(identifier: String) -> Bool { + // Drop finished/canceling tasks globally + cleanup() + + return active.contains { + $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) + } + } + + /// Tracks a newly created `URLSessionTask` for the given identifier. + /// + /// If a running entry for the same identifier exists, it is removed before appending the new one. + /// - Parameters: + /// - identifier: The identifier associated with the task. + /// - task: The `URLSessionTask` to track. + func track(identifier: String, task: URLSessionTask) { + // Drop finished/canceling tasks globally + cleanup() + + active.removeAll { + $0.identifier == identifier && $0.task.state == .running + } + active.append((identifier, task)) + nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) + } + + /// create a Identifier + /// + func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { + if let account, + let path { + return account + "_" + path + "_" + name + } else if let path { + return path + "_" + name + } else { + return name + } + } + + /// Cancels and removes all tasks associated with the given id. + /// + /// - Parameter identifier: The identifier whose tasks should be canceled. + func cancel(identifier: String) { + // Drop finished/canceling tasks globally + cleanup() + + for element in active where element.identifier == identifier { + element.task.cancel() + nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) + } + active.removeAll { + $0.identifier == identifier + } + } + + /// Cancels all tracked `URLSessionTask` and clears the registry. + /// + /// Call this when leaving the page/screen or when the operation must be forcefully stopped. + func cancelAll() { + active.forEach { + $0.task.cancel() + nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) + } + active.removeAll() + } + + /// Removes tasks that have completed from the registry. + /// + /// Useful to keep the in-memory list compact during long-running operations. + func cleanup() { + active.removeAll { + $0.task.state == .completed || $0.task.state == .canceling + } + } +} + +/// Quantizes per-task progress updates to integer percentages (0...100). +/// Each (serverUrlFileName) pair is tracked separately, so you get +/// at most one update per integer percent for each transfer. +actor ProgressQuantizer { + private var lastPercent: [String: Int] = [:] + + /// Returns `true` only when integer percent changes (or hits 100). + /// + /// - Parameters: + /// - serverUrlFileName: The name of the file being transferred. + /// - fraction: Progress fraction [0.0 ... 1.0]. + func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { + let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) + + let last = lastPercent[serverUrlFileName] ?? -1 + guard percent != last || percent == 100 else { + return false + } + + lastPercent[serverUrlFileName] = percent + return true + } + + /// Clears stored state for a finished transfer. + func clear(serverUrlFileName: String) { + lastPercent.removeValue(forKey: serverUrlFileName) + } +} diff --git a/iOSClient/Networking/NCNetworking+Recommendations.swift b/iOSClient/Networking/NCNetworking+Recommendations.swift index c52e22d688..a61c1dd7f0 100644 --- a/iOSClient/Networking/NCNetworking+Recommendations.swift +++ b/iOSClient/Networking/NCNetworking+Recommendations.swift @@ -50,7 +50,10 @@ extension NCNetworking { } await NCManageDatabase.shared.createRecommendedFilesAsync(account: session.account, recommendations: recommendationsToInsert) - await collectionView.reloadData() + + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferReloadData(serverUrl: serverUrl) + } } } } diff --git a/iOSClient/Networking/NCNetworking+Task.swift b/iOSClient/Networking/NCNetworking+Task.swift index 2887838214..7680895b5b 100644 --- a/iOSClient/Networking/NCNetworking+Task.swift +++ b/iOSClient/Networking/NCNetworking+Task.swift @@ -110,7 +110,7 @@ extension NCNetworking { await networking.transferDispatcher.notifyAllDelegates { delegate in serverUrls.forEach { serverUrl in - delegate.transferReloadData(serverUrl: serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: nil) } } } diff --git a/iOSClient/Networking/NCNetworking+TransferDelegate.swift b/iOSClient/Networking/NCNetworking+TransferDelegate.swift index 3a51057f66..87e113ddff 100644 --- a/iOSClient/Networking/NCNetworking+TransferDelegate.swift +++ b/iOSClient/Networking/NCNetworking+TransferDelegate.swift @@ -13,6 +13,12 @@ extension NCNetworking: NCTransferDelegate { } } + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, @@ -116,9 +122,7 @@ extension NCNetworking: NCTransferDelegate { guard hasPermission else { Task {@MainActor in let error = NKError(errorCode: NCGlobal.shared.errorFileNotSaved, errorDescription: "_access_photo_not_enabled_msg_") - await showErrorBanner(scene: scene, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(scene: scene, errorDescription: error.errorDescription) } return } @@ -134,9 +138,7 @@ extension NCNetworking: NCTransferDelegate { }) { success, _ in if !success { Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: errorSave.errorDescription, - errorCode: errorSave.errorCode) + await showErrorBanner(scene: scene, errorDescription: errorSave.errorDescription) } } } @@ -146,25 +148,19 @@ extension NCNetworking: NCTransferDelegate { }) { success, _ in if !success { Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: errorSave.errorDescription, - errorCode: errorSave.errorCode) + await showErrorBanner(scene: scene, errorDescription: errorSave.errorDescription) } } } } else { Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: errorSave.errorDescription, - errorCode: errorSave.errorCode) + await showErrorBanner(scene: scene, errorDescription: errorSave.errorDescription) } return } } catch { Task {@MainActor in - await showErrorBanner(scene: scene, - errorDescription: errorSave.errorDescription, - errorCode: errorSave.errorCode) + await showErrorBanner(scene: scene, errorDescription: errorSave.errorDescription) } } } @@ -234,9 +230,7 @@ extension NCNetworking: NCTransferDelegate { } guard resultsFile.error == .success, let file = resultsFile.file else { Task {@MainActor in - await showErrorBanner(controller: viewController.tabBarController, - errorDescription: resultsFile.error.errorDescription, - errorCode: resultsFile.error.errorCode) + await showErrorBanner(controller: viewController.tabBarController, errorDescription: resultsFile.error.errorDescription) } return } diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 2d3d19b5e7..f96b565613 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -292,7 +292,7 @@ extension NCNetworking { destination: nil, error: error) } others: { delegate in - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: metadata.serverUrl, requestData: false, status: nil) } } else { await transferDispatcher.notifyAllDelegates { delegate in @@ -355,7 +355,7 @@ extension NCNetworking { await deleteLocalFile(metadata: metadata) await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: metadata.serverUrl, requestData: false, status: nil) } } @@ -448,13 +448,15 @@ extension NCNetworking { serverUrls.insert(metadata.serverUrl) } + let ocIdss = ocIds + let serverUrlss = serverUrls await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - for ocId in ocIds { + for ocId in ocIdss { await NCManageDatabase.shared.setMetadataSessionAsync(ocId: ocId, status: self.global.metadataStatusWaitDelete) } - serverUrls.forEach { serverUrl in - delegate.transferReloadData(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitDelete) + serverUrlss.forEach { serverUrl in + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitDelete) } } } @@ -531,9 +533,11 @@ extension NCNetworking { #endif } else { Task { + let ocId = metadata.ocId + let serverUrl = metadata.serverUrl await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: metadata.ocId, status: self.global.metadataStatusWaitRename) - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: self.global.metadataStatusWaitRename) + await NCManageDatabase.shared.renameMetadata(fileNameNew: fileNameNew, ocId: ocId, status: self.global.metadataStatusWaitRename) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitRename) } } } @@ -583,9 +587,11 @@ extension NCNetworking { } Task { + let ocId = metadata.ocId + let serverUrl = metadata.serverUrl await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - await NCManageDatabase.shared.setMetadataCopyMoveAsync(ocId: metadata.ocId, destination: destination, overwrite: overwrite.description, status: self.global.metadataStatusWaitMove) - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: self.global.metadataStatusWaitMove) + await NCManageDatabase.shared.setMetadataCopyMoveAsync(ocId: ocId, destination: destination, overwrite: overwrite.description, status: self.global.metadataStatusWaitMove) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitMove) } } } @@ -645,9 +651,11 @@ extension NCNetworking { } Task { + let ocId = metadata.ocId + let serverUrl = metadata.serverUrl await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - await NCManageDatabase.shared.setMetadataCopyMoveAsync(ocId: metadata.ocId, destination: destination, overwrite: overwrite.description, status: self.global.metadataStatusWaitCopy) - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: self.global.metadataStatusWaitCopy) + await NCManageDatabase.shared.setMetadataCopyMoveAsync(ocId: ocId, destination: destination, overwrite: overwrite.description, status: self.global.metadataStatusWaitCopy) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitCopy) } } } @@ -705,16 +713,19 @@ extension NCNetworking { } Task { + let ocId = metadata.ocId + let serverUrl = metadata.serverUrl + let favorite = metadata.favorite await self.transferDispatcher.notifyAllDelegatesAsync { delegate in - await NCManageDatabase.shared.setMetadataFavoriteAsync(ocId: metadata.ocId, favorite: !metadata.favorite, saveOldFavorite: metadata.favorite.description, status: self.global.metadataStatusWaitFavorite) - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: self.global.metadataStatusWaitFavorite) + await NCManageDatabase.shared.setMetadataFavoriteAsync(ocId: ocId, favorite: !favorite, saveOldFavorite: favorite.description, status: self.global.metadataStatusWaitFavorite) + delegate.transferReloadDataSource(serverUrl: serverUrl, requestData: false, status: self.global.metadataStatusWaitFavorite) } } } func setFavorite(metadata: tableMetadata) async -> NKError { let session = NCSession.Session(account: metadata.account, urlBase: metadata.urlBase, user: metadata.user, userId: metadata.userId) - let fileName = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + let fileName = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) let results = await NextcloudKit.shared.setFavoriteAsync(fileName: fileName, favorite: metadata.favorite, account: metadata.account) { task in Task { @@ -775,7 +786,7 @@ extension NCNetworking { Task { await self.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: metadata.serverUrl, requestData: false, status: nil) } } } @@ -1091,7 +1102,6 @@ class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) } } completion: { _, image, _, etag, _, error in - if error == .success, let image { NCManageDatabase.shared.addAvatar(fileName: self.fileName, etag: etag ?? "") #if !EXTENSION @@ -1101,12 +1111,11 @@ class NCOperationDownloadAvatar: ConcurrentOperation, @unchecked Sendable { DispatchQueue.main.async { let visibleCells: [UIView] = (self.view as? UICollectionView)?.visibleCells ?? (self.view as? UITableView)?.visibleCells ?? [] for case let cell as NCCellProtocol in visibleCells { - if self.user == cell.fileUser { - - if self.isPreviewImageView, let filePreviewImageView = cell.filePreviewImageView { - UIView.transition(with: filePreviewImageView, duration: 0.75, options: .transitionCrossDissolve, animations: { filePreviewImageView.image = image}, completion: nil) - } else if let fileAvatarImageView = cell.fileAvatarImageView { - UIView.transition(with: fileAvatarImageView, duration: 0.75, options: .transitionCrossDissolve, animations: { fileAvatarImageView.image = image}, completion: nil) + if self.user == cell.metadata?.ownerId { + if self.isPreviewImageView, let previewImageView = cell.previewImageView { + UIView.transition(with: previewImageView, duration: 0.75, options: .transitionCrossDissolve, animations: { previewImageView.image = image}, completion: nil) + } else if let avatarImageView = cell.avatarImageView { + UIView.transition(with: avatarImageView, duration: 0.75, options: .transitionCrossDissolve, animations: { avatarImageView.image = image}, completion: nil) } break } diff --git a/iOSClient/Networking/NCNetworking.swift b/iOSClient/Networking/NCNetworking.swift index 3f6cffe649..226efc5e2c 100644 --- a/iOSClient/Networking/NCNetworking.swift +++ b/iOSClient/Networking/NCNetworking.swift @@ -12,7 +12,7 @@ import UIKit import NextcloudKit import Alamofire -@objc protocol ClientCertificateDelegate { +protocol ClientCertificateDelegate: AnyObject { func onIncorrectPassword() func didAskForClientCertificate() } @@ -28,7 +28,8 @@ protocol NCTransferDelegate: AnyObject { ocId: String, destination: String?, error: NKError) - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) + func transferReloadData(serverUrl: String?) func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, @@ -36,203 +37,6 @@ protocol NCTransferDelegate: AnyObject { serverUrl: String) } -extension NCTransferDelegate { - func transferChange(status: String, - account: String, - fileName: String, - serverUrl: String, - selector: String?, - ocId: String, - destination: String?, - error: NKError) {} - func transferReloadData(serverUrl: String?, requestData: Bool, status: Int?) {} - func transferProgressDidUpdate(progress: Float, - totalBytes: Int64, - totalBytesExpected: Int64, - fileName: String, - serverUrl: String) {} -} - -/// Actor-based delegate dispatcher using weak references. -actor NCTransferDelegateDispatcher { - // Weak reference collection of delegates - private var transferDelegates = NSHashTable.weakObjects() - - /// Adds a delegate safely. - func addDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.add(delegate) - } - - /// Remove a delegate safely. - func removeDelegate(_ delegate: NCTransferDelegate) { - transferDelegates.remove(delegate) - } - - /// Notifies all delegates. - func notifyAllDelegates(_ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - block(delegate) - } - } - - func notifyAllDelegatesAsync(_ block: @escaping (NCTransferDelegate) async -> Void) async { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - await block(delegate) - } - } - - /// Notifies the delegate for a specific scene. - func notifyDelegate(forScene sceneIdentifier: String, _ block: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - block(delegate) - } - } - } - - /// Notifies matching and non-matching delegates for a specific scene. - func notifyDelegates(forScene sceneIdentifier: String, - matching: (NCTransferDelegate) -> Void, - others: (NCTransferDelegate) -> Void) { - let delegatesCopy = transferDelegates.allObjects.compactMap { $0 as? NCTransferDelegate } - for delegate in delegatesCopy { - if delegate.sceneIdentifier == sceneIdentifier { - matching(delegate) - } else { - others(delegate) - } - } - } -} - -/// A thread-safe registry for tracking in-flight `URLSessionTask` instances. -/// -/// Each task is associated with a string identifier (`identifier`) that you define, -/// allowing you to check whether a request is already running, avoid duplicates, -/// and cancel all active tasks at once. The registry automatically removes -/// completed tasks via `cleanupCompleted()` to keep memory usage compact. -/// -/// Typical use cases: -/// - Ensure only one task per identifier is active at a time. -/// - Query whether a specific request is still running (`isReading`). -/// - Forcefully stop a specific request (`cancel`). -/// - Forcefully stop all tasks when leaving a screen (`cancelAll`). -actor NetworkingTasks { - private var active: [(identifier: String, task: URLSessionTask)] = [] - - /// Returns whether there is an in-flight task for the given URL. - /// - /// A task is considered in-flight if its `state` is `.running` or `.suspended`. - /// - Parameter identifier: The identifier to check. - /// - Returns: `true` if a matching in-flight task exists; otherwise `false`. - func isReading(identifier: String) -> Bool { - // Drop finished/canceling tasks globally - cleanup() - - return active.contains { - $0.identifier == identifier && ($0.task.state == .running || $0.task.state == .suspended) - } - } - - /// Tracks a newly created `URLSessionTask` for the given identifier. - /// - /// If a running entry for the same identifier exists, it is removed before appending the new one. - /// - Parameters: - /// - identifier: The identifier associated with the task. - /// - task: The `URLSessionTask` to track. - func track(identifier: String, task: URLSessionTask) { - // Drop finished/canceling tasks globally - cleanup() - - active.removeAll { - $0.identifier == identifier && $0.task.state == .running - } - active.append((identifier, task)) - nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .start, message: "Start task for identifier: \(identifier)", consoleOnly: true) - } - - /// create a Identifier - /// - func createIdentifier(account: String? = nil, path: String? = nil, name: String) -> String { - if let account, - let path { - return account + "_" + path + "_" + name - } else if let path { - return path + "_" + name - } else { - return name - } - } - - /// Cancels and removes all tasks associated with the given id. - /// - /// - Parameter identifier: The identifier whose tasks should be canceled. - func cancel(identifier: String) { - // Drop finished/canceling tasks globally - cleanup() - - for element in active where element.identifier == identifier { - element.task.cancel() - nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .cancel, message: "Cancel task for identifier: \(identifier)", consoleOnly: true) - } - active.removeAll { - $0.identifier == identifier - } - } - - /// Cancels all tracked `URLSessionTask` and clears the registry. - /// - /// Call this when leaving the page/screen or when the operation must be forcefully stopped. - func cancelAll() { - active.forEach { - $0.task.cancel() - nkLog(tag: NCGlobal.shared.logTagNetworkingTasks, emoji: .cancel, message: "Cancel task with identifier: \($0.identifier)", consoleOnly: true) - } - active.removeAll() - } - - /// Removes tasks that have completed from the registry. - /// - /// Useful to keep the in-memory list compact during long-running operations. - func cleanup() { - active.removeAll { - $0.task.state == .completed || $0.task.state == .canceling - } - } -} - -/// Quantizes per-task progress updates to integer percentages (0...100). -/// Each (serverUrlFileName) pair is tracked separately, so you get -/// at most one update per integer percent for each transfer. -actor ProgressQuantizer { - private var lastPercent: [String: Int] = [:] - - /// Returns `true` only when integer percent changes (or hits 100). - /// - /// - Parameters: - /// - serverUrlFileName: The name of the file being transferred. - /// - fraction: Progress fraction [0.0 ... 1.0]. - func shouldEmit(serverUrlFileName: String, fraction: Double) -> Bool { - let percent = min(max(Int((fraction * 100).rounded(.down)), 0), 100) - - let last = lastPercent[serverUrlFileName] ?? -1 - guard percent != last || percent == 100 else { - return false - } - - lastPercent[serverUrlFileName] = percent - return true - } - - /// Clears stored state for a finished transfer. - func clear(serverUrlFileName: String) { - lastPercent.removeValue(forKey: serverUrlFileName) - } -} - class NCNetworking: @unchecked Sendable, NextcloudKitDelegate { static let shared = NCNetworking() diff --git a/iOSClient/Notification/NCNotification.swift b/iOSClient/Notification/NCNotification.swift index 00a2662c9e..26b1de2981 100644 --- a/iOSClient/Notification/NCNotification.swift +++ b/iOSClient/Notification/NCNotification.swift @@ -142,9 +142,9 @@ class NCNotification: UITableViewController, NCNotificationCellDelegate { let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) if results.image == nil { - cell.fileAvatarImageView?.image = utility.loadUserImage(for: user, displayName: json["user"]?["name"].string, urlBase: session.urlBase) + cell.avatarImageView?.image = utility.loadUserImage(for: user, displayName: json["user"]?["name"].string, urlBase: session.urlBase) } else { - cell.fileAvatarImageView?.image = results.image + cell.avatarImageView?.image = results.image } if !(results.tblAvatar?.loaded ?? false), @@ -371,7 +371,7 @@ class NCNotificationCell: UITableViewCell, NCCellProtocol { get { return index } set { index = newValue } } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return avatar } var fileUser: String? { diff --git a/iOSClient/RichWorkspace/NCRichWorkspaceCommon.swift b/iOSClient/RichWorkspace/NCRichWorkspaceCommon.swift index b67df6108f..c255e743db 100644 --- a/iOSClient/RichWorkspace/NCRichWorkspaceCommon.swift +++ b/iOSClient/RichWorkspace/NCRichWorkspaceCommon.swift @@ -22,7 +22,7 @@ class NCRichWorkspaceCommon: NSObject { NCActivityIndicator.shared.start(backgroundView: viewController.view) - let fileNamePath = utilityFileSystem.getFileNamePath(NCGlobal.shared.fileNameRichWorkspace, serverUrl: serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(NCGlobal.shared.fileNameRichWorkspace, serverUrl: serverUrl, session: session) NextcloudKit.shared.textCreateFile(fileNamePath: fileNamePath, editorId: textCreators.editor, creatorId: textCreators.identifier, templateId: "", account: session.account) { task in Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: session.account, @@ -58,7 +58,7 @@ class NCRichWorkspaceCommon: NSObject { if metadata.url.isEmpty { NCActivityIndicator.shared.start(backgroundView: viewController.view) - let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) NextcloudKit.shared.textOpenFile(fileNamePath: fileNamePath, editor: "text", account: metadata.account) { task in Task { let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index cd3903f02a..04cbd0ab8d 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -359,7 +359,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } let serverUrl = controller.currentServerUrl() let fileName = await NCNetworking.shared.createFileName(fileNameBase: NSLocalizedString("_untitled_", comment: "") + "." + creator.ext, account: session.account, serverUrl: serverUrl) - let fileNamePath = NCUtilityFileSystem().getFileNamePath(String(describing: fileName), serverUrl: serverUrl, session: session) + let fileNamePath = NCUtilityFileSystem().getRelativeFilePath(String(describing: fileName), serverUrl: serverUrl, session: session) await NCCreate().createDocument(controller: controller, fileNamePath: fileNamePath, fileName: String(describing: fileName), editorId: "text", creatorId: creator.identifier, templateId: "document", account: session.account) case self.global.actionVoiceMemo: diff --git a/iOSClient/Select/NCSelect.swift b/iOSClient/Select/NCSelect.swift index 572fa3bd73..15cd58d26b 100644 --- a/iOSClient/Select/NCSelect.swift +++ b/iOSClient/Select/NCSelect.swift @@ -29,7 +29,7 @@ protocol NCSelectDelegate: AnyObject { func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) } -class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresentationControllerDelegate, NCListCellDelegate, NCSectionFirstHeaderDelegate, NCTransferDelegate { +class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresentationControllerDelegate, NCSectionFirstHeaderDelegate, NCTransferDelegate { @IBOutlet private var collectionView: UICollectionView! @IBOutlet private var buttonCancel: UIBarButtonItem! @IBOutlet private var bottomContraint: NSLayoutConstraint? @@ -145,7 +145,7 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent super.viewWillAppear(animated) Task { @MainActor in - let folderPath = utilityFileSystem.getFileNamePath("", serverUrl: serverUrl, session: session) + let folderPath = utilityFileSystem.getRelativeFilePath("", serverUrl: serverUrl, session: session) let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) if serverUrl.isEmpty || !FileNameValidator.checkFolderPath(folderPath, account: session.account, capabilities: capabilities) { @@ -202,7 +202,13 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent // Dismission } - // MARK: - NotificationCenter + // MARK: - + + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } func transferChange(status: String, account: String, @@ -262,16 +268,7 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent overwrite = sender.isOn } - func tapShareListItem(with ocId: String, ocIdTransfer: String, sender: Any) { } - - func tapMoreListItem(with ocId: String, ocIdTransfer: String, image: UIImage?, sender: Any) { } - - func longPressListItem(with odId: String, ocIdTransfer: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func tapRichWorkspace(_ sender: Any) { } - - func tapRecommendationsButtonMenu(with metadata: tableMetadata, image: UIImage?, sender: Any?) { } - func tapRecommendations(with metadata: tableMetadata) { } // MARK: - Push metadata @@ -337,12 +334,12 @@ extension NCSelect: UICollectionViewDataSource { // Thumbnail if !metadata.directory { if let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt512, userId: metadata.userId, urlBase: metadata.urlBase) { - (cell as? NCCellProtocol)?.filePreviewImageView?.image = image + (cell as? NCCellProtocol)?.previewImageView?.image = image } else { if metadata.iconName.isEmpty { - (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.shared.getImageFile() + (cell as? NCCellProtocol)?.previewImageView?.image = NCImageCache.shared.getImageFile() } else { - (cell as? NCCellProtocol)?.filePreviewImageView?.image = self.utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + (cell as? NCCellProtocol)?.previewImageView?.image = self.utility.loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } if metadata.hasPreview, metadata.status == NCGlobal.shared.metadataStatusNormal { @@ -376,11 +373,9 @@ extension NCSelect: UICollectionViewDataSource { isShare = metadata.permissions.contains(NCMetadataPermissions.permissionShared) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionShared) isMounted = metadata.permissions.contains(NCMetadataPermissions.permissionMounted) && !metadataFolder.permissions.contains(NCMetadataPermissions.permissionMounted) - cell.listCellDelegate = self +// cell.listCellDelegate = self - cell.fileOcId = metadata.ocId - cell.fileOcIdTransfer = metadata.ocIdTransfer - cell.fileUser = metadata.ownerId + cell.metadata = metadata cell.labelTitle.text = metadata.fileNameView cell.labelTitle.textColor = NCBrandColor.shared.textColor diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index dbbcd83492..63a614357a 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -290,4 +290,3 @@ struct E2EESection: View { #Preview { NCSettingsView(model: NCSettingsModel(controller: nil)) } - diff --git a/iOSClient/Share/NCShareCommentsCell.swift b/iOSClient/Share/NCShareCommentsCell.swift index 59cdf78c81..9c12503cea 100644 --- a/iOSClient/Share/NCShareCommentsCell.swift +++ b/iOSClient/Share/NCShareCommentsCell.swift @@ -43,7 +43,7 @@ class NCShareCommentsCell: UITableViewCell, NCCellProtocol { get { return index } set { index = newValue } } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return imageItem } var fileUser: String? { diff --git a/iOSClient/Share/NCShareNetworking.swift b/iOSClient/Share/NCShareNetworking.swift index b696bac0e8..71f459fc97 100644 --- a/iOSClient/Share/NCShareNetworking.swift +++ b/iOSClient/Share/NCShareNetworking.swift @@ -66,7 +66,7 @@ class NCShareNetworking: NSObject { if showLoadingIndicator { NCActivityIndicator.shared.start(backgroundView: view) } - let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + let filenamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) let parameter = NKShareParameter(path: filenamePath) NextcloudKit.shared.readShares(parameters: parameter, account: metadata.account) { task in @@ -116,7 +116,7 @@ class NCShareNetworking: NSObject { func createShare(_ shareable: Shareable, downloadLimit: DownloadLimitViewModel) { NCActivityIndicator.shared.start(backgroundView: view) - let filenamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + let filenamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) let capabilities = NCNetworking.shared.capabilities[self.metadata.account] ?? NKCapabilities.Capabilities() NextcloudKit.shared.createShare(path: filenamePath, @@ -157,7 +157,7 @@ class NCShareNetworking: NSObject { Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) } } } else { @@ -186,7 +186,7 @@ class NCShareNetworking: NSObject { Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) } } } else { @@ -227,7 +227,7 @@ class NCShareNetworking: NSObject { Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) } } } else { diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index fe044d7dad..c5177afdb8 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -126,7 +126,7 @@ class NCSharePaging: UIViewController { super.viewWillDisappear(animated) Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: false, status: nil) } } } diff --git a/iOSClient/Share/NCShareUserCell.swift b/iOSClient/Share/NCShareUserCell.swift index c749a99b5f..e6b60c8a93 100644 --- a/iOSClient/Share/NCShareUserCell.swift +++ b/iOSClient/Share/NCShareUserCell.swift @@ -46,7 +46,7 @@ class NCShareUserCell: UITableViewCell, NCCellProtocol { get { return index } set { index = newValue } } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return imageItem } var fileUser: String? { @@ -180,7 +180,7 @@ class NCSearchUserDropDownCell: DropDownCell, NCCellProtocol { get { return index } set { index = newValue } } - var fileAvatarImageView: UIImageView? { + var avatarImageView: UIImageView? { return imageItem } var fileUser: String? { diff --git a/iOSClient/StatusMessage/NCStatusMessageView.swift b/iOSClient/StatusMessage/NCStatusMessageView.swift index 1c6c10c920..01b4060193 100644 --- a/iOSClient/StatusMessage/NCStatusMessageView.swift +++ b/iOSClient/StatusMessage/NCStatusMessageView.swift @@ -31,7 +31,7 @@ struct NCStatusMessageView: View { } } .padding(.top, 8) - + HStack { Text("_clear_status_message_after_") Menu { diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index a0fb214d17..fa5310d83b 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -328,8 +328,8 @@ "_date_" = "Date"; "_size_" = "Size"; "_delete_selected_files_" = "Delete files"; -"_remove_favorites_" = "Remove from favorites"; -"_add_favorites_" = "Add to favorites"; +"_remove_favorites_" = "Unfavorite"; +"_add_favorites_" = "Favorite"; "_share_" = "Share"; "_remove_local_file_" = "Remove locally"; "_folders_" = "folders"; diff --git a/iOSClient/Terms of service/NCTermOfServiceModel.swift b/iOSClient/Terms of service/NCTermOfServiceModel.swift index 25b3bb9d32..0f1c1e82cd 100644 --- a/iOSClient/Terms of service/NCTermOfServiceModel.swift +++ b/iOSClient/Terms of service/NCTermOfServiceModel.swift @@ -55,7 +55,7 @@ class NCTermOfServiceModel: ObservableObject { if let error { if error == .success { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: nil, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: nil, requestData: true, status: nil) } } else { NCContentPresenter().showError(error: error) diff --git a/iOSClient/Transfers/NCTransfersModel.swift b/iOSClient/Transfers/NCTransfersModel.swift index a4fd4edbc2..2e1556a59d 100644 --- a/iOSClient/Transfers/NCTransfersModel.swift +++ b/iOSClient/Transfers/NCTransfersModel.swift @@ -173,6 +173,12 @@ final class TransfersViewModel: ObservableObject, NCMetadataTransfersSuccessDele } extension TransfersViewModel: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferChange(status: String, account: String, fileName: String, serverUrl: String, selector: String?, ocId: String, destination: String?, error: NKError) { } + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { Task { @MainActor in let key = "\(serverUrl)|\(fileName)" diff --git a/iOSClient/Trash/NCTrash+Networking.swift b/iOSClient/Trash/NCTrash+Networking.swift index 30e27c75bc..39aedb4fab 100644 --- a/iOSClient/Trash/NCTrash+Networking.swift +++ b/iOSClient/Trash/NCTrash+Networking.swift @@ -38,8 +38,8 @@ extension NCTrash { let resultsListingTrash = await NextcloudKit.shared.listingTrashAsync(filename: filename, showHiddenFiles: false, account: session.account) { task in Task { await NCNetworking.shared.networkingTasks.track(identifier: "NCTrash", task: task) + await self.collectionView.reloadData() } - self.collectionView.reloadData() } if let items = resultsListingTrash.items { diff --git a/iOSClient/Trash/NCTrash.swift b/iOSClient/Trash/NCTrash.swift index 797fba1e7c..4a79337bfc 100644 --- a/iOSClient/Trash/NCTrash.swift +++ b/iOSClient/Trash/NCTrash.swift @@ -158,8 +158,6 @@ class NCTrash: UIViewController, NCTrashListCellDelegate, NCTrashGridCellDelegat } } - func longPressGridItem(with objectId: String, gestureRecognizer: UILongPressGestureRecognizer) { } - func longPressMoreGridItem(with objectId: String, gestureRecognizer: UILongPressGestureRecognizer) { } // MARK: - DataSource diff --git a/iOSClient/Utility/NCDebouncer.swift b/iOSClient/Utility/NCDebouncer.swift index 6055569760..a6307f28b7 100644 --- a/iOSClient/Utility/NCDebouncer.swift +++ b/iOSClient/Utility/NCDebouncer.swift @@ -45,15 +45,23 @@ public actor NCDebouncer { } public func pause() { - guard !isPaused else { return } + guard !isPaused else { + return + } isPaused = true pendingTask?.cancel() pendingTask = nil } + public func isPausedNow() -> Bool { + return isPaused + } + public func resume() { - guard isPaused else { return } + guard isPaused else { + return + } isPaused = false @@ -73,7 +81,9 @@ public actor NCDebouncer { // MARK: - Internal private func scheduleIfNeeded() { - guard pendingTask == nil, !isPaused else { return } + guard pendingTask == nil, !isPaused else { + return + } pendingTask = Task { [weak self] in guard let self else { return } @@ -83,13 +93,17 @@ public actor NCDebouncer { } private func commit() { - guard !isPaused else { return } + guard !isPaused else { + return + } pendingTask?.cancel() pendingTask = nil eventCount = 0 - guard let block = latestBlock else { return } + guard let block = latestBlock else { + return + } latestBlock = nil Task { @MainActor in diff --git a/iOSClient/Utility/NCUtilityFileSystem.swift b/iOSClient/Utility/NCUtilityFileSystem.swift index d1bbae9a9c..530ca86af4 100644 --- a/iOSClient/Utility/NCUtilityFileSystem.swift +++ b/iOSClient/Utility/NCUtilityFileSystem.swift @@ -148,7 +148,22 @@ final class NCUtilityFileSystem: NSObject, @unchecked Sendable { } } - func getFileNamePath(_ fileName: String, serverUrl: String, session: NCSession.Session) -> String { + /// Constructs the relative path of a file by removing the home server URL prefix. + /// + /// - Parameters: + /// - fileName: The name of the file + /// - serverUrl: The full server URL where the file is located. + /// - session: The user NCSession. + /// - Returns: The relative path from the user's home directory (e.g., `"someFolder/Image.png"`). + /// + /// Example: + /// ```swift + /// // Input: fileName = "Image.png" + /// // serverUrl = "https://instance.com/remote.php/dav/files/user1/someFolder" + /// // Output: "someFolder/Image.png" + /// let path = getRelativeFilePath("Image.png", serverUrl: serverUrl, session: session) + /// ``` + func getRelativeFilePath(_ fileName: String, serverUrl: String, session: NCSession.Session) -> String { let home = getHomeServer(session: session) var fileNamePath = serverUrl.replacingOccurrences(of: home, with: "") + "/" + fileName if fileNamePath.first == "/" { @@ -157,7 +172,23 @@ final class NCUtilityFileSystem: NSObject, @unchecked Sendable { return fileNamePath } - func getFileNamePath(_ fileName: String, serverUrl: String, urlBase: String, userId: String) -> String { + /// Constructs the relative path of a file by removing the home server URL prefix. + /// + /// - Parameters: + /// - fileName: The name of the file + /// - serverUrl: The full server URL where the file is located. + /// - urlBase: The base URL of the server instance. + /// - userId: The user identifier. + /// - Returns: The relative path from the user's home directory (e.g., `"someFolder/Image.png"`). + /// + /// Example: + /// ```swift + /// // Input: fileName = "Image.png" + /// // serverUrl = "https://instance.com/remote.php/dav/files/user1/someFolder" + /// // Output: "someFolder/Image.png" + /// let path = getRelativeFilePath("Image.png", serverUrl: serverUrl, urlBase: urlBase, userId: userId) + /// ``` + func getRelativeFilePath(_ fileName: String, serverUrl: String, urlBase: String, userId: String) -> String { let home = getHomeServer(urlBase: urlBase, userId: userId) var fileNamePath = serverUrl.replacingOccurrences(of: home, with: "") + "/" + fileName if fileNamePath.first == "/" { diff --git a/iOSClient/Viewer/NCViewer.swift b/iOSClient/Viewer/NCViewer.swift index c0bdf8b4b0..51ad31a170 100644 --- a/iOSClient/Viewer/NCViewer.swift +++ b/iOSClient/Viewer/NCViewer.swift @@ -124,7 +124,7 @@ class NCViewer: NSObject { options = NKRequestOptions(customUserAgent: utility.getCustomUserAgentOnlyOffice()) } if metadata.url.isEmpty { - let fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + let fileNamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) NCActivityIndicator.shared.start(backgroundView: delegate?.view) let results = await NextcloudKit.shared.textOpenFileAsync(fileNamePath: fileNamePath, editor: editor, account: metadata.account, options: options) { task in diff --git a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift index ccb930df56..2d0096e767 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCPlayer/NCPlayerToolBar.swift @@ -492,9 +492,7 @@ extension NCPlayerToolBar: NCSelectDelegate { if error == .success { self.addPlaybackSlave(type: type, metadata: metadata) } else if error.errorCode != 200 { - await showErrorBanner(scene: scene, - errorDescription: error.errorDescription, - errorCode: error.errorCode) + await showErrorBanner(scene: scene, errorDescription: error.errorDescription) } } } diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift index 0b2d267e41..af33beaee0 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMedia.swift @@ -614,6 +614,12 @@ extension NCViewerMedia: EasyTipViewDelegate { } extension NCViewerMedia: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift index 48bc45de63..9dceb20fc1 100644 --- a/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift +++ b/iOSClient/Viewer/NCViewerMedia/NCViewerMediaPage.swift @@ -569,6 +569,10 @@ extension NCViewerMediaPage: UIScrollViewDelegate { } extension NCViewerMediaPage: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift b/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift index 09e22c0e33..d9326694bc 100644 --- a/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift +++ b/iOSClient/Viewer/NCViewerNextcloudText/NCViewerNextcloudText.swift @@ -219,7 +219,7 @@ extension NCViewerNextcloudText: UINavigationControllerDelegate { Task { if parent == nil { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: true, status: nil) } } } @@ -227,6 +227,12 @@ extension NCViewerNextcloudText: UINavigationControllerDelegate { } extension NCViewerNextcloudText: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/Viewer/NCViewerPDF/NCViewerPDF.swift b/iOSClient/Viewer/NCViewerPDF/NCViewerPDF.swift index f602e3dc07..01e43d1df1 100644 --- a/iOSClient/Viewer/NCViewerPDF/NCViewerPDF.swift +++ b/iOSClient/Viewer/NCViewerPDF/NCViewerPDF.swift @@ -514,6 +514,12 @@ extension NCViewerPDF: EasyTipViewDelegate { } extension NCViewerPDF: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/Viewer/NCViewerProviderContextMenu.swift b/iOSClient/Viewer/NCViewerProviderContextMenu.swift index 29e4db791a..db5a3c7841 100644 --- a/iOSClient/Viewer/NCViewerProviderContextMenu.swift +++ b/iOSClient/Viewer/NCViewerProviderContextMenu.swift @@ -269,6 +269,12 @@ extension NCViewerProviderContextMenu: VLCMediaPlayerDelegate { } extension NCViewerProviderContextMenu: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift b/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift index 594509e293..40215c7214 100644 --- a/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift +++ b/iOSClient/Viewer/NCViewerRichdocument/NCViewerRichDocument.swift @@ -297,7 +297,7 @@ class NCViewerRichDocument: UIViewController, WKNavigationDelegate, WKScriptMess func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) { if let serverUrl, let metadata { - let path = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: serverUrl, session: session) + let path = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: serverUrl, session: session) NextcloudKit.shared.createAssetRichdocuments(path: path, account: metadata.account) { task in Task { @@ -318,7 +318,7 @@ class NCViewerRichDocument: UIViewController, WKNavigationDelegate, WKScriptMess } func select(_ metadata: tableMetadata!, serverUrl: String!) { - let path = utilityFileSystem.getFileNamePath(metadata!.fileName, serverUrl: serverUrl!, session: session) + let path = utilityFileSystem.getRelativeFilePath(metadata!.fileName, serverUrl: serverUrl!, session: session) NextcloudKit.shared.createAssetRichdocuments(path: path, account: metadata.account) { task in Task { @@ -361,7 +361,7 @@ class NCViewerRichDocument: UIViewController, WKNavigationDelegate, WKScriptMess NCActivityIndicator.shared.stop() } - // MARK: - Hekper + // MARK: - Helper func filenameFromContentDisposition(_ disposition: String) -> String? { if let range = disposition.range(of: "filename=") { @@ -385,7 +385,7 @@ extension NCViewerRichDocument: UINavigationControllerDelegate { Task { if parent == nil { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in - delegate.transferReloadData(serverUrl: metadata.serverUrl, requestData: false, status: nil) + delegate.transferReloadDataSource(serverUrl: self.metadata.serverUrl, requestData: false, status: nil) } } } @@ -393,6 +393,12 @@ extension NCViewerRichDocument: UINavigationControllerDelegate { } extension NCViewerRichDocument: NCTransferDelegate { + func transferReloadData(serverUrl: String?) { } + + func transferReloadDataSource(serverUrl: String?, requestData: Bool, status: Int?) { } + + func transferProgressDidUpdate(progress: Float, totalBytes: Int64, totalBytesExpected: Int64, fileName: String, serverUrl: String) { } + func transferChange(status: String, account: String, fileName: String, diff --git a/iOSClient/main.swift b/iOSClient/main.swift new file mode 100644 index 0000000000..bf621bd0f4 --- /dev/null +++ b/iOSClient/main.swift @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit + +/// Entry point of the application. +/// +/// This call bootstraps the UIKit application using a custom `UIApplication` +/// subclass (`NCApplication`) in order to intercept and observe low-level +/// user interaction events globally. +UIApplicationMain( + CommandLine.argc, + CommandLine.unsafeArgv, + NSStringFromClass(NCApplication.self), + NSStringFromClass(AppDelegate.self) +) + +/// Custom `UIApplication` subclass used to intercept all UIEvents +/// before they are dispatched to windows, scenes, and the view hierarchy. +/// +/// This is the lowest and most reliable interception point in UIKit for +/// observing user interactions such as touches, presses, and motion events. +/// The implementation forwards events to `UserInteractionMonitor` while +/// preserving default UIKit behavior +final class NCApplication: UIApplication { + /// Intercepts and forwards every UIEvent dispatched by the application. + /// + /// - Parameter event: The UIEvent generated by the system. + /// + /// This method must always call `super.sendEvent(_:)` to ensure + /// normal event delivery. It is intentionally lightweight and delegates + /// all processing to `UserInteractionMonitor` + override func sendEvent(_ event: UIEvent) { + super.sendEvent(event) + UserInteractionMonitor.shared.handle(event: event) + } +} + +/// Centralized monitor for observing user interaction activity. +/// +/// This class listens for low-level UIEvents and emits a notification +/// when a complete user interaction cycle has finished (i.e. all touches +/// have ended or been cancelled). +/// +/// Typical use cases include: +/// - inactivity or idle detection +/// - global debouncing or refresh triggers +/// - analytics or telemetry +/// - security auto-lock timers +/// +/// The monitor is intentionally decoupled from UI components and communicates +/// exclusively via NotificationCenter +final class UserInteractionMonitor { + static let shared = UserInteractionMonitor() + + private init() {} + + /// Handles a UIEvent forwarded by the application. + /// + /// - Parameter event: The UIEvent received from `UIApplication.sendEvent(_:)`. + /// + /// Only touch events are considered. When all touches associated with the + /// event are either `.ended` or `.cancelled`, a notification is posted + /// indicating that a user interaction cycle has completed. + /// + /// The notification name is defined by `NCGlobal.shared.notificationCenterUserInteractionMonitor` + func handle(event: UIEvent) { + guard event.type == .touches else { return } + guard let touches = event.allTouches, !touches.isEmpty else { return } + + let allEnded = touches.allSatisfy { + $0.phase == .ended || $0.phase == .cancelled + } + + if allEnded { + NotificationCenter.default.post(name: Notification.Name(rawValue: NCGlobal.shared.notificationCenterUserInteractionMonitor), object: nil, userInfo: nil) + } + } +}