From f6886b2c91115fe25b93188a0719813be646b14b Mon Sep 17 00:00:00 2001 From: Iva Horn Date: Fri, 22 Nov 2024 10:48:42 +0100 Subject: [PATCH] #3184: Download Limit Support - Added tableDownloadLimit entity to app database. - Extended capability query to also consider download limit app. - Extended capabilities list view for display of download limit availability. - Extended share detail user interface to mimic the web user interface for managing download limits. - Every time WebDAV properties of a file are retrieved, its associated download limits are removed and recreated. Housekeeping: - Outsourced NCShareDateCell into dedicated source code file. - Outsourced NCShareToggleCell into dedicated source code file. Notes: - In my first attempt I had a detail view in the download limit row of the advanced share options showing the remaining number of downloads. However, that required to inject and retain the download limit entity object into the complicated share table configuration object. That, in turn, results in inconsistent data state due to invalid and outdated references. To resolve those issues, the assembly of the advanced share options user interface needs some refactoring which appears to expansive at this point and I prefer to leave it as it is for now. Signed-off-by: Iva Horn --- Brand/Database.swift | 2 +- Nextcloud.xcodeproj/project.pbxproj | 46 +++++ .../Data/NCManageDatabase+Capabilities.swift | 9 + .../Data/NCManageDatabase+DownloadLimit.swift | 95 +++++++++++ iOSClient/Data/NCManageDatabase+Share.swift | 46 ++++- iOSClient/Data/NCManageDatabase.swift | 3 +- iOSClient/Menu/NCShare+Menu.swift | 1 + iOSClient/NCCapabilities.swift | 2 + .../Networking/NCNetworking+WebDAV.swift | 18 ++ .../Capabilities/NCCapabilitiesModel.swift | 8 + ...hareDownloadLimitTableViewController.swift | 160 ++++++++++++++++++ .../NCShareDownloadLimitViewController.swift | 55 ++++++ .../Advanced/NCShareAdvancePermission.swift | 9 + iOSClient/Share/Advanced/NCShareCells.swift | 133 +++------------ .../Share/Advanced/NCShareDateCell.swift | 97 +++++++++++ .../Share/Advanced/NCShareToggleCell.swift | 26 +++ iOSClient/Share/NCShare.storyboard | 123 +++++++++++++- .../NCShareDownloadLimitNetworking.swift | 71 ++++++++ .../en.lproj/Localizable.strings | 3 + 19 files changed, 781 insertions(+), 126 deletions(-) create mode 100644 iOSClient/Data/NCManageDatabase+DownloadLimit.swift create mode 100644 iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift create mode 100644 iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift create mode 100644 iOSClient/Share/Advanced/NCShareDateCell.swift create mode 100644 iOSClient/Share/Advanced/NCShareToggleCell.swift create mode 100644 iOSClient/Share/NCShareDownloadLimitNetworking.swift diff --git a/Brand/Database.swift b/Brand/Database.swift index fe82386fed..59ec61a758 100644 --- a/Brand/Database.swift +++ b/Brand/Database.swift @@ -26,4 +26,4 @@ import Foundation // Database Realm // let databaseName = "nextcloud.realm" -let databaseSchemaVersion: UInt64 = 367 +let databaseSchemaVersion: UInt64 = 368 diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index f2bd2cb58b..8d6250837c 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -16,6 +16,18 @@ 371B5A2E23D0B04500FAFAE9 /* NCMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */; }; 3781B9B023DB2B7E006B4B1D /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3781B9AF23DB2B7E006B4B1D /* AppDelegate+Menu.swift */; }; 8491B1CD273BBA82001C8C5B /* UIViewController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8491B1CC273BBA82001C8C5B /* UIViewController+Menu.swift */; }; + AA3494FE2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3494FF2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495002CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495012CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495022CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495032CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AA3495042CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */; }; + AAAC0A122CEE34700001949E /* NCShareDownloadLimitNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */; }; + AAF806B22CE25E67009C2D43 /* NCShareDateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */; }; + AAF806B42CE25EFF009C2D43 /* NCShareToggleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */; }; + AAF806B62CE34C7A009C2D43 /* NCShareDownloadLimitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */; }; + AAF806B82CE37C1A009C2D43 /* NCShareDownloadLimitTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -1152,6 +1164,13 @@ 371B5A2D23D0B04500FAFAE9 /* NCMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMenu.swift; sourceTree = ""; }; 3781B9AF23DB2B7E006B4B1D /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = ""; }; 8491B1CC273BBA82001C8C5B /* UIViewController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Menu.swift"; sourceTree = ""; }; + AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+DownloadLimit.swift"; sourceTree = ""; }; + AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitNetworking.swift; sourceTree = ""; }; + AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDateCell.swift; sourceTree = ""; }; + AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareToggleCell.swift; sourceTree = ""; }; + AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitViewController.swift; sourceTree = ""; }; + AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareDownloadLimitTableViewController.swift; sourceTree = ""; }; + AAF806B92CE38BB2009C2D43 /* NextcloudKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NextcloudKit; path = ../NextcloudKit; sourceTree = SOURCE_ROOT; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -1921,6 +1940,15 @@ path = Menu; sourceTree = ""; }; + AA3494FC2CE4FF02005CC075 /* DownloadLimit */ = { + isa = PBXGroup; + children = ( + AAF806B52CE34C72009C2D43 /* NCShareDownloadLimitViewController.swift */, + AAF806B72CE37C15009C2D43 /* NCShareDownloadLimitTableViewController.swift */, + ); + path = DownloadLimit; + sourceTree = ""; + }; AF8ED1FA2757821000B8DBC4 /* NextcloudUnitTests */ = { isa = PBXGroup; children = ( @@ -1932,11 +1960,14 @@ AF93471327E235EB002537EE /* Advanced */ = { isa = PBXGroup; children = ( + AA3494FC2CE4FF02005CC075 /* DownloadLimit */, AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */, AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */, AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */, AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */, + AAF806B12CE25E60009C2D43 /* NCShareDateCell.swift */, AF93474D27E3F211002537EE /* NCShareNewUserAddComment.swift */, + AAF806B32CE25EFE009C2D43 /* NCShareToggleCell.swift */, ); path = Advanced; sourceTree = ""; @@ -2148,6 +2179,7 @@ F787704E22E7019900F287A9 /* NCShareLinkCell.xib */, AF2D7C7B2742556F00ADF566 /* NCShareLinkCell.swift */, F769454722E9F20D000A798A /* NCShareNetworking.swift */, + AAAC0A112CEE346A0001949E /* NCShareDownloadLimitNetworking.swift */, F769453F22E9F077000A798A /* NCSharePaging.swift */, F774264822EB4D0000B23912 /* NCSearchUserDropDownCell.xib */, F769453B22E9CFFF000A798A /* NCShareUserCell.xib */, @@ -2678,6 +2710,7 @@ F7BAAD951ED5A63D00B7EAD4 /* Data */ = { isa = PBXGroup; children = ( + AA3494FD2CE65EA9005CC075 /* NCManageDatabase+DownloadLimit.swift */, F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */, AF4BF613275629E20081CEEF /* NCManageDatabase+Account.swift */, AF4BF61D27562B3F0081CEEF /* NCManageDatabase+Activity.swift */, @@ -3014,6 +3047,7 @@ F7F67B9F1A24D27800EE80DA = { isa = PBXGroup; children = ( + AAF806B92CE38BB2009C2D43 /* NextcloudKit */, F7B8B82F25681C3400967775 /* GoogleService-Info.plist */, F7C1CDD91E6DFC6F005D92BE /* Brand */, F7F67BAA1A24D27800EE80DA /* iOSClient */, @@ -3844,6 +3878,7 @@ F711A4E22AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F7401C1B2C75E6F300649E87 /* NCCapabilities.swift in Sources */, AF4BF61C27562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */, + AA3495022CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F78E2D6B29AF02DB0024D4F3 /* Database.swift in Sources */, F7817CFF29802D1A00FFBC65 /* NCPushNotificationEncryption.m in Sources */, F798F0EC2588060A000DAFFD /* UIColor+Extension.swift in Sources */, @@ -3916,6 +3951,7 @@ F711A4E12AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F76882382C0DD22F001CF441 /* NCKeychain.swift in Sources */, F7C9B9222B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, + AA3495032CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7490E8529882C8C009DCE94 /* NCManageDatabase+Video.swift in Sources */, F7490E7729882C10009DCE94 /* UIColor+Extension.swift in Sources */, F70716E62987F81500E72C1D /* DocumentActionViewController.swift in Sources */, @@ -3964,6 +4000,7 @@ F73EF7DA2B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F7817CFB29801A3500FFBC65 /* Data+Extension.swift in Sources */, F72429362AFE39860040AEF3 /* NCLivePhoto.swift in Sources */, + AA3494FF2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, AF4BF61F27562B3F0081CEEF /* NCManageDatabase+Activity.swift in Sources */, F7CBC1262BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, F7A0D1362591FBC5008F8A13 /* String+Extension.swift in Sources */, @@ -4086,6 +4123,7 @@ F783030328B4C4DD00B84583 /* ThreadSafeDictionary.swift in Sources */, F77ED59128C9CE9D00E24ED0 /* ToolbarData.swift in Sources */, F78302F728B4C3C900B84583 /* NCManageDatabase.swift in Sources */, + AA3495042CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F359D8682A7D03420023F405 /* NCUtility+Exif.swift in Sources */, F7346E1628B0EF5C006CE2D2 /* Widget.swift in Sources */, F78302F828B4C3E100B84583 /* NCManageDatabase+Activity.swift in Sources */, @@ -4218,6 +4256,7 @@ F7327E392B73B8D400A462C7 /* Array+Extension.swift in Sources */, F78E2D6929AF02DB0024D4F3 /* Database.swift in Sources */, F749B64E297B0CBB00087535 /* NCManageDatabase+Share.swift in Sources */, + AA3494FE2CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F73EF7B32B0224350087E6E9 /* NCManageDatabase+DirectEditing.swift in Sources */, F7401C192C75E6F300649E87 /* NCCapabilities.swift in Sources */, F771E3F320E239A600AFB62D /* FileProviderData.swift in Sources */, @@ -4329,6 +4368,7 @@ F758B460212C56A400515F55 /* NCScan.swift in Sources */, F76882262C0DD1E7001CF441 /* NCSettingsView.swift in Sources */, F78ACD52219046DC0088454D /* NCSectionFirstHeader.swift in Sources */, + AAF806B42CE25EFF009C2D43 /* NCShareToggleCell.swift in Sources */, F72944F52A8424F800246839 /* NCEndToEndMetadataV1.swift in Sources */, F710D2022405826100A6033D /* NCViewer+Menu.swift in Sources */, F765E9CD295C585800A09ED8 /* NCUploadScanDocument.swift in Sources */, @@ -4366,6 +4406,7 @@ F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, + AA3495012CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, @@ -4407,6 +4448,7 @@ F7EB9B132BBC12F300EDF036 /* UIApplication+Extension.swift in Sources */, F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */, F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */, + AAF806B62CE34C7A009C2D43 /* NCShareDownloadLimitViewController.swift in Sources */, F76882222C0DD1E7001CF441 /* NCCapabilitiesView.swift in Sources */, AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */, F7F878AE1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, @@ -4494,6 +4536,7 @@ F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, F70968A424212C4E00ED60E5 /* NCLivePhoto.swift in Sources */, + AAF806B22CE25E67009C2D43 /* NCShareDateCell.swift in Sources */, F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */, F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */, F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnail.swift in Sources */, @@ -4515,8 +4558,10 @@ F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, F799DF8B2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift in Sources */, + AAAC0A122CEE34700001949E /* NCShareDownloadLimitNetworking.swift in Sources */, F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */, F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */, + AAF806B82CE37C1A009C2D43 /* NCShareDownloadLimitTableViewController.swift in Sources */, F76882242C0DD1E7001CF441 /* NCSettingsAdvancedView.swift in Sources */, F75CA1472962F13700B01130 /* NCHUDView.swift in Sources */, F77BB748289985270090FC19 /* UITabBarController+Extension.swift in Sources */, @@ -4558,6 +4603,7 @@ F7C9739528F17131002C43E2 /* IntentHandler.swift in Sources */, F7A8D73D28F181D3008BBE1C /* NCUtilityFileSystem.swift in Sources */, F73EF7E12B02266D0087E6E9 /* NCManageDatabase+Trash.swift in Sources */, + AA3495002CE65EB6005CC075 /* NCManageDatabase+DownloadLimit.swift in Sources */, F7C9B91F2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, F75DD767290ABB25002EB562 /* Intent.intentdefinition in Sources */, F72437812C10B92500C7C68D /* NCPermissions.swift in Sources */, diff --git a/iOSClient/Data/NCManageDatabase+Capabilities.swift b/iOSClient/Data/NCManageDatabase+Capabilities.swift index 059847c1fa..d983e61398 100644 --- a/iOSClient/Data/NCManageDatabase+Capabilities.swift +++ b/iOSClient/Data/NCManageDatabase+Capabilities.swift @@ -88,6 +88,7 @@ extension NCManageDatabase { } struct Capabilities: Codable { + let downloadLimit: DownloadLimit? let filessharing: FilesSharing? let theming: Theming? let endtoendencryption: EndToEndEncryption? @@ -102,6 +103,7 @@ extension NCManageDatabase { let assistant: Assistant? enum CodingKeys: String, CodingKey { + case downloadLimit = "downloadlimit" case filessharing = "files_sharing" case theming case endtoendencryption = "end-to-end-encryption" @@ -112,6 +114,11 @@ extension NCManageDatabase { case assistant } + struct DownloadLimit: Codable { + let enabled: Bool? + let defaultLimit: Int? + } + struct FilesSharing: Codable { let apienabled: Bool? let groupsharing: Bool? @@ -327,6 +334,8 @@ extension NCManageDatabase { capabilities.capabilityFileSharingInternalExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.days ?? 0 capabilities.capabilityFileSharingRemoteExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateremote?.enforced ?? false capabilities.capabilityFileSharingRemoteExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateremote?.days ?? 0 + capabilities.capabilityFileSharingDownloadLimit = data.capabilities.downloadLimit?.enabled ?? false + capabilities.capabilityFileSharingDownloadLimitDefaultLimit = data.capabilities.downloadLimit?.defaultLimit ?? 1 capabilities.capabilityThemingColor = data.capabilities.theming?.color ?? "" capabilities.capabilityThemingColorElement = data.capabilities.theming?.colorelement ?? "" diff --git a/iOSClient/Data/NCManageDatabase+DownloadLimit.swift b/iOSClient/Data/NCManageDatabase+DownloadLimit.swift new file mode 100644 index 0000000000..300ea10b12 --- /dev/null +++ b/iOSClient/Data/NCManageDatabase+DownloadLimit.swift @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit +import RealmSwift + +/// +/// Data model for storing information about download limits of shares. +/// +class tableDownloadLimit: Object { + /// + /// The number of downloads which already happened. + /// + @Persisted + @objc dynamic var count: Int = 0 + + /// + /// Total number of allowed downloas. + /// + @Persisted + @objc dynamic var limit: Int = 0 + + /// + /// The token identifying the related share. + /// + @Persisted(primaryKey: true) + @objc dynamic var token: String = "" +} + +extension NCManageDatabase { + /// + /// Create a new download limit object in the database. + /// + @discardableResult + func createDownloadLimit(count: Int, limit: Int, token: String) throws -> tableDownloadLimit? { + let downloadLimit = tableDownloadLimit() + downloadLimit.count = count + downloadLimit.limit = limit + downloadLimit.token = token + + do { + let realm = try Realm() + + try realm.write { + realm.add(downloadLimit, update: .all) + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + + return downloadLimit + } + + /// + /// Delete an existing download limit object identified by the token of its related share. + /// + /// - Parameter token: The `token` of the associated ``Nextcloud/tableShare/token``. + /// + func deleteDownloadLimit(byShareToken token: String) throws { + do { + let realm = try Realm() + + try realm.write { + let result = realm.objects(tableDownloadLimit.self).filter("token == %@", token) + realm.delete(result) + } + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)") + } + } + + /// + /// Retrieve a download limit by the token of the associated ``Nextcloud/tableShare/token``. + /// + /// - Parameter token: The `token` of the associated ``tableShare``. + /// + func getDownloadLimit(byShareToken token: String) throws -> tableDownloadLimit? { + do { + let realm = try Realm() + let predicate = NSPredicate(format: "token == %@", token) + + guard let result = realm.objects(tableDownloadLimit.self).filter(predicate).first else { + return nil + } + + return result + } catch let error as NSError { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") + } + + return nil + } +} diff --git a/iOSClient/Data/NCManageDatabase+Share.swift b/iOSClient/Data/NCManageDatabase+Share.swift index c518597d42..86b2f9a921 100644 --- a/iOSClient/Data/NCManageDatabase+Share.swift +++ b/iOSClient/Data/NCManageDatabase+Share.swift @@ -54,7 +54,12 @@ class tableShareV2: Object { @objc dynamic var primaryKey = "" @objc dynamic var sendPasswordByTalk: Bool = false @objc dynamic var serverUrl = "" + + /// + /// See [OCS Share API documentation](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html) for semantic definitions of the different possible values. + /// @objc dynamic var shareType: Int = 0 + @objc dynamic var shareWith = "" @objc dynamic var shareWithDisplayname = "" @objc dynamic var storage: Int = 0 @@ -146,22 +151,48 @@ extension NCManageDatabase { return [] } + /// + /// Fetch all available shares of an item identified by the given metadata. + /// + /// - Returns: A tuple consisting of the first public share link and any _additional_ shares that might be there. + /// It is possible that there is no public share link but still shares of other types. + /// In the latter case, all shares are returned as the second tuple value. + /// func getTableShares(metadata: tableMetadata) -> (firstShareLink: tableShare?, share: [tableShare]?) { do { let realm = try Realm() realm.refresh() - let sortProperties = [SortDescriptor(keyPath: "shareType", ascending: false), SortDescriptor(keyPath: "idShare", ascending: false)] - let firstShareLink = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName).first + + let sortProperties = [ + SortDescriptor(keyPath: "shareType", ascending: false), + SortDescriptor(keyPath: "idShare", ascending: false) + ] + + let firstShareLink = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName) + .first + if let firstShareLink = firstShareLink { - let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare).sorted(by: sortProperties) - return(firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) })) + let results = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare) + .sorted(by: sortProperties) + + return (firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) })) } else { - let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName).sorted(by: sortProperties) - return(firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) })) + let results = realm + .objects(tableShare.self) + .filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName) + .sorted(by: sortProperties) + + return (firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) })) } + } catch let error as NSError { NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)") } + return (nil, nil) } @@ -190,6 +221,9 @@ extension NCManageDatabase { return [] } + /// + /// Fetch all shares of a file regardless of type. + /// func getTableShares(account: String, serverUrl: String, fileName: String) -> [tableShare] { do { let realm = try Realm() diff --git a/iOSClient/Data/NCManageDatabase.swift b/iOSClient/Data/NCManageDatabase.swift index 2934948612..6e6008ddf5 100644 --- a/iOSClient/Data/NCManageDatabase.swift +++ b/iOSClient/Data/NCManageDatabase.swift @@ -84,7 +84,8 @@ class NCManageDatabase: NSObject { tableDashboardWidget.self, tableDashboardWidgetButton.self, NCDBLayoutForView.self, - TableSecurityGuardDiagnostics.self] + TableSecurityGuardDiagnostics.self, + tableDownloadLimit.self] // Disable file protection for directory DB // https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm diff --git a/iOSClient/Menu/NCShare+Menu.swift b/iOSClient/Menu/NCShare+Menu.swift index 62ab9c8271..2770b5e471 100644 --- a/iOSClient/Menu/NCShare+Menu.swift +++ b/iOSClient/Menu/NCShare+Menu.swift @@ -54,6 +54,7 @@ extension NCShare { advancePermission.share = tableShare(value: share) advancePermission.oldTableShare = tableShare(value: share) advancePermission.metadata = self.metadata + advancePermission.downloadLimit = try? self.database.getDownloadLimit(byShareToken: share.token) navigationController.pushViewController(advancePermission, animated: true) } ) diff --git a/iOSClient/NCCapabilities.swift b/iOSClient/NCCapabilities.swift index 8d56106b48..755665c404 100644 --- a/iOSClient/NCCapabilities.swift +++ b/iOSClient/NCCapabilities.swift @@ -46,6 +46,8 @@ public class NCCapabilities: NSObject { var capabilityFileSharingRemoteExpireDateEnforced: Bool = false var capabilityFileSharingRemoteExpireDateDays: Int = 0 var capabilityFileSharingDefaultPermission: Int = 0 + var capabilityFileSharingDownloadLimit: Bool = false + var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1 var capabilityThemingColor: String = "" var capabilityThemingColorElement: String = "" var capabilityThemingColorText: String = "" diff --git a/iOSClient/Networking/NCNetworking+WebDAV.swift b/iOSClient/Networking/NCNetworking+WebDAV.swift index 22625208b6..271d1c0848 100644 --- a/iOSClient/Networking/NCNetworking+WebDAV.swift +++ b/iOSClient/Networking/NCNetworking+WebDAV.swift @@ -95,6 +95,24 @@ extension NCNetworking { let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file) let metadata = self.database.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE) + // Remove all known download limits from shares related to the given file. + // This avoids obsolete download limit objects to stay around. + // Afterwards create new download limits, should any such be returned for the known shares. + + let shares = self.database.getTableShares(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName) + + do { + try shares.forEach { share in + try self.database.deleteDownloadLimit(byShareToken: share.token) + + if let receivedDownloadLimit = file.downloadLimits.first(where: { $0.token == share.token }) { + try self.database.createDownloadLimit(count: receivedDownloadLimit.count, limit: receivedDownloadLimit.limit, token: receivedDownloadLimit.token) + } + } + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not update download limits: \(error)") + } + completion(account, metadata, error) } } diff --git a/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift b/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift index 86c6f43bcb..ef4c169c18 100644 --- a/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift +++ b/iOSClient/Settings/Advanced/Capabilities/NCCapabilitiesModel.swift @@ -10,6 +10,11 @@ import Foundation import UIKit import SwiftUI +/// +/// Data model for ``NCCapabilitiesView``. +/// +/// Compiles capabilities, their availability and symbol images for display. +/// class NCCapabilitiesModel: ObservableObject, ViewOnAppearHandling { struct Capability: Identifiable, Hashable { let id = UUID() @@ -44,6 +49,9 @@ class NCCapabilitiesModel: ObservableObject, ViewOnAppearHandling { var image = utility.loadImage(named: "person.fill.badge.plus") capabililies.append(Capability(text: "File sharing", image: image, resize: false, available: capability.capabilityFileSharingApiEnabled)) + image = utility.loadImage(named: "gauge.with.dots.needle.bottom.100percent") + capabililies.append(Capability(text: "Download Limit", image: image, resize: false, available: capability.capabilityFileSharingDownloadLimit)) + image = utility.loadImage(named: "network") capabililies.append(Capability(text: "External site", image: image, resize: false, available: capability.capabilityExternalSites)) diff --git a/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift new file mode 100644 index 0000000000..a91a675bc2 --- /dev/null +++ b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitTableViewController.swift @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import NextcloudKit +import UIKit + +/// +/// View controller for the table view managing the input form for download limits. +/// +/// This child view controller is required because table views require a dedicated table view controller. +/// +class NCShareDownloadLimitTableViewController: UITableViewController { + let database = NCManageDatabase.shared + + /// + /// The initial state injected from the parent view controller on appearance. + /// + public var initialDownloadLimit: tableDownloadLimit? + public var metadata: tableMetadata! + public var share: NCTableShareable! + + /// + /// Default value for limits as possibly provided by the server capabilities. + /// + var defaultLimit: Int { + NCCapabilities.shared.getCapabilities(account: metadata.account).capabilityFileSharingDownloadLimitDefaultLimit + } + + /// + /// Share token required to work with download limits. + /// + private var token: String! + + /// + /// The final state to apply once the view is about to disappear. + /// + private var finalDownloadLimit: tableDownloadLimit? + + private var networking: NCShareDownloadLimitNetworking! + + @IBOutlet var limitSwitch: UISwitch! + @IBOutlet var limitTextField: UITextField! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let initialDownloadLimit { + limitSwitch.isOn = true + limitTextField.text = "\(initialDownloadLimit.limit)" + + finalDownloadLimit = tableDownloadLimit() + finalDownloadLimit?.count = initialDownloadLimit.count + finalDownloadLimit?.limit = initialDownloadLimit.limit + finalDownloadLimit?.token = initialDownloadLimit.token + } else { + limitSwitch.isOn = false + } + + if let token = self.database.getTableShare(account: metadata.account, idShare: share.idShare)?.token { + self.token = token + } else { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to resolve share token!") + self.token = "" + } + + networking = NCShareDownloadLimitNetworking(account: metadata.account, delegate: self, token: token) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row == 1 else { + super.tableView(tableView, didSelectRowAt: indexPath) + return + } + + // The accessory text field should become first responder regardless where the user tapped in the table row. + limitTextField.becomeFirstResponder() + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + // Programmatically hide the limit input row depending on limit enablement. + if limitSwitch.isOn == false && indexPath.row == 1 { + return 0 + } + + return super.tableView(tableView, heightForRowAt: indexPath) + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if let finalDownloadLimit { + String(format: NSLocalizedString("_remaining_share_downloads_", comment: "Table footer text for form of configuring download limits."), finalDownloadLimit.limit - finalDownloadLimit.count) + } else { + nil + } + } + + @IBAction func switchDownloadLimit(_ sender: UISwitch) { + if sender.isOn { + finalDownloadLimit = tableDownloadLimit() + finalDownloadLimit?.count = 0 + finalDownloadLimit?.limit = defaultLimit + finalDownloadLimit?.token = token + + limitTextField.text = String(defaultLimit) + } else { + finalDownloadLimit = nil + } + + tableView.reloadData() + dispatchShareDownloadLimitUpdate() + } + + @IBAction func editingAllowedDownloadsDidBegin(_ sender: UITextField) { + sender.selectAll(nil) + } + + @IBAction func editingAllowedDownloadsDidEnd(_ sender: UITextField) { + finalDownloadLimit?.limit = Int(sender.text ?? "1") ?? defaultLimit + finalDownloadLimit?.count = 0 + + tableView.reloadData() + dispatchShareDownloadLimitUpdate() + } + + func dispatchShareDownloadLimitUpdate() { + guard let text = limitTextField.text else { + return + } + + guard let limit = Int(text) else { + return + } + + if limitSwitch.isOn { + networking.setShareDownloadLimit(limit: limit) + } else { + networking.removeShareDownloadLimit() + } + } +} + +// MARK: - NCShareDownloadLimitNetworkingDelegate + +extension NCShareDownloadLimitTableViewController: NCShareDownloadLimitNetworkingDelegate { + func downloadLimitRemoved(by token: String, in account: String) { + do { + try self.database.deleteDownloadLimit(byShareToken: token) + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to delete download limit in database!") + } + } + + func downloadLimitSet(to limit: Int, by token: String, in account: String) { + do { + try self.database.createDownloadLimit(count: 0, limit: limit, token: token) + } catch { + NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Failed to create download limit in database!") + } + } +} diff --git a/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift new file mode 100644 index 0000000000..13e9515353 --- /dev/null +++ b/iOSClient/Share/Advanced/DownloadLimit/NCShareDownloadLimitViewController.swift @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +/// +/// View controller for the download limit detail view in share details. +/// +class NCShareDownloadLimitViewController: UIViewController, NCShareDetail { + public var downloadLimit: tableDownloadLimit? + public var metadata: tableMetadata! + public var onDismiss: (() -> Void)? + public var share: NCTableShareable! + + @IBOutlet var headerContainerView: UIView! + + override func viewDidLoad() { + super.viewDidLoad() + self.setNavigationTitle() + + // Set up header view. + + guard let headerView = (Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader) else { return } + headerContainerView.addSubview(headerView) + headerView.frame = headerContainerView.frame + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.topAnchor.constraint(equalTo: headerContainerView.topAnchor).isActive = true + headerView.bottomAnchor.constraint(equalTo: headerContainerView.bottomAnchor).isActive = true + headerView.leftAnchor.constraint(equalTo: headerContainerView.leftAnchor).isActive = true + headerView.rightAnchor.constraint(equalTo: headerContainerView.rightAnchor).isActive = true + + headerView.setupUI(with: metadata) + + // End editing of inputs when the user taps anywhere else. + + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapGesture) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + guard let tableViewController = segue.destination as? NCShareDownloadLimitTableViewController else { + return + } + + tableViewController.initialDownloadLimit = downloadLimit + tableViewController.metadata = metadata + tableViewController.share = share + } + + @objc private func dismissKeyboard() { + view.endEditing(true) + } +} diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift index 23d395bcd1..8e5df63e92 100644 --- a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift +++ b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift @@ -71,6 +71,7 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFotterDeleg var share: NCTableShareable! var isNewShare: Bool { share is NCTableShareOptions } var metadata: tableMetadata! + var downloadLimit: tableDownloadLimit? var shareConfig: NCShareConfig! var networking: NCShareNetworking? @@ -165,6 +166,14 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFotterDeleg } switch cellConfig { + case .limitDownload: + let storyboard = UIStoryboard(name: "NCShare", bundle: nil) + guard let viewController = storyboard.instantiateViewController(withIdentifier: "NCShareDownloadLimit") as? NCShareDownloadLimitViewController else { return } + viewController.downloadLimit = self.downloadLimit + viewController.metadata = self.metadata + viewController.share = self.share + viewController.onDismiss = tableView.reloadData + self.navigationController?.pushViewController(viewController, animated: true) case .hideDownload: share.hideDownload.toggle() tableView.reloadData() diff --git a/iOSClient/Share/Advanced/NCShareCells.swift b/iOSClient/Share/Advanced/NCShareCells.swift index 575b57c27d..f7418e8b37 100644 --- a/iOSClient/Share/Advanced/NCShareCells.swift +++ b/iOSClient/Share/Advanced/NCShareCells.swift @@ -192,10 +192,14 @@ enum NCLinkPermission: NCPermission { static let forFile: [NCLinkPermission] = [.allowEdit] } +/// +/// Individual aspects of share. +/// enum NCShareDetails: CaseIterable, NCShareCellConfig { func didSelect(for share: NCTableShareable) { switch self { case .hideDownload: share.hideDownload.toggle() + case .limitDownload: return case .expirationDate: return case .password: return case .note: return @@ -207,6 +211,10 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { switch self { case .hideDownload: return NCShareToggleCell(isOn: share.hideDownload) + case .limitDownload: + let cell = UITableViewCell(style: .value1, reuseIdentifier: "downloadLimit") + cell.accessoryType = .disclosureIndicator + return cell case .expirationDate: return NCShareDateCell(share: share) case .password: return NCShareToggleCell(isOn: !share.password.isEmpty, customIcons: ("lock", "lock_open")) @@ -225,6 +233,7 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { var title: String { switch self { case .hideDownload: return NSLocalizedString("_share_hide_download_", comment: "") + case .limitDownload: return NSLocalizedString("_share_limit_download_", comment: "") case .expirationDate: return NSLocalizedString("_share_expiration_date_", comment: "") case .password: return NSLocalizedString("_share_password_protect_", comment: "") case .note: return NSLocalizedString("_share_note_recipient_", comment: "") @@ -232,7 +241,7 @@ enum NCShareDetails: CaseIterable, NCShareCellConfig { } } - case label, hideDownload, expirationDate, password, note + case label, hideDownload, limitDownload, expirationDate, password, note static let forLink: [NCShareDetails] = NCShareDetails.allCases static let forUser: [NCShareDetails] = [.expirationDate, .note] } @@ -248,7 +257,16 @@ struct NCShareConfig { self.resharePermission = parentMetadata.sharePermissionsCollaborationServices let type: NCPermission.Type = share.shareType == NCShareCommon().SHARE_TYPE_LINK ? NCLinkPermission.self : NCUserPermission.self self.permissions = parentMetadata.directory ? (parentMetadata.e2eEncrypted ? type.forDirectoryE2EE(account: parentMetadata.account) : type.forDirectory) : type.forFile - self.advanced = share.shareType == NCShareCommon().SHARE_TYPE_LINK ? NCShareDetails.forLink : NCShareDetails.forUser + + if share.shareType == NCShareCommon().SHARE_TYPE_LINK { + if NCCapabilities.shared.getCapabilities(account: parentMetadata.account).capabilityFileSharingDownloadLimit { + self.advanced = NCShareDetails.forLink + } else { + self.advanced = NCShareDetails.forLink.filter { $0 != .limitDownload } + } + } else { + self.advanced = NCShareDetails.forUser + } } func cellFor(indexPath: IndexPath) -> UITableViewCell? { @@ -275,114 +293,3 @@ struct NCShareConfig { } else { return nil } } } - -class NCShareToggleCell: UITableViewCell { - typealias CustomToggleIcon = (onIconName: String?, offIconName: String?) - init(isOn: Bool, customIcons: CustomToggleIcon? = nil) { - super.init(style: .default, reuseIdentifier: "toggleCell") - self.accessibilityValue = isOn ? NSLocalizedString("_on_", comment: "") : NSLocalizedString("_off_", comment: "") - - guard let customIcons = customIcons, - let iconName = isOn ? customIcons.onIconName : customIcons.offIconName else { - self.accessoryType = isOn ? .checkmark : .none - return - } - let image = NCUtility().loadImage(named: iconName, colors: [NCBrandColor.shared.customer], size: self.frame.height - 26) - self.accessoryView = UIImageView(image: image) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class NCShareDateCell: UITableViewCell { - let picker = UIDatePicker() - let textField = UITextField() - var shareType: Int - var onReload: (() -> Void)? - let shareCommon = NCShareCommon() - - init(share: NCTableShareable) { - self.shareType = share.shareType - super.init(style: .value1, reuseIdentifier: "shareExpDate") - - picker.datePickerMode = .date - picker.minimumDate = Date() - picker.preferredDatePickerStyle = .wheels - picker.action(for: .valueChanged) { datePicker in - guard let datePicker = datePicker as? UIDatePicker else { return } - self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date) - } - accessoryView = textField - - let toolbar = UIToolbar.toolbar { - self.resignFirstResponder() - share.expirationDate = nil - self.onReload?() - } onDone: { - self.resignFirstResponder() - share.expirationDate = self.picker.date as NSDate - self.onReload?() - } - - textField.isAccessibilityElement = false - textField.accessibilityElementsHidden = true - textField.inputAccessoryView = toolbar.wrappedSafeAreaContainer - textField.inputView = picker - - if let expDate = share.expirationDate { - detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date) - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func checkMaximumDate(account: String) { - let defaultExpDays = defaultExpirationDays(account: account) - if defaultExpDays > 0 && isExpireDateEnforced(account: account) { - let enforcedInSecs = TimeInterval(defaultExpDays * 24 * 60 * 60) - self.picker.maximumDate = Date().advanced(by: enforcedInSecs) - } - } - - private func isExpireDateEnforced(account: String) -> Bool { - switch self.shareType { - case shareCommon.SHARE_TYPE_LINK, - shareCommon.SHARE_TYPE_EMAIL, - shareCommon.SHARE_TYPE_GUEST: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateEnforced - case shareCommon.SHARE_TYPE_USER, - shareCommon.SHARE_TYPE_GROUP, - shareCommon.SHARE_TYPE_CIRCLE, - shareCommon.SHARE_TYPE_ROOM: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateEnforced - case shareCommon.SHARE_TYPE_REMOTE, - shareCommon.SHARE_TYPE_REMOTE_GROUP: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateEnforced - default: - return false - } - } - - private func defaultExpirationDays(account: String) -> Int { - switch self.shareType { - case shareCommon.SHARE_TYPE_LINK, - shareCommon.SHARE_TYPE_EMAIL, - shareCommon.SHARE_TYPE_GUEST: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateDays - case shareCommon.SHARE_TYPE_USER, - shareCommon.SHARE_TYPE_GROUP, - shareCommon.SHARE_TYPE_CIRCLE, - shareCommon.SHARE_TYPE_ROOM: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateDays - case shareCommon.SHARE_TYPE_REMOTE, - shareCommon.SHARE_TYPE_REMOTE_GROUP: - return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateDays - default: - return 0 - } - } -} diff --git a/iOSClient/Share/Advanced/NCShareDateCell.swift b/iOSClient/Share/Advanced/NCShareDateCell.swift new file mode 100644 index 0000000000..fab608f1aa --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareDateCell.swift @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Storch +// SPDX-License-Identifier: GPL-3.0-or-later + +/// +/// Table view cell to manage the expiration date on a share in its details. +/// +class NCShareDateCell: UITableViewCell { + let picker = UIDatePicker() + let textField = UITextField() + var shareType: Int + var onReload: (() -> Void)? + let shareCommon = NCShareCommon() + + init(share: NCTableShareable) { + self.shareType = share.shareType + super.init(style: .value1, reuseIdentifier: "shareExpDate") + + picker.datePickerMode = .date + picker.minimumDate = Date() + picker.preferredDatePickerStyle = .wheels + picker.action(for: .valueChanged) { datePicker in + guard let datePicker = datePicker as? UIDatePicker else { return } + self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date) + } + accessoryView = textField + + let toolbar = UIToolbar.toolbar { + self.resignFirstResponder() + share.expirationDate = nil + self.onReload?() + } onDone: { + self.resignFirstResponder() + share.expirationDate = self.picker.date as NSDate + self.onReload?() + } + + textField.isAccessibilityElement = false + textField.accessibilityElementsHidden = true + textField.inputAccessoryView = toolbar.wrappedSafeAreaContainer + textField.inputView = picker + + if let expDate = share.expirationDate { + detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func checkMaximumDate(account: String) { + let defaultExpDays = defaultExpirationDays(account: account) + if defaultExpDays > 0 && isExpireDateEnforced(account: account) { + let enforcedInSecs = TimeInterval(defaultExpDays * 24 * 60 * 60) + self.picker.maximumDate = Date().advanced(by: enforcedInSecs) + } + } + + private func isExpireDateEnforced(account: String) -> Bool { + switch self.shareType { + case shareCommon.SHARE_TYPE_LINK, + shareCommon.SHARE_TYPE_EMAIL, + shareCommon.SHARE_TYPE_GUEST: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateEnforced + case shareCommon.SHARE_TYPE_USER, + shareCommon.SHARE_TYPE_GROUP, + shareCommon.SHARE_TYPE_CIRCLE, + shareCommon.SHARE_TYPE_ROOM: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateEnforced + case shareCommon.SHARE_TYPE_REMOTE, + shareCommon.SHARE_TYPE_REMOTE_GROUP: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateEnforced + default: + return false + } + } + + private func defaultExpirationDays(account: String) -> Int { + switch self.shareType { + case shareCommon.SHARE_TYPE_LINK, + shareCommon.SHARE_TYPE_EMAIL, + shareCommon.SHARE_TYPE_GUEST: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingPubExpireDateDays + case shareCommon.SHARE_TYPE_USER, + shareCommon.SHARE_TYPE_GROUP, + shareCommon.SHARE_TYPE_CIRCLE, + shareCommon.SHARE_TYPE_ROOM: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingInternalExpireDateDays + case shareCommon.SHARE_TYPE_REMOTE, + shareCommon.SHARE_TYPE_REMOTE_GROUP: + return NCCapabilities.shared.getCapabilities(account: account).capabilityFileSharingRemoteExpireDateDays + default: + return 0 + } + } +} diff --git a/iOSClient/Share/Advanced/NCShareToggleCell.swift b/iOSClient/Share/Advanced/NCShareToggleCell.swift new file mode 100644 index 0000000000..b3ff60457a --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareToggleCell.swift @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2022 Henrik Storch +// SPDX-License-Identifier: GPL-3.0-or-later + +/// +/// A table view cell for logical switches in the detaills of a share configuration. +/// +class NCShareToggleCell: UITableViewCell { + typealias CustomToggleIcon = (onIconName: String?, offIconName: String?) + init(isOn: Bool, customIcons: CustomToggleIcon? = nil) { + super.init(style: .default, reuseIdentifier: "toggleCell") + self.accessibilityValue = isOn ? NSLocalizedString("_on_", comment: "") : NSLocalizedString("_off_", comment: "") + + guard let customIcons = customIcons, + let iconName = isOn ? customIcons.onIconName : customIcons.offIconName else { + self.accessoryType = isOn ? .checkmark : .none + return + } + let image = NCUtility().loadImage(named: iconName, colors: [NCBrandColor.shared.customer], size: self.frame.height - 26) + self.accessoryView = UIImageView(image: image) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/iOSClient/Share/NCShare.storyboard b/iOSClient/Share/NCShare.storyboard index d06257f31f..e3e3bbd089 100644 --- a/iOSClient/Share/NCShare.storyboard +++ b/iOSClient/Share/NCShare.storyboard @@ -1,9 +1,9 @@ - + - + @@ -247,6 +247,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -282,16 +395,16 @@ - + - + - + diff --git a/iOSClient/Share/NCShareDownloadLimitNetworking.swift b/iOSClient/Share/NCShareDownloadLimitNetworking.swift new file mode 100644 index 0000000000..325e81c5b7 --- /dev/null +++ b/iOSClient/Share/NCShareDownloadLimitNetworking.swift @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2024 Iva Horn +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +/// +/// Delegate requirements for ``NCShareDownloadLimitNetworking`` to handle results. +/// +protocol NCShareDownloadLimitNetworkingDelegate: AnyObject { + /// + /// The download limit was successfully removed from the share on the server. + /// + func downloadLimitRemoved(by token: String, in account: String) + + /// + /// The download limit was successfully removed from the share on the server. + /// + func downloadLimitSet(to limit: Int, by token: String, in account: String) +} + +/// +/// Share-bound network abstraction for download limits. +/// +class NCShareDownloadLimitNetworking: NSObject { + let account: String + weak var delegate: (any NCShareDownloadLimitNetworkingDelegate)? + weak var view: UIView? + let token: String + + init(account: String, delegate: (any NCShareDownloadLimitNetworkingDelegate)?, token: String) { + self.account = account + self.delegate = delegate + self.token = token + } + + /// + /// Remove the download limit on the share, if existent. + /// + func removeShareDownloadLimit() { + NCActivityIndicator.shared.start(backgroundView: view) + NextcloudKit.shared.removeShareDownloadLimit(account: account, token: token) { error in + NCActivityIndicator.shared.stop() + + if error == .success { + self.delegate?.downloadLimitRemoved(by: self.token, in: self.account) + } else { + NCContentPresenter().showError(error: error) + } + } + } + + /// + /// Set the download limit for the share. + /// + /// - Parameter limit: The new download limit to set. + /// + func setShareDownloadLimit(limit: Int) { + NCActivityIndicator.shared.start(backgroundView: view) + NextcloudKit.shared.setShareDownloadLimit(account: account, token: token, limit: limit) { error in + NCActivityIndicator.shared.stop() + + if error == .success { + self.delegate?.downloadLimitSet(to: limit, by: self.token, in: self.account) + } else { + NCContentPresenter().showError(error: error) + } + } + } +} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index e318445cd5..2b359a52ac 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -675,6 +675,9 @@ "_share_file_drop_" = "File drop (upload only)"; "_share_secure_file_drop_" = "Secure file drop (upload only)"; "_share_hide_download_" = "Hide download"; +"_share_limit_download_" = "Limit downloads"; +"_remaining_share_downloads_" = "%d remaining downloads allowed"; +"_remaining_" = "%d remaining"; "_share_password_protect_" = "Password protect"; "_share_expiration_date_" = "Set expiration date"; "_share_note_recipient_" = "Note to recipient";