瀏覽代碼

V 5.2.0 (#2833)

Signed-off-by: Nextcloud bot <bot@nextcloud.com>
Signed-off-by: Marino Faggiana <marino@marinofaggiana.com>
Signed-off-by: Milen Pivchev <milen.pivchev@gmail.com>
Signed-off-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
Co-authored-by: Nextcloud bot <bot@nextcloud.com>
Co-authored-by: Milen Pivchev <milen.pivchev@gmail.com>
Co-authored-by: rakekniven <2069590+rakekniven@users.noreply.github.com>
Marino Faggiana 1 年之前
父節點
當前提交
62c79619fc
共有 100 個文件被更改,包括 1628 次插入1025 次删除
  1. 二進制
      Animation.gif
  2. 1 1
      Brand/Database.swift
  3. 1 1
      File Provider Extension/FileProviderEnumerator.swift
  4. 2 2
      File Provider Extension/FileProviderExtension+Actions.swift
  5. 13 17
      Nextcloud.xcodeproj/project.pbxproj
  6. 2 1
      Widget/Files/FilesData.swift
  7. 0 9
      iOSClient/Account Request/NCAccountRequest.swift
  8. 15 8
      iOSClient/AppDelegate.swift
  9. 18 17
      iOSClient/Data/NCManageDatabase+Directory.swift
  10. 3 3
      iOSClient/Data/NCManageDatabase+E2EE.swift
  11. 12 2
      iOSClient/Data/NCManageDatabase+Metadata.swift
  12. 17 3
      iOSClient/Files/NCFiles.swift
  13. 1 1
      iOSClient/Groupfolders/NCGroupfolders.swift
  14. 158 68
      iOSClient/Main/Collection Common/NCCollectionViewCommon.swift
  15. 6 11
      iOSClient/Main/Collection Common/NCGridCell.swift
  16. 79 63
      iOSClient/Main/Collection Common/NCGridCell.xib
  17. 14 4
      iOSClient/Main/Collection Common/NCListCell.swift
  18. 19 10
      iOSClient/Main/Collection Common/NCListCell.xib
  19. 11 11
      iOSClient/Main/Collection Common/NCSelectableNavigationView.swift
  20. 5 0
      iOSClient/Main/NCCellProtocol.swift
  21. 3 3
      iOSClient/Main/Section Header Footer/NCSectionFooter.xib
  22. 2 2
      iOSClient/Main/Section Header Footer/NCSectionHeaderMenu.swift
  23. 3 3
      iOSClient/Main/Section Header Footer/NCSectionHeaderMenu.xib
  24. 19 13
      iOSClient/Media/Cell/NCGridMediaCell.swift
  25. 40 27
      iOSClient/Media/Cell/NCGridMediaCell.xib
  26. 216 0
      iOSClient/Media/NCMedia+Command.swift
  27. 65 4
      iOSClient/Media/NCMedia.storyboard
  28. 222 139
      iOSClient/Media/NCMedia.swift
  29. 0 253
      iOSClient/Media/NCMediaCommandView.swift
  30. 0 77
      iOSClient/Media/NCMediaCommandView.xib
  31. 85 65
      iOSClient/Media/NCMediaDataSource.swift
  32. 16 8
      iOSClient/Media/NCMediaDownloadThumbnaill.swift
  33. 0 79
      iOSClient/Media/NCMediaGridLayout.swift
  34. 320 0
      iOSClient/Media/NCMediaLayout.swift
  35. 1 1
      iOSClient/Menu/NCCollectionViewCommon+Menu.swift
  36. 5 4
      iOSClient/NCGlobal.swift
  37. 88 38
      iOSClient/NCImageCache.swift
  38. 3 3
      iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift
  39. 8 8
      iOSClient/Networking/E2EE/NCEndToEndMetadataV20.swift
  40. 89 29
      iOSClient/Networking/E2EE/NCNetworkingE2EE.swift
  41. 1 1
      iOSClient/Networking/E2EE/NCNetworkingE2EECreateFolder.swift
  42. 1 1
      iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift
  43. 12 10
      iOSClient/Networking/NCNetworking+Download.swift
  44. 5 6
      iOSClient/Networking/NCNetworking+WebDAV.swift
  45. 1 1
      iOSClient/Recent/NCRecent.swift
  46. 1 1
      iOSClient/Settings/CCAdvanced.m
  47. 41 17
      iOSClient/Settings/NCKeychain.swift
  48. 二進制
      iOSClient/Supporting Files/af.lproj/Localizable.strings
  49. 二進制
      iOSClient/Supporting Files/an.lproj/Localizable.strings
  50. 二進制
      iOSClient/Supporting Files/ar.lproj/Localizable.strings
  51. 二進制
      iOSClient/Supporting Files/ast.lproj/Localizable.strings
  52. 二進制
      iOSClient/Supporting Files/az.lproj/Localizable.strings
  53. 二進制
      iOSClient/Supporting Files/be.lproj/Localizable.strings
  54. 二進制
      iOSClient/Supporting Files/bg_BG.lproj/Localizable.strings
  55. 二進制
      iOSClient/Supporting Files/bn_BD.lproj/Localizable.strings
  56. 二進制
      iOSClient/Supporting Files/br.lproj/Localizable.strings
  57. 二進制
      iOSClient/Supporting Files/bs.lproj/Localizable.strings
  58. 二進制
      iOSClient/Supporting Files/ca.lproj/Localizable.strings
  59. 二進制
      iOSClient/Supporting Files/cs-CZ.lproj/Localizable.strings
  60. 二進制
      iOSClient/Supporting Files/cy_GB.lproj/Localizable.strings
  61. 二進制
      iOSClient/Supporting Files/da.lproj/Localizable.strings
  62. 二進制
      iOSClient/Supporting Files/de.lproj/Localizable.strings
  63. 二進制
      iOSClient/Supporting Files/el.lproj/Localizable.strings
  64. 二進制
      iOSClient/Supporting Files/en-GB.lproj/Localizable.strings
  65. 4 0
      iOSClient/Supporting Files/en.lproj/Localizable.strings
  66. 二進制
      iOSClient/Supporting Files/eo.lproj/Localizable.strings
  67. 二進制
      iOSClient/Supporting Files/es-419.lproj/Localizable.strings
  68. 二進制
      iOSClient/Supporting Files/es-AR.lproj/Localizable.strings
  69. 二進制
      iOSClient/Supporting Files/es-CL.lproj/Localizable.strings
  70. 二進制
      iOSClient/Supporting Files/es-CO.lproj/Localizable.strings
  71. 二進制
      iOSClient/Supporting Files/es-CR.lproj/Localizable.strings
  72. 二進制
      iOSClient/Supporting Files/es-DO.lproj/Localizable.strings
  73. 二進制
      iOSClient/Supporting Files/es-EC.lproj/Localizable.strings
  74. 二進制
      iOSClient/Supporting Files/es-GT.lproj/Localizable.strings
  75. 二進制
      iOSClient/Supporting Files/es-HN.lproj/Localizable.strings
  76. 二進制
      iOSClient/Supporting Files/es-MX.lproj/Localizable.strings
  77. 二進制
      iOSClient/Supporting Files/es-NI.lproj/Localizable.strings
  78. 二進制
      iOSClient/Supporting Files/es-PA.lproj/Localizable.strings
  79. 二進制
      iOSClient/Supporting Files/es-PE.lproj/Localizable.strings
  80. 二進制
      iOSClient/Supporting Files/es-PR.lproj/Localizable.strings
  81. 二進制
      iOSClient/Supporting Files/es-PY.lproj/Localizable.strings
  82. 二進制
      iOSClient/Supporting Files/es-SV.lproj/Localizable.strings
  83. 二進制
      iOSClient/Supporting Files/es-UY.lproj/Localizable.strings
  84. 二進制
      iOSClient/Supporting Files/es.lproj/Localizable.strings
  85. 二進制
      iOSClient/Supporting Files/et_EE.lproj/Localizable.strings
  86. 二進制
      iOSClient/Supporting Files/eu.lproj/Localizable.strings
  87. 二進制
      iOSClient/Supporting Files/fa.lproj/Localizable.strings
  88. 二進制
      iOSClient/Supporting Files/fi-FI.lproj/Localizable.strings
  89. 二進制
      iOSClient/Supporting Files/fo.lproj/Localizable.strings
  90. 二進制
      iOSClient/Supporting Files/fr.lproj/Localizable.strings
  91. 二進制
      iOSClient/Supporting Files/gd.lproj/Localizable.strings
  92. 二進制
      iOSClient/Supporting Files/gl.lproj/Localizable.strings
  93. 二進制
      iOSClient/Supporting Files/he.lproj/Localizable.strings
  94. 二進制
      iOSClient/Supporting Files/hi_IN.lproj/Localizable.strings
  95. 二進制
      iOSClient/Supporting Files/hr.lproj/Localizable.strings
  96. 二進制
      iOSClient/Supporting Files/hsb.lproj/Localizable.strings
  97. 二進制
      iOSClient/Supporting Files/hu.lproj/Localizable.strings
  98. 二進制
      iOSClient/Supporting Files/hy.lproj/Localizable.strings
  99. 二進制
      iOSClient/Supporting Files/ia.lproj/Localizable.strings
  100. 二進制
      iOSClient/Supporting Files/id.lproj/Localizable.strings

二進制
Animation.gif


+ 1 - 1
Brand/Database.swift

@@ -26,4 +26,4 @@ import Foundation
 // Database Realm
 //
 let databaseName                    = "nextcloud.realm"
-let databaseSchemaVersion: UInt64   = 342
+let databaseSchemaVersion: UInt64   = 345

+ 1 - 1
File Provider Extension/FileProviderEnumerator.swift

@@ -217,7 +217,7 @@ class FileProviderEnumerator: NSObject, NSFileProviderEnumerator {
                                 NCManageDatabase.shared.updateMetadatas(metadatas, predicate: predicate)
                                 for metadata in metadatasFolder {
                                     let serverUrl = metadata.serverUrl + "/" + metadata.fileNameView
-                                    NCManageDatabase.shared.addDirectory(encrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrl, account: metadata.account)
+                                    NCManageDatabase.shared.addDirectory(e2eEncrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrl, account: metadata.account)
                                 }
                                 let metadatas = NCManageDatabase.shared.getAdvancedMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", fileProviderData.shared.account, serverUrl), sorted: "fileName", ascending: true)
                                 completionHandler(metadatas)

+ 2 - 2
File Provider Extension/FileProviderExtension+Actions.swift

@@ -48,7 +48,7 @@ extension FileProviderExtension {
                         let isDirectoryEncrypted = self.utilityFileSystem.isDirectoryE2EE(file: file)
                         let metadata = NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryEncrypted)
 
-                        NCManageDatabase.shared.addDirectory(encrypted: false, favorite: false, ocId: ocId!, fileId: metadata.fileId, etag: metadata.etag, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
+                        NCManageDatabase.shared.addDirectory(e2eEncrypted: false, favorite: false, ocId: ocId!, fileId: metadata.fileId, etag: metadata.etag, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
                         NCManageDatabase.shared.addMetadata(metadata)
 
                         guard let metadataInsert = NCManageDatabase.shared.getMetadataFromOcId(ocId!) else {
@@ -194,7 +194,7 @@ extension FileProviderExtension {
 
                 if metadata.directory {
 
-                    NCManageDatabase.shared.setDirectory(serverUrl: fileNamePathFrom, serverUrlTo: fileNamePathTo, etag: nil, ocId: nil, fileId: nil, encrypted: directoryTable.e2eEncrypted, richWorkspace: nil, account: account)
+                    NCManageDatabase.shared.setDirectory(serverUrl: fileNamePathFrom, serverUrlTo: fileNamePathTo, encrypted: directoryTable.e2eEncrypted, account: account)
 
                 } else {
 

+ 13 - 17
Nextcloud.xcodeproj/project.pbxproj

@@ -213,6 +213,7 @@
 		F7239871253D86B600257F49 /* NCEmptyDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7239870253D86B600257F49 /* NCEmptyDataSet.swift */; };
 		F7239877253D86D300257F49 /* NCEmptyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7239876253D86D300257F49 /* NCEmptyView.xib */; };
 		F723B3DD22FC6D1D00301EFE /* NCShareCommentsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F723B3DC22FC6D1C00301EFE /* NCShareCommentsCell.xib */; };
+		F72408332B8A27C900F128E2 /* NCMedia+Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72408322B8A27C900F128E2 /* NCMedia+Command.swift */; };
 		F72429362AFE39860040AEF3 /* NCLivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70968A324212C4E00ED60E5 /* NCLivePhoto.swift */; };
 		F72429372AFE39980040AEF3 /* NCLivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70968A324212C4E00ED60E5 /* NCLivePhoto.swift */; };
 		F72429382AFE39A80040AEF3 /* NCLivePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70968A324212C4E00ED60E5 /* NCLivePhoto.swift */; };
@@ -432,6 +433,7 @@
 		F75379222AE2ADA100C0250E /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = F75379212AE2ADA100C0250E /* JGProgressHUD */; };
 		F753BA93281FD8020015BFB6 /* EasyTipView in Frameworks */ = {isa = PBXBuildFile; productRef = F753BA92281FD8020015BFB6 /* EasyTipView */; };
 		F755BD9B20594AC7008C5FBB /* NCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F755BD9A20594AC7008C5FBB /* NCService.swift */; };
+		F755CB402B8CB13C00CE27E9 /* NCMediaLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F755CB3F2B8CB13C00CE27E9 /* NCMediaLayout.swift */; };
 		F757CC8229E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757CC8129E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift */; };
 		F757CC8329E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757CC8129E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift */; };
 		F757CC8429E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757CC8129E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift */; };
@@ -622,7 +624,6 @@
 		F78ACD54219047D40088454D /* NCSectionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = F78ACD53219047D40088454D /* NCSectionFooter.xib */; };
 		F78B87E72B62527100C65ADC /* NCMediaDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */; };
 		F78B87E92B62550800C65ADC /* NCMediaDownloadThumbnaill.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */; };
-		F78B87EB2B627AA100C65ADC /* NCMediaCommandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78B87EA2B627AA100C65ADC /* NCMediaCommandView.swift */; };
 		F78C6FDE296D677300C952C3 /* NCContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78C6FDD296D677300C952C3 /* NCContextMenu.swift */; };
 		F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; };
 		F78E2D6629AF02DB0024D4F3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = F78E2D6429AF02DB0024D4F3 /* Database.swift */; };
@@ -712,7 +713,6 @@
 		F7BB7E4727A18C56009B9F29 /* Parchment in Frameworks */ = {isa = PBXBuildFile; productRef = F7BB7E4627A18C56009B9F29 /* Parchment */; };
 		F7BC287E26663F6C004D46C5 /* NCViewCertificateDetails.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */; };
 		F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BC287F26663F85004D46C5 /* NCViewCertificateDetails.swift */; };
-		F7BD50312B65216300D5AEF9 /* NCMediaGridLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BD50302B65216300D5AEF9 /* NCMediaGridLayout.swift */; };
 		F7BD71E62636EAFC00643C34 /* NCNetworkingE2EE.swift in Sources */ = {isa = PBXBuildFile; fileRef = F785EE9C246196DF00B3F945 /* NCNetworkingE2EE.swift */; };
 		F7BF9D822934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; };
 		F7BF9D832934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BF9D812934CA21009EE9A6 /* NCManageDatabase+LayoutForView.swift */; };
@@ -781,7 +781,6 @@
 		F7EE66AD2A20B226009AE765 /* UILabel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */; };
 		F7EFA47825ADBA500083159A /* NCViewerProviderContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EFA47725ADBA500083159A /* NCViewerProviderContextMenu.swift */; };
 		F7EFC0CD256BF8DD00461AAD /* NCUserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */; };
-		F7F1E54C2492369A00E42386 /* NCMediaCommandView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7F1E54B2492369A00E42386 /* NCMediaCommandView.xib */; };
 		F7F4F10527ECDBDB008676F9 /* Inconsolata-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */; };
 		F7F4F10627ECDBDB008676F9 /* Inconsolata-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */; };
 		F7F4F10727ECDBDB008676F9 /* Inconsolata-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F7F4F0FF27ECDBDB008676F9 /* Inconsolata-Black.ttf */; };
@@ -1093,6 +1092,7 @@
 		F7239870253D86B600257F49 /* NCEmptyDataSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEmptyDataSet.swift; sourceTree = "<group>"; };
 		F7239876253D86D300257F49 /* NCEmptyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCEmptyView.xib; sourceTree = "<group>"; };
 		F723B3DC22FC6D1C00301EFE /* NCShareCommentsCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCShareCommentsCell.xib; sourceTree = "<group>"; };
+		F72408322B8A27C900F128E2 /* NCMedia+Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+Command.swift"; sourceTree = "<group>"; };
 		F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThreadSafeDictionary.swift; sourceTree = "<group>"; };
 		F7267A81225DFCE100D6DB7D /* AFNetworking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AFNetworking.framework; path = Carthage/Build/iOS/AFNetworking.framework; sourceTree = "<group>"; };
 		F72685E827C78E490019EF5E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -1168,6 +1168,7 @@
 		F753701922723E0D0041C76C /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
 		F753701A22723EC80041C76C /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Localizable.strings; sourceTree = "<group>"; };
 		F755BD9A20594AC7008C5FBB /* NCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCService.swift; sourceTree = "<group>"; };
+		F755CB3F2B8CB13C00CE27E9 /* NCMediaLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCMediaLayout.swift; sourceTree = "<group>"; };
 		F757CC8129E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Groupfolders.swift"; sourceTree = "<group>"; };
 		F757CC8A29E82D0500F31428 /* NCGroupfolders.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCGroupfolders.storyboard; sourceTree = "<group>"; };
 		F757CC8B29E82D0500F31428 /* NCGroupfolders.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCGroupfolders.swift; sourceTree = "<group>"; };
@@ -1290,7 +1291,6 @@
 		F78ACD53219047D40088454D /* NCSectionFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCSectionFooter.xib; sourceTree = "<group>"; };
 		F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaDataSource.swift; sourceTree = "<group>"; };
 		F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaDownloadThumbnaill.swift; sourceTree = "<group>"; };
-		F78B87EA2B627AA100C65ADC /* NCMediaCommandView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaCommandView.swift; sourceTree = "<group>"; };
 		F78C6FDD296D677300C952C3 /* NCContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenu.swift; sourceTree = "<group>"; };
 		F78D6F461F0B7CB9002F9619 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		F78D6F4D1F0B7CE4002F9619 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -1390,7 +1390,6 @@
 		F7BB04851FD58ACB00BBFD2A /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewCertificateDetails.storyboard; sourceTree = "<group>"; };
 		F7BC287F26663F85004D46C5 /* NCViewCertificateDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewCertificateDetails.swift; sourceTree = "<group>"; };
-		F7BD50302B65216300D5AEF9 /* NCMediaGridLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMediaGridLayout.swift; sourceTree = "<group>"; };
 		F7BE7C25290AC8C9002ABB61 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Intent.strings; sourceTree = "<group>"; };
 		F7BE7C27290ADEFD002ABB61 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intent.strings; sourceTree = "<group>"; };
 		F7BE7C29290ADEFD002ABB61 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Intent.strings; sourceTree = "<group>"; };
@@ -1493,7 +1492,6 @@
 		F7EE66AC2A20B226009AE765 /* UILabel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Extension.swift"; sourceTree = "<group>"; };
 		F7EFA47725ADBA500083159A /* NCViewerProviderContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewerProviderContextMenu.swift; sourceTree = "<group>"; };
 		F7EFC0CC256BF8DD00461AAD /* NCUserStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCUserStatus.swift; sourceTree = "<group>"; };
-		F7F1E54B2492369A00E42386 /* NCMediaCommandView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCMediaCommandView.xib; sourceTree = "<group>"; };
 		F7F35B592578FB63003F5589 /* CollaboraOnlineWebViewKeyboardManager.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CollaboraOnlineWebViewKeyboardManager.framework; path = Carthage/Build/iOS/CollaboraOnlineWebViewKeyboardManager.framework; sourceTree = "<group>"; };
 		F7F4F0FD27ECDBDB008676F9 /* Inconsolata-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-SemiBold.ttf"; sourceTree = "<group>"; };
 		F7F4F0FE27ECDBDB008676F9 /* Inconsolata-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inconsolata-Medium.ttf"; sourceTree = "<group>"; };
@@ -2538,11 +2536,10 @@
 				F720B5B72507B9A5008C94E5 /* Cell */,
 				F7501C302212E57400FB1415 /* NCMedia.storyboard */,
 				F7501C312212E57400FB1415 /* NCMedia.swift */,
-				F78B87EA2B627AA100C65ADC /* NCMediaCommandView.swift */,
-				F7F1E54B2492369A00E42386 /* NCMediaCommandView.xib */,
+				F72408322B8A27C900F128E2 /* NCMedia+Command.swift */,
 				F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */,
 				F78B87E82B62550800C65ADC /* NCMediaDownloadThumbnaill.swift */,
-				F7BD50302B65216300D5AEF9 /* NCMediaGridLayout.swift */,
+				F755CB3F2B8CB13C00CE27E9 /* NCMediaLayout.swift */,
 				F741C2232B6B9FD600E849BB /* NCMediaSelectTabBar.swift */,
 			);
 			path = Media;
@@ -3372,7 +3369,6 @@
 				F7F4F10B27ECDBDB008676F9 /* Inconsolata-Light.ttf in Resources */,
 				3704EB2A23D5A58400455C5B /* NCMenu.storyboard in Resources */,
 				AF93471C27E2361E002537EE /* NCShareAdvancePermissionHeader.xib in Resources */,
-				F7F1E54C2492369A00E42386 /* NCMediaCommandView.xib in Resources */,
 				F76032A0252F0F8E0015A421 /* NCTransferCell.xib in Resources */,
 				F7F4F10527ECDBDB008676F9 /* Inconsolata-SemiBold.ttf in Resources */,
 				F7A48415297028FC00BD1B49 /* Nextcloud Hub.png in Resources */,
@@ -3927,8 +3923,9 @@
 				F76B649C2ADFFAED00014640 /* NCImageCache.swift in Sources */,
 				F78A18B823CDE2B300F681F3 /* NCViewerRichWorkspace.swift in Sources */,
 				F7A60F86292D215000FCE1F2 /* NCShareAccounts.swift in Sources */,
+				F72408332B8A27C900F128E2 /* NCMedia+Command.swift in Sources */,
 				F77910AB25DD53C700CEDB9E /* NCSettingsBundleHelper.swift in Sources */,
-				F78B87EB2B627AA100C65ADC /* NCMediaCommandView.swift in Sources */,
+				F755CB402B8CB13C00CE27E9 /* NCMediaLayout.swift in Sources */,
 				F73EF7B72B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift in Sources */,
 				AF4BF61927562A4B0081CEEF /* NCManageDatabase+Metadata.swift in Sources */,
 				F73EF7E72B0226B90087E6E9 /* NCManageDatabase+UserStatus.swift in Sources */,
@@ -4001,7 +3998,6 @@
 				F72944F22A84246400246839 /* NCEndToEndMetadataV20.swift in Sources */,
 				F70BFC7420E0FA7D00C67599 /* NCUtility.swift in Sources */,
 				F79EDAA526B004980007D134 /* NCPlayer.swift in Sources */,
-				F7BD50312B65216300D5AEF9 /* NCMediaGridLayout.swift in Sources */,
 				F7C1EEA525053A9C00866ACC /* NCDataSource.swift in Sources */,
 				F713FF002472764100214AF6 /* UIImage+animatedGIF.m in Sources */,
 				AFCE353527E4ED5900FEA6C2 /* DateFormatter+Extension.swift in Sources */,
@@ -4995,7 +4991,7 @@
 				CLANG_WARN_UNREACHABLE_CODE = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 7;
+				CURRENT_PROJECT_VERSION = 9;
 				DEBUG_INFORMATION_FORMAT = dwarf;
 				DEVELOPMENT_TEAM = NKUJUXUJ3B;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -5021,7 +5017,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 5.1.0;
+				MARKETING_VERSION = 5.2.0;
 				ONLY_ACTIVE_ARCH = YES;
 				OTHER_LDFLAGS = "";
 				SDKROOT = iphoneos;
@@ -5060,7 +5056,7 @@
 				CLANG_WARN_UNREACHABLE_CODE = YES;
 				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
 				COPY_PHASE_STRIP = NO;
-				CURRENT_PROJECT_VERSION = 7;
+				CURRENT_PROJECT_VERSION = 9;
 				DEVELOPMENT_TEAM = NKUJUXUJ3B;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
@@ -5083,7 +5079,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 5.1.0;
+				MARKETING_VERSION = 5.2.0;
 				ONLY_ACTIVE_ARCH = YES;
 				OTHER_LDFLAGS = "";
 				SDKROOT = iphoneos;
@@ -5375,7 +5371,7 @@
 			repositoryURL = "https://github.com/nextcloud/NextcloudKit";
 			requirement = {
 				kind = exactVersion;
-				version = 2.9.6;
+				version = 2.9.7;
 			};
 		};
 		F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {

+ 2 - 1
Widget/Files/FilesData.swift

@@ -241,7 +241,8 @@ func getFilesDataEntry(configuration: AccountIntent?, isPreview: Bool, displaySi
                     let fileNamePathOrFileId = utilityFileSystem.getFileNamePath(file.fileName, serverUrl: file.serverUrl, urlBase: file.urlBase, userId: file.userId)
                     let fileNamePreviewLocalPath = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(file.ocId, etag: file.etag)
                     let fileNameIconLocalPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(file.ocId, etag: file.etag)
-                    let (_, _, imageIcon, _, _, _) = await NextcloudKit.shared.downloadPreview(fileNamePathOrFileId: fileNamePathOrFileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, widthPreview: NCGlobal.shared.sizePreview, heightPreview: NCGlobal.shared.sizePreview, fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: NCGlobal.shared.sizeIcon, options: options)
+                    let sizePreview = NCUtility().getSizePreview(width: Int(file.width), height: Int(file.height))
+                    let (_, _, imageIcon, _, _, _) = await NextcloudKit.shared.downloadPreview(fileNamePathOrFileId: fileNamePathOrFileId, fileNamePreviewLocalPath: fileNamePreviewLocalPath, widthPreview: Int(sizePreview.width), heightPreview: Int(sizePreview.height), fileNameIconLocalPath: fileNameIconLocalPath, sizeIcon: NCGlobal.shared.sizeIcon, options: options)
                     if let image = imageIcon {
                         imageRecent = image
                     }

+ 0 - 9
iOSClient/Account Request/NCAccountRequest.swift

@@ -141,15 +141,6 @@ extension NCAccountRequest: UITableViewDelegate {
         progressView.progress = 0
     }
 
-    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
-        if decelerate {
-//            startTimer()
-        }
-    }
-
-    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
-//        startTimer()
-    }
 
     func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
         return heightCell

+ 15 - 8
iOSClient/AppDelegate.swift

@@ -124,6 +124,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             NCManageDatabase.shared.setCapabilities(account: account)
 
             NCBrandColor.shared.settingThemingColor(account: activeAccount.account)
+            DispatchQueue.global().async {
+                NCImageCache.shared.createMediaCache(account: self.account, withCacheSize: true)
+            }
 
         } else {
 
@@ -181,8 +184,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
         NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Application did become active")
 
-        DispatchQueue.global().async { NCImageCache.shared.createMediaCache(account: self.account) }
-
         NCSettingsBundleHelper.setVersionAndBuildNumber()
         NCSettingsBundleHelper.checkAndExecuteSettings(delay: 0.5)
 
@@ -245,10 +246,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
     // L' applicazione è entrata nello sfondo
     func applicationDidEnterBackground(_ application: UIApplication) {
-
         NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Application did enter in background")
-
         guard !account.isEmpty else { return }
+
         let activeAccount = NCManageDatabase.shared.getActiveAccount()
 
         if let autoUpload = activeAccount?.autoUpload, autoUpload {
@@ -599,6 +599,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
 
         guard let tableAccount = NCManageDatabase.shared.setAccountActive(account) else { return }
 
+        if account != self.account {
+            DispatchQueue.global().async {
+                if NCManageDatabase.shared.getAccounts()?.count == 1 {
+                    NCImageCache.shared.createMediaCache(account: account, withCacheSize: true)
+                } else {
+                    NCImageCache.shared.createMediaCache(account: account, withCacheSize: false)
+                }
+            }
+        }
+
         self.account = tableAccount.account
         self.urlBase = tableAccount.urlBase
         self.user = tableAccount.user
@@ -620,10 +630,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
             NextcloudKit.shared.nkCommonInstance.writeLog("[INFO] Initialize Auto upload with \(items) uploads")
         }
 
-        DispatchQueue.global().async {
-            NCImageCache.shared.createMediaCache(account: self.account)
-            NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeUser)
-        }
+        NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeUser)
     }
 
     @objc func deleteAccount(_ account: String, wipe: Bool) {

+ 18 - 17
iOSClient/Data/NCManageDatabase+Directory.swift

@@ -47,28 +47,29 @@ class tableDirectory: Object {
 
 extension NCManageDatabase {
 
-    func addDirectory(encrypted: Bool, favorite: Bool, ocId: String, fileId: String, etag: String? = nil, permissions: String? = nil, serverUrl: String, account: String) {
+    func addDirectory(e2eEncrypted: Bool, favorite: Bool, ocId: String, fileId: String, etag: String? = nil, permissions: String? = nil, richWorkspace: String? = nil, serverUrl: String, account: String) {
         do {
             let realm = try Realm()
             try realm.write {
-                var addObject = tableDirectory()
-                if let result = realm.objects(tableDirectory.self).filter("ocId == %@", ocId).first {
-                    addObject = result
+                if let result = realm.objects(tableDirectory.self).filter("account == %@ AND ocId == %@", account, ocId).first {
+                    result.e2eEncrypted = e2eEncrypted
+                    result.favorite = favorite
+                    if let etag { result.etag = etag }
+                    if let permissions { result.permissions = permissions }
+                    if let richWorkspace { result.richWorkspace = richWorkspace }
                 } else {
-                    addObject.ocId = ocId
+                    let result = tableDirectory()
+                    result.e2eEncrypted = e2eEncrypted
+                    result.favorite = favorite
+                    result.ocId = ocId
+                    result.fileId = fileId
+                    if let etag { result.etag = etag }
+                    if let permissions { result.permissions = permissions }
+                    if let richWorkspace { result.richWorkspace = richWorkspace }
+                    result.serverUrl = serverUrl
+                    result.account = account
+                    realm.add(result, update: .all)
                 }
-                addObject.account = account
-                addObject.e2eEncrypted = encrypted
-                addObject.favorite = favorite
-                addObject.fileId = fileId
-                if let etag = etag {
-                    addObject.etag = etag
-                }
-                if let permissions = permissions {
-                    addObject.permissions = permissions
-                }
-                addObject.serverUrl = serverUrl
-                realm.add(addObject, update: .all)
             }
         } catch let error {
             NextcloudKit.shared.nkCommonInstance.writeLog("Could not write to database: \(error)")

+ 3 - 3
iOSClient/Data/NCManageDatabase+E2EE.swift

@@ -34,8 +34,8 @@ class tableE2eEncryptionLock: Object {
     @Persisted var e2eToken = ""
 }
 
-typealias tableE2eEncryption = tableE2eEncryptionV3
-class tableE2eEncryptionV3: Object {
+typealias tableE2eEncryption = tableE2eEncryptionV4
+class tableE2eEncryptionV4: Object {
 
     @Persisted(primaryKey: true) var primaryKey = ""
     @Persisted var account = ""
@@ -47,7 +47,7 @@ class tableE2eEncryptionV3: Object {
     @Persisted var initializationVector = ""
     @Persisted var metadataKey = ""
     @Persisted var metadataKeyIndex: Int = 0
-    @Persisted var metadataVersion: Double = 0
+    @Persisted var version: String = ""
     @Persisted var mimeType = ""
     @Persisted var ocIdServerUrl: String = ""
     @Persisted var serverUrl = ""

+ 12 - 2
iOSClient/Data/NCManageDatabase+Metadata.swift

@@ -121,6 +121,7 @@ class tableMetadata: Object, NCUserBaseUrl {
     @objc dynamic var userId = ""
     @objc dynamic var latitude: Double = 0
     @objc dynamic var longitude: Double = 0
+    @objc dynamic var altitude: Double = 0
     @objc dynamic var height: Int = 0
     @objc dynamic var width: Int = 0
     @objc dynamic var errorCode: Int = 0
@@ -272,6 +273,14 @@ extension tableMetadata {
         !isFlaggedAsLivePhotoByServer
     }
 
+    var imageSize: CGSize {
+        CGSize(width: width, height: height)
+    }
+
+    var hasPreviewBorder: Bool {
+        !isImage && !isAudioOrVideo && hasPreview && NCUtilityFileSystem().fileProviderStoragePreviewIconExists(ocId, etag: etag)
+    }
+
     /// Returns false if the user is lokced out of the file. I.e. The file is locked but by somone else
     func canUnlock(as user: String) -> Bool {
         return !lock || (lockOwner == user && lockOwnerType == 0)
@@ -360,8 +369,9 @@ extension NCManageDatabase {
         metadata.userId = file.userId
         metadata.latitude = file.latitude
         metadata.longitude = file.longitude
-        metadata.height = file.height
-        metadata.width = file.width
+        metadata.altitude = file.altitude
+        metadata.height = Int(file.height)
+        metadata.width = Int(file.width)
         metadata.livePhotoFile = file.livePhotoFile
         metadata.isFlaggedAsLivePhotoByServer = file.isFlaggedAsLivePhotoByServer
 

+ 17 - 3
iOSClient/Files/NCFiles.swift

@@ -70,7 +70,7 @@ class NCFiles: NCCollectionViewCommon {
                 }
 
                 self.titleCurrentFolder = self.getNavigationTitle()
-                self.setNavigationItems()
+                self.setNavigationLeftItems()
 
                 self.reloadDataSource()
                 self.reloadDataSourceNetwork()
@@ -204,11 +204,25 @@ class NCFiles: NCCollectionViewCommon {
                        NCKeychain().isEndToEndEnabled(account: self.appDelegate.account),
                        !NCNetworkingE2EE().isInUpload(account: self.appDelegate.account, serverUrl: self.serverUrl) {
                         let lock = NCManageDatabase.shared.getE2ETokenLock(account: self.appDelegate.account, serverUrl: self.serverUrl)
-                        NextcloudKit.shared.getE2EEMetadata(fileId: metadataFolder.ocId, e2eToken: lock?.e2eToken, options: NCNetworkingE2EE().getOptions()) { account, e2eMetadata, signature, _, error in
+                        NCNetworkingE2EE().getMetadata(fileId: metadataFolder.ocId, e2eToken: lock?.e2eToken) { account, version, e2eMetadata, signature, _, error in
                             if error == .success, let e2eMetadata = e2eMetadata {
                                 let error = NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: signature, serverUrl: self.serverUrl, account: account, urlBase: self.appDelegate.urlBase, userId: self.appDelegate.userId)
                                 if error == .success {
-                                    self.reloadDataSource()
+                                    if version == "v1", NCGlobal.shared.capabilityE2EEApiVersion == NCGlobal.shared.e2eeVersionV20 {
+                                        NextcloudKit.shared.nkCommonInstance.writeLog("[E2EE] Conversion v1 to v2")
+                                        NCActivityIndicator.shared.start()
+                                        Task {
+                                            let serverUrl = metadataFolder.serverUrl + "/" + metadataFolder.fileName
+                                            let error = await NCNetworkingE2EE().uploadMetadata(account: metadataFolder.account, serverUrl: serverUrl, userId: metadataFolder.userId, updateVersionV1V2: true)
+                                            if error != .success {
+                                                NCContentPresenter().showError(error: error)
+                                            }
+                                            NCActivityIndicator.shared.stop()
+                                            self.reloadDataSource()
+                                        }
+                                    } else {
+                                        self.reloadDataSource()
+                                    }
                                 } else {
                                     // Client Diagnostic
                                     NCManageDatabase.shared.addDiagnostic(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors)

+ 1 - 1
iOSClient/Groupfolders/NCGroupfolders.swift

@@ -93,7 +93,7 @@ class NCGroupfolders: NCCollectionViewCommon {
                                 let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file)
                                 let metadata = NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE)
                                 NCManageDatabase.shared.addMetadata(metadata)
-                                NCManageDatabase.shared.addDirectory(encrypted: isDirectoryE2EE, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
+                                NCManageDatabase.shared.addDirectory(e2eEncrypted: isDirectoryE2EE, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
                             }
                         }
                     }

+ 158 - 68
iOSClient/Main/Collection Common/NCCollectionViewCommon.swift

@@ -64,7 +64,13 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
     var listLayout: NCListLayout!
     var gridLayout: NCGridLayout!
     var literalSearch: String?
-    var isReloadDataSourceNetworkInProgress: Bool = false
+    var isReloadDataSourceNetworkInProgress: Bool = false {
+        didSet {
+            DispatchQueue.main.async {
+                self.setNavigationRightItems(enableMoreMenu: !self.isReloadDataSourceNetworkInProgress)
+            }
+        }
+    }
     var tabBarSelect: NCSelectableViewTabBar?
 
     var timerNotificationCenter: Timer?
@@ -82,6 +88,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
     var emptyTitle: String = ""
     var emptyDescription: String = ""
 
+    private var showDescription: Bool {
+        !headerRichWorkspaceDisable && NCKeychain().showDescription
+    }
+
+    private var infoLabelsSeparator: String {
+        layoutForView?.layout == NCGlobal.shared.layoutList ? " - " : ""
+    }
+
     // MARK: - View Life Cycle
 
     required init?(coder aDecoder: NSCoder) {
@@ -215,12 +229,14 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
         navigationController?.navigationBar.prefersLargeTitles = true
         navigationController?.setNavigationBarHidden(false, animated: true)
         navigationController?.setNavigationBarAppearance()
-        setNavigationItems()
+        setNavigationLeftItems()
 
         // FIXME: iPAD PDF landscape mode iOS 16
         DispatchQueue.main.async {
             self.collectionView?.collectionViewLayout.invalidateLayout()
         }
+
+        setNavigationRightItems(enableMoreMenu: false)
     }
 
     override func viewWillDisappear(_ animated: Bool) {
@@ -261,8 +277,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
         // TIP
         self.tipView?.dismiss()
 
-        isEditMode = false
-        setNavigationItems()
+        toggleSelect(isOn: false)
     }
 
     func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
@@ -321,7 +336,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
               let error = userInfo["error"] as? NKError,
               error.errorCode != NCGlobal.shared.errorNotModified else { return }
 
-        setNavigationItems()
+        setNavigationLeftItems()
     }
 
     @objc func changeTheming() {
@@ -560,18 +575,22 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
                         cell.writeInfoDateSize(date: metadata.date, size: metadata.size)
                     } else {
                         cell.fileInfoLabel?.text = ""
+                        cell.fileSubinfoLabel?.text = ""
                     }
                 } else {
                     cell.fileProgressView?.isHidden = false
                     cell.fileProgressView?.progress = progressNumber.floatValue
                     cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCImageCache.images.buttonStop)
                     if status == NCGlobal.shared.metadataStatusDownloading {
-                        cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + " - ↓ " + self.utilityFileSystem.transformedSize(totalBytes)
+                        cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected)
+                        cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↓ " + self.utilityFileSystem.transformedSize(totalBytes)
                     } else if status == NCGlobal.shared.metadataStatusUploading {
                         if totalBytes > 0 {
-                            cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + " - ↑ " + self.utilityFileSystem.transformedSize(totalBytes)
+                            cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected)
+                            cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ " + self.utilityFileSystem.transformedSize(totalBytes)
                         } else {
-                            cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected) + " - ↑ …"
+                            cell.fileInfoLabel?.text = self.utilityFileSystem.transformedSize(totalBytesExpected)
+                            cell.fileSubinfoLabel?.text = self.infoLabelsSeparator + "↑ …"
                         }
                     }
                 }
@@ -591,9 +610,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
 
     // MARK: - Layout
 
-    func setNavigationItems() {
-
-        self.setNavigationRightItems()
+    func setNavigationLeftItems() {
         navigationItem.title = titleCurrentFolder
 
         guard layoutKey == NCGlobal.shared.layoutViewFiles else { return }
@@ -602,40 +619,53 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
 
         let activeAccount = NCManageDatabase.shared.getActiveAccount()
 
-        let image = utility.loadUserImage(
-            for: appDelegate.user,
-               displayName: activeAccount?.displayName,
-               userBaseUrl: appDelegate)
+        let image = utility.loadUserImage(for: appDelegate.user, displayName: activeAccount?.displayName, userBaseUrl: appDelegate)
 
-        let button = UIButton(type: .custom)
+        let button = AccountSwitcherButton(type: .custom)
         button.setImage(image, for: .normal)
-
+        button.setImage(image, for: .highlighted)
         button.semanticContentAttribute = .forceLeftToRight
         button.sizeToFit()
-        button.action(for: .touchUpInside) { _ in
 
-            let accounts = NCManageDatabase.shared.getAllAccountOrderAlias()
-            if !accounts.isEmpty, !NCBrandOptions.shared.disable_multiaccount, !NCBrandOptions.shared.disable_manage_account {
+        let accounts = NCManageDatabase.shared.getAllAccountOrderAlias()
 
-                if let vcAccountRequest = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest {
+        if !accounts.isEmpty, !NCBrandOptions.shared.disable_multiaccount, !NCBrandOptions.shared.disable_manage_account {
+            let accountActions: [UIAction] = accounts.map { account in
+                let image = utility.loadUserImage(for: account.user, displayName: account.displayName, userBaseUrl: account)
 
-                    vcAccountRequest.activeAccount = NCManageDatabase.shared.getActiveAccount()
-                    vcAccountRequest.accounts = accounts
-                    vcAccountRequest.enableTimerProgress = false
-                    vcAccountRequest.enableAddAccount = true
-                    vcAccountRequest.delegate = self
-                    vcAccountRequest.dismissDidEnterBackground = true
+                var name: String = ""
+                var url: String = ""
 
-                    let screenHeighMax = UIScreen.main.bounds.height - (UIScreen.main.bounds.height / 5)
-                    let numberCell = accounts.count + 1
-                    let height = min(CGFloat(numberCell * Int(vcAccountRequest.heightCell) + 45), screenHeighMax)
-
-                    let popup = NCPopupViewController(contentController: vcAccountRequest, popupWidth: 300, popupHeight: height)
+                if account.alias.isEmpty {
+                    name = account.displayName
+                    url = (URL(string: account.urlBase)?.host ?? "")
+                } else {
+                    name = account.alias
+                }
 
-                    self.present(popup, animated: true)
+                let action = UIAction(title: name, image: image, state: account.active ? .on : .off) { _ in
+                    if !account.active {
+                        self.appDelegate.changeAccount(account.account, userProfile: nil)
+                    }
                 }
 
-                // TIP
+                action.subtitle = url
+
+                return action
+            }
+
+            let addAccountAction = UIAction(title: NSLocalizedString("_add_account_", comment: ""), image: .init(systemName: "person.crop.circle.badge.plus")) { _ in
+                self.appDelegate.openLogin(viewController: self, selector: NCGlobal.shared.introLogin, openLoginWeb: false)
+            }
+
+            let addAccountSubmenu = UIMenu(title: "", options: .displayInline, children: [addAccountAction])
+
+            let menu = UIMenu(children: accountActions + [addAccountSubmenu])
+
+            button.menu = menu
+            button.showsMenuAsPrimaryAction = true
+
+            button.onMenuOpened = {
                 self.dismissTip()
             }
         }
@@ -889,7 +919,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
     }
 
     @objc func reloadDataSourceNetwork() {
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
+        DispatchQueue.main.async {
             self.isReloadDataSourceNetworkInProgress = true
             self.collectionView?.reloadData()
         }
@@ -1117,49 +1147,63 @@ extension NCCollectionViewCommon: UICollectionViewDelegate {
 extension NCCollectionViewCommon: UICollectionViewDataSource {
 
     func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
-        guard let metadata = dataSource.cellForItemAt(indexPath: indexPath) else { return }
+        guard let metadata = dataSource.cellForItemAt(indexPath: indexPath),
+              let cell = (cell as? NCCellProtocol) else { return }
+
+        cell.filePreviewImageView?.layer.borderWidth = 0
+
+        if metadata.isImage {
+            cell.filePreviewImageView?.contentMode = .scaleAspectFill
+        } else {
+            cell.filePreviewImageView?.contentMode = .scaleAspectFit
+        }
 
         // Thumbnail
         if !metadata.directory {
+            if metadata.hasPreviewBorder {
+                cell.filePreviewImageView?.layer.borderWidth = 0.2
+                cell.filePreviewImageView?.layer.borderColor = UIColor.systemGray3.cgColor
+            }
             if metadata.name == NCGlobal.shared.appName {
-                    if let image = utility.createFilePreviewImage(ocId: metadata.ocId, etag: metadata.etag, fileNameView: metadata.fileNameView, classFile: metadata.classFile, status: metadata.status, createPreviewMedia: !metadata.hasPreview) {
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = image
+                if let image = utility.createFilePreviewImage(ocId: metadata.ocId, etag: metadata.etag, fileNameView: metadata.fileNameView, classFile: metadata.classFile, status: metadata.status, createPreviewMedia: !metadata.hasPreview) {
+
+                    cell.filePreviewImageView?.image = image
                 } else {
                     if metadata.iconName.isEmpty {
-                        (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.file
+                        cell.filePreviewImageView?.image = NCImageCache.images.file
                     } else {
-                        (cell as? NCCellProtocol)?.filePreviewImageView?.image = UIImage(named: metadata.iconName)
+                        cell.filePreviewImageView?.image = UIImage(named: metadata.iconName)
                     }
                     if metadata.hasPreview && metadata.status == NCGlobal.shared.metadataStatusNormal && (!utilityFileSystem.fileProviderStoragePreviewIconExists(metadata.ocId, etag: metadata.etag)) {
                         for case let operation as NCCollectionViewDownloadThumbnail in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId { return }
-                        NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, cell: (cell as? NCCellProtocol), collectionView: collectionView))
+                        NCNetworking.shared.downloadThumbnailQueue.addOperation(NCCollectionViewDownloadThumbnail(metadata: metadata, cell: cell, collectionView: collectionView))
                     }
                 }
             } else {
                 // Unified search
                 switch metadata.iconName {
                 case let str where str.contains("contacts"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconContacts
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconContacts
                 case let str where str.contains("conversation"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconTalk
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconTalk
                 case let str where str.contains("calendar"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconCalendar
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconCalendar
                 case let str where str.contains("deck"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconDeck
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconDeck
                 case let str where str.contains("mail"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconMail
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconMail
                 case let str where str.contains("talk"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconTalk
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconTalk
                 case let str where str.contains("confirm"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconConfirm
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconConfirm
                 case let str where str.contains("pages"):
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.iconPages
+                    cell.filePreviewImageView?.image = NCImageCache.images.iconPages
                 default:
-                    (cell as? NCCellProtocol)?.filePreviewImageView?.image = NCImageCache.images.file
+                    cell.filePreviewImageView?.image = NCImageCache.images.file
                 }
 
                 if !metadata.iconUrl.isEmpty {
-                    if let ownerId = getAvatarFromIconUrl(metadata: metadata), let cell = cell as? NCCellProtocol {
+                    if let ownerId = getAvatarFromIconUrl(metadata: metadata) {
                         let fileName = metadata.userBaseUrl + "-" + ownerId + ".png"
                         NCNetworking.shared.downloadAvatar(user: ownerId, dispalyName: nil, fileName: fileName, cell: cell, view: collectionView, cellImageView: cell.filePreviewImageView)
                     }
@@ -1170,8 +1214,7 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
         // Avatar
         if !metadata.ownerId.isEmpty,
            metadata.ownerId != appDelegate.userId,
-           appDelegate.account == metadata.account,
-           let cell = cell as? NCCellProtocol {
+           appDelegate.account == metadata.account {
             let fileName = metadata.userBaseUrl + "-" + metadata.ownerId + ".png"
             NCNetworking.shared.downloadAvatar(user: metadata.ownerId, dispalyName: metadata.ownerDisplayName, fileName: fileName, cell: cell, view: collectionView, cellImageView: cell.fileAvatarImageView)
         }
@@ -1195,8 +1238,6 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
         let numberItems = dataSource.numberOfItemsInSection(section)
         emptyDataSet?.numberOfItemsInSection(numberItems, section: section)
 
-        setNavigationRightItems()
-
         return numberItems
     }
 
@@ -1258,7 +1299,9 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
             } else {
                 cell.fileInfoLabel?.text = metadata.subline
             }
+            cell.fileSubinfoLabel?.isHidden = true
         } else {
+            cell.fileSubinfoLabel?.isHidden = false
             cell.fileTitleLabel?.text = metadata.fileNameView
             cell.fileTitleLabel?.lineBreakMode = .byTruncatingMiddle
             cell.writeInfoDateSize(date: metadata.date, size: metadata.size)
@@ -1351,14 +1394,18 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
         // Write status on Label Info
         switch metadata.status {
         case NCGlobal.shared.metadataStatusWaitDownload:
-            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) + " - " + NSLocalizedString("_status_wait_download_", comment: "")
+            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size)
+            cell.fileSubinfoLabel?.text = infoLabelsSeparator + NSLocalizedString("_status_wait_download_", comment: "")
         case NCGlobal.shared.metadataStatusDownloading:
-            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) + " - ↓ …"
+            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size)
+            cell.fileSubinfoLabel?.text = infoLabelsSeparator + "↓ …"
         case NCGlobal.shared.metadataStatusWaitUpload:
-            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) + " - " + NSLocalizedString("_status_wait_upload_", comment: "")
+            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size)
+            cell.fileSubinfoLabel?.text = infoLabelsSeparator + NSLocalizedString("_status_wait_upload_", comment: "")
             cell.fileLocalImage?.image = nil
         case NCGlobal.shared.metadataStatusUploading:
-            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size) + " - ↑ …"
+            cell.fileInfoLabel?.text = utilityFileSystem.transformedSize(metadata.size)
+            cell.fileSubinfoLabel?.text = infoLabelsSeparator + "↑ …"
             cell.fileLocalImage?.image = nil
         case NCGlobal.shared.metadataStatusUploadError:
             if metadata.sessionError.isEmpty {
@@ -1407,9 +1454,13 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
         }
 
         // Accessibility
-        cell.setAccessibility(label: metadata.fileNameView + ", " + (cell.fileInfoLabel?.text ?? ""), value: a11yValues.joined(separator: ", "))
+        cell.setAccessibility(label: metadata.fileNameView + ", " + (cell.fileInfoLabel?.text ?? "") + (cell.fileSubinfoLabel?.text ?? ""), value: a11yValues.joined(separator: ", "))
 
         // Color string find in search
+
+        cell.fileTitleLabel?.textColor = .label
+        cell.fileTitleLabel?.font = .systemFont(ofSize: 15)
+
         if isSearchingMode, let literalSearch = self.literalSearch, let title = cell.fileTitleLabel?.text {
             let longestWordRange = (title.lowercased() as NSString).range(of: literalSearch)
             let attributedString = NSMutableAttributedString(string: title, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)])
@@ -1547,7 +1598,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegateFlowLayout {
 
         var headerRichWorkspace: CGFloat = 0
 
-        if let richWorkspaceText = richWorkspaceText, !headerRichWorkspaceDisable {
+        if let richWorkspaceText = richWorkspaceText, showDescription {
             let trimmed = richWorkspaceText.trimmingCharacters(in: .whitespaces)
             if !trimmed.isEmpty && !isSearchingMode {
                 headerRichWorkspace = UIScreen.main.bounds.size.height / 6
@@ -1610,7 +1661,7 @@ extension NCCollectionViewCommon: EasyTipViewDelegate {
 }
 
 extension NCCollectionViewCommon: NCSelectableNavigationView, NCCollectionViewCommonSelectTabBarDelegate {
-    func setNavigationRightItems() {
+    func setNavigationRightItems(enableMoreMenu: Bool = true) {
         var selectedMetadatas: [tableMetadata] = []
         var isAnyOffline = false
         var isAnyDirectory = false
@@ -1641,6 +1692,7 @@ extension NCCollectionViewCommon: NCSelectableNavigationView, NCCollectionViewCo
             }
 
             guard !isAnyOffline else { continue }
+
             if metadata.directory,
                let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", appDelegate.account, metadata.serverUrl + "/" + metadata.fileName)) {
                 isAnyOffline = directory.offline
@@ -1675,6 +1727,8 @@ extension NCCollectionViewCommon: NCSelectableNavigationView, NCCollectionViewCo
             let menu = UIMenu(children: createMenuActions())
             let menuButton = UIBarButtonItem(image: .init(systemName: "ellipsis.circle"), menu: menu)
 
+            menuButton.isEnabled = enableMoreMenu
+
             if layoutKey == NCGlobal.shared.layoutViewFiles {
                 navigationItem.rightBarButtonItems = [menuButton, notification]
             } else {
@@ -1866,12 +1920,22 @@ extension NCCollectionViewCommon: NCSelectableNavigationView, NCCollectionViewCo
             self.saveLayout(layoutForView)
         }
 
-        let foldersSubmenu = UIMenu(title: "", options: .displayInline, children: [foldersOnTop])
+        let showDescriptionKeychain = NCKeychain().showDescription
+
+        let showDescription = UIAction(title: NSLocalizedString("_show_description_", comment: ""), image: UIImage(systemName: "list.dash.header.rectangle"), attributes: richWorkspaceText == nil ? .disabled : [], state: showDescriptionKeychain && richWorkspaceText != nil ? .on : .off) { _ in
+            NCKeychain().showDescription = !showDescriptionKeychain
+            self.collectionView.reloadData()
+            self.setNavigationRightItems()
+        }
+
+        showDescription.subtitle = richWorkspaceText == nil ? NSLocalizedString("_no_description_available_", comment: "") : ""
+
+        let additionalSubmenu = UIMenu(title: "", options: .displayInline, children: [foldersOnTop, showDescription])
 
         if layoutKey == NCGlobal.shared.layoutViewRecent {
             return [select]
         } else {
-            return [select, viewStyleSubmenu, sortSubmenu, foldersSubmenu]
+            return [select, viewStyleSubmenu, sortSubmenu, additionalSubmenu]
         }
     }
 }
@@ -1918,7 +1982,7 @@ extension NCCollectionViewCommon {
             return
         }
 
-        // DOWNLOAD
+        // DOWNLOAD FOREGROUND
         if metadata.session == NextcloudKit.shared.nkCommonInstance.sessionIdentifierDownload {
             if let request = NCNetworking.shared.downloadRequest[fileNameLocalPath] {
                 request.cancel()
@@ -1937,6 +2001,18 @@ extension NCCollectionViewCommon {
             return
         }
 
+        // DOWNLOAD BACKGROUND
+        if metadata.session == NCNetworking.shared.sessionDownloadBackground {
+            let session: URLSession? = NCNetworking.shared.sessionManagerDownloadBackground
+            if let tasks = await session?.tasks {
+                for task in tasks.2 { // ([URLSessionDataTask], [URLSessionUploadTask], [URLSessionDownloadTask])
+                    if task.taskIdentifier == metadata.sessionTaskIdentifier {
+                        task.cancel()
+                    }
+                }
+            }
+        }
+
         // UPLOAD FOREGROUND
         if metadata.session == NextcloudKit.shared.nkCommonInstance.sessionIdentifierUpload {
             if let request = NCNetworking.shared.uploadRequest[fileNameLocalPath] {
@@ -2030,18 +2106,19 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation {
     }
 
     override func start() {
-
         guard !isCancelled else { return self.finish() }
 
         var etagResource: String?
+        let sizePreview = NCUtility().getSizePreview(width: metadata.width, height: metadata.height)
+
         if FileManager.default.fileExists(atPath: fileNameIconLocalPath) && FileManager.default.fileExists(atPath: fileNamePreviewLocalPath) {
             etagResource = metadata.etagResource
         }
 
         NextcloudKit.shared.downloadPreview(fileNamePathOrFileId: fileNamePath,
                                             fileNamePreviewLocalPath: fileNamePreviewLocalPath,
-                                            widthPreview: NCGlobal.shared.sizePreview,
-                                            heightPreview: NCGlobal.shared.sizePreview,
+                                            widthPreview: Int(sizePreview.width),
+                                            heightPreview: Int(sizePreview.height),
                                             fileNameIconLocalPath: fileNameIconLocalPath,
                                             sizeIcon: NCGlobal.shared.sizeIcon,
                                             etag: etagResource,
@@ -2051,6 +2128,10 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation {
                 NCManageDatabase.shared.setMetadataEtagResource(ocId: self.metadata.ocId, etagResource: etag)
                 DispatchQueue.main.async {
                     if self.metadata.ocId == self.cell?.fileObjectId, let filePreviewImageView = self.cell?.filePreviewImageView {
+                        if self.metadata.hasPreviewBorder {
+                            self.cell?.filePreviewImageView?.layer.borderWidth = 0.2
+                            self.cell?.filePreviewImageView?.layer.borderColor = UIColor.systemGray3.cgColor
+                        }
                         UIView.transition(with: filePreviewImageView,
                                           duration: 0.75,
                                           options: .transitionCrossDissolve,
@@ -2065,3 +2146,12 @@ class NCCollectionViewDownloadThumbnail: ConcurrentOperation {
         }
     }
 }
+
+private class AccountSwitcherButton: UIButton {
+    var onMenuOpened: (() -> Void)?
+
+    override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
+        super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)
+        onMenuOpened?()
+    }
+}

+ 6 - 11
iOSClient/Main/Collection Common/NCGridCell.swift

@@ -32,9 +32,9 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
     @IBOutlet weak var imageLocal: UIImageView!
     @IBOutlet weak var labelTitle: UILabel!
     @IBOutlet weak var labelInfo: UILabel!
+    @IBOutlet weak var labelSubinfo: UILabel!
     @IBOutlet weak var buttonMore: UIButton!
     @IBOutlet weak var imageVisualEffect: UIVisualEffectView!
-    @IBOutlet weak var progressView: UIProgressView!
 
     var objectId = ""
     var indexPath = IndexPath()
@@ -63,9 +63,9 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
         get { return labelInfo }
         set { labelInfo = newValue }
     }
-    var fileProgressView: UIProgressView? {
-        get { return progressView }
-        set { progressView = newValue }
+    var fileSubinfoLabel: UILabel? {
+        get { return labelSubinfo }
+        set { labelSubinfo = newValue }
     }
     var fileSelectImage: UIImageView? {
         get { return imageSelect }
@@ -100,10 +100,6 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
         imageVisualEffect.clipsToBounds = true
         imageVisualEffect.alpha = 0.5
 
-        progressView.tintColor = NCBrandColor.shared.brand
-        progressView.transform = CGAffineTransform(scaleX: 1.0, y: 0.5)
-        progressView.trackTintColor = .clear
-
         let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gestureRecognizer:)))
         longPressedGesture.minimumPressDuration = 0.5
         longPressedGesture.delegate = self
@@ -118,8 +114,6 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
 
         labelTitle.text = ""
         labelInfo.text = ""
-        labelTitle.textColor = .label
-        labelInfo.textColor = .systemGray
     }
 
     override func prepareForReuse() {
@@ -209,7 +203,8 @@ class NCGridCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
         dateFormatter.timeStyle = .none
         dateFormatter.locale = Locale.current
 
-        labelInfo.text = dateFormatter.string(from: date as Date) + " · " + NCUtilityFileSystem().transformedSize(size)
+        labelInfo.text = dateFormatter.string(from: date as Date)
+        labelSubinfo.text = NCUtilityFileSystem().transformedSize(size)
     }
 
     func setAccessibility(label: String, value: String) {

+ 79 - 63
iOSClient/Main/Collection Common/NCGridCell.xib

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
     <device id="retina4_7" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -12,110 +12,123 @@
         <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
         <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
         <collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="gridCell" id="vf1-Kf-9uL" customClass="NCGridCell" customModule="Nextcloud" customModuleProvider="target">
-            <rect key="frame" x="0.0" y="0.0" width="416" height="494"/>
+            <rect key="frame" x="0.0" y="0.0" width="416" height="529"/>
             <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
             <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
-                <rect key="frame" x="0.0" y="0.0" width="416" height="494"/>
+                <rect key="frame" x="0.0" y="0.0" width="416" height="529"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
-                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5Ci-V1-hf5" userLabel="imageItem">
-                        <rect key="frame" x="0.0" y="0.0" width="416" height="434"/>
+                    <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5Ci-V1-hf5" userLabel="imageItem">
+                        <rect key="frame" x="0.0" y="20" width="416" height="419"/>
                     </imageView>
-                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eU3-lY-fKr" userLabel="labelTitle">
-                        <rect key="frame" x="5" y="444" width="406" height="13.5"/>
-                        <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="11"/>
-                        <nil key="textColor"/>
+                    <stackView opaque="NO" contentMode="scaleToFill" alignment="top" translatesAutoresizingMaskIntoConstraints="NO" id="VRH-IZ-lXO">
+                        <rect key="frame" x="5" y="447" width="406" height="36"/>
+                        <subviews>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="254" horizontalCompressionResistancePriority="756" verticalCompressionResistancePriority="759" text="Label" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eU3-lY-fKr" userLabel="labelTitle">
+                                <rect key="frame" x="0.0" y="0.0" width="406" height="18"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                        </subviews>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="36" id="Soj-7j-hoM"/>
+                        </constraints>
+                    </stackView>
+                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2po-8g-XeS">
+                        <rect key="frame" x="25" y="490" width="366" height="12"/>
+                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                        <color key="textColor" systemColor="systemGrayColor"/>
                         <nil key="highlightedColor"/>
                     </label>
-                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2po-8g-XeS">
-                        <rect key="frame" x="5" y="464.5" width="386" height="12"/>
+                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="12P-pO-DHO" userLabel="Label Size">
+                        <rect key="frame" x="25" y="505" width="366" height="12"/>
                         <fontDescription key="fontDescription" type="system" pointSize="10"/>
-                        <color key="textColor" systemColor="systemGray2Color"/>
+                        <color key="textColor" systemColor="systemGrayColor"/>
                         <nil key="highlightedColor"/>
                     </label>
-                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EJs-Ro-nbe" userLabel="buttonMoreGrid">
-                        <rect key="frame" x="391" y="458" width="25" height="25"/>
+                    <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="AYs-f2-vve" userLabel="imageFavorite">
+                        <rect key="frame" x="391" y="25" width="20" height="20"/>
                         <constraints>
-                            <constraint firstAttribute="height" constant="25" id="4Ba-Uy-pX2"/>
-                            <constraint firstAttribute="width" constant="25" id="aRK-GA-Nba"/>
-                        </constraints>
-                        <state key="normal" image="more"/>
-                        <connections>
-                            <action selector="touchUpInsideMore:" destination="vf1-Kf-9uL" eventType="touchUpInside" id="GDx-NN-gE9"/>
-                        </connections>
-                    </button>
-                    <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="AYs-f2-vve" userLabel="imageFavorite">
-                        <rect key="frame" x="396" y="5" width="15" height="15"/>
-                        <constraints>
-                            <constraint firstAttribute="height" constant="15" id="ZjS-Hv-JNm"/>
-                            <constraint firstAttribute="width" constant="15" id="kDr-15-VeJ"/>
+                            <constraint firstAttribute="height" constant="20" id="ZjS-Hv-JNm"/>
+                            <constraint firstAttribute="width" constant="20" id="kDr-15-VeJ"/>
                         </constraints>
                     </imageView>
                     <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="a0p-rj-jnV" userLabel="imageStatus">
-                        <rect key="frame" x="5" y="414" width="15" height="15"/>
+                        <rect key="frame" x="5" y="414" width="20" height="20"/>
                         <constraints>
-                            <constraint firstAttribute="height" constant="15" id="gq1-0a-eLC"/>
-                            <constraint firstAttribute="width" constant="15" id="uJE-4b-Qt7"/>
+                            <constraint firstAttribute="height" constant="20" id="gq1-0a-eLC"/>
+                            <constraint firstAttribute="width" constant="20" id="uJE-4b-Qt7"/>
                         </constraints>
                     </imageView>
                     <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="81G-wH-fjN" userLabel="imageLocal">
-                        <rect key="frame" x="396" y="414" width="15" height="15"/>
+                        <rect key="frame" x="391" y="414" width="20" height="20"/>
                         <constraints>
-                            <constraint firstAttribute="height" constant="15" id="NTa-Gi-uzY"/>
-                            <constraint firstAttribute="width" constant="15" id="xLe-lb-N1p"/>
+                            <constraint firstAttribute="height" constant="20" id="NTa-Gi-uzY"/>
+                            <constraint firstAttribute="width" constant="20" id="xLe-lb-N1p"/>
                         </constraints>
                     </imageView>
                     <visualEffectView hidden="YES" opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="W0L-HY-al1">
-                        <rect key="frame" x="0.0" y="0.0" width="416" height="434"/>
+                        <rect key="frame" x="0.0" y="20" width="416" height="419"/>
                         <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="0m6-A2-SwD">
-                            <rect key="frame" x="0.0" y="0.0" width="416" height="434"/>
+                            <rect key="frame" x="0.0" y="0.0" width="416" height="419"/>
                             <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         </view>
                         <blurEffect style="extraLight"/>
                     </visualEffectView>
                     <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DHy-Up-3Bh" userLabel="imageSelect">
-                        <rect key="frame" x="5" y="5" width="25" height="25"/>
+                        <rect key="frame" x="5" y="25" width="25" height="25"/>
                         <constraints>
                             <constraint firstAttribute="height" constant="25" id="SoZ-J3-98x"/>
                             <constraint firstAttribute="width" constant="25" id="cZG-gx-gwt"/>
                         </constraints>
                     </imageView>
-                    <progressView hidden="YES" opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JQo-Vc-Ejk">
-                        <rect key="frame" x="5" y="485" width="406" height="4"/>
-                    </progressView>
+                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EJs-Ro-nbe" userLabel="buttonMoreGrid">
+                        <rect key="frame" x="396" y="492" width="20" height="20"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="20" id="4Ba-Uy-pX2"/>
+                            <constraint firstAttribute="width" constant="20" id="aRK-GA-Nba"/>
+                        </constraints>
+                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                        <color key="tintColor" systemColor="systemGray2Color"/>
+                        <state key="normal" image="ellipsis" catalog="system"/>
+                        <connections>
+                            <action selector="touchUpInsideMore:" destination="vf1-Kf-9uL" eventType="touchUpInside" id="GDx-NN-gE9"/>
+                        </connections>
+                    </button>
                 </subviews>
             </view>
             <viewLayoutGuide key="safeArea" id="VXh-sQ-LeX"/>
             <constraints>
                 <constraint firstItem="DHy-Up-3Bh" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="1T3-8p-uIW"/>
-                <constraint firstItem="AYs-f2-vve" firstAttribute="leading" secondItem="5Ci-V1-hf5" secondAttribute="trailing" constant="-20" id="3e3-0A-NSl"/>
-                <constraint firstItem="eU3-lY-fKr" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="10" id="4Yq-Nh-z1l"/>
-                <constraint firstItem="2po-8g-XeS" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="5dp-1s-MQi"/>
-                <constraint firstItem="2po-8g-XeS" firstAttribute="top" secondItem="eU3-lY-fKr" secondAttribute="bottom" constant="7" id="5wo-Td-XeT"/>
+                <constraint firstItem="AYs-f2-vve" firstAttribute="leading" secondItem="5Ci-V1-hf5" secondAttribute="trailing" constant="-25" id="3e3-0A-NSl"/>
                 <constraint firstItem="W0L-HY-al1" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" id="6tC-PK-fYX"/>
-                <constraint firstItem="EJs-Ro-nbe" firstAttribute="centerY" secondItem="2po-8g-XeS" secondAttribute="centerY" id="8qW-SF-u1h"/>
-                <constraint firstItem="EJs-Ro-nbe" firstAttribute="leading" secondItem="2po-8g-XeS" secondAttribute="trailing" id="ABr-PB-TZg"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="JQo-Vc-Ejk" secondAttribute="trailing" constant="5" id="E03-Dk-iZ5"/>
+                <constraint firstItem="2po-8g-XeS" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" constant="25" id="9JI-KS-d7J"/>
                 <constraint firstItem="DHy-Up-3Bh" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" constant="5" id="ESV-qE-tbO"/>
+                <constraint firstAttribute="trailing" secondItem="VRH-IZ-lXO" secondAttribute="trailing" constant="5" id="HAS-uF-AuS"/>
+                <constraint firstItem="2po-8g-XeS" firstAttribute="top" secondItem="VRH-IZ-lXO" secondAttribute="bottom" constant="7" id="OPl-xq-XOx"/>
                 <constraint firstItem="5Ci-V1-hf5" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" id="Ouj-ZD-UFm"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="EJs-Ro-nbe" secondAttribute="trailing" id="Pfe-J0-t9I"/>
                 <constraint firstItem="W0L-HY-al1" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" id="Py6-0z-K3t"/>
-                <constraint firstItem="5Ci-V1-hf5" firstAttribute="leading" secondItem="a0p-rj-jnV" secondAttribute="trailing" constant="-20" id="UtQ-6D-cYc"/>
+                <constraint firstItem="2po-8g-XeS" firstAttribute="centerX" secondItem="vf1-Kf-9uL" secondAttribute="centerX" id="TTW-HT-yEO"/>
+                <constraint firstItem="5Ci-V1-hf5" firstAttribute="leading" secondItem="a0p-rj-jnV" secondAttribute="trailing" constant="-25" id="UtQ-6D-cYc"/>
                 <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="W0L-HY-al1" secondAttribute="trailing" id="VMW-0Y-aOH"/>
-                <constraint firstItem="81G-wH-fjN" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="-20" id="aEb-vq-8sk"/>
+                <constraint firstItem="81G-wH-fjN" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="-25" id="aEb-vq-8sk"/>
                 <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="5Ci-V1-hf5" secondAttribute="trailing" id="cHT-cP-NN6"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="eU3-lY-fKr" secondAttribute="trailing" constant="5" id="csl-Ny-rdF"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="60" id="eEC-eB-alE"/>
-                <constraint firstItem="eU3-lY-fKr" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="gZe-FC-8XQ"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="W0L-HY-al1" secondAttribute="bottom" constant="60" id="jI9-M1-Nl8"/>
-                <constraint firstItem="81G-wH-fjN" firstAttribute="leading" secondItem="5Ci-V1-hf5" secondAttribute="trailing" constant="-20" id="nFH-Pc-end"/>
+                <constraint firstItem="VRH-IZ-lXO" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="8" id="cj5-Ez-3cy"/>
+                <constraint firstItem="VRH-IZ-lXO" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" constant="5" id="dTF-dl-Awr"/>
+                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="90" id="eEC-eB-alE"/>
+                <constraint firstItem="12P-pO-DHO" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" constant="25" id="fTQ-Ye-Xj2"/>
+                <constraint firstItem="12P-pO-DHO" firstAttribute="top" secondItem="2po-8g-XeS" secondAttribute="bottom" constant="3" id="hbc-KL-fRj"/>
+                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="W0L-HY-al1" secondAttribute="bottom" constant="90" id="jI9-M1-Nl8"/>
+                <constraint firstItem="EJs-Ro-nbe" firstAttribute="trailing" secondItem="5Ci-V1-hf5" secondAttribute="trailing" id="l6Z-DK-OZi"/>
+                <constraint firstItem="81G-wH-fjN" firstAttribute="leading" secondItem="5Ci-V1-hf5" secondAttribute="trailing" constant="-25" id="nFH-Pc-end"/>
+                <constraint firstItem="EJs-Ro-nbe" firstAttribute="top" secondItem="VRH-IZ-lXO" secondAttribute="bottom" constant="9" id="o5n-Oi-Uh7"/>
                 <constraint firstItem="5Ci-V1-hf5" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" id="qT3-WD-iTV"/>
-                <constraint firstItem="5Ci-V1-hf5" firstAttribute="top" secondItem="AYs-f2-vve" secondAttribute="bottom" constant="-20" id="rLL-6g-ypv"/>
-                <constraint firstItem="a0p-rj-jnV" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="-20" id="upV-Ov-WWd"/>
-                <constraint firstItem="JQo-Vc-Ejk" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="wiV-1m-wt8"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="JQo-Vc-Ejk" secondAttribute="bottom" constant="5" id="zV9-iQ-Zm5"/>
+                <constraint firstItem="5Ci-V1-hf5" firstAttribute="top" secondItem="AYs-f2-vve" secondAttribute="bottom" constant="-25" id="rLL-6g-ypv"/>
+                <constraint firstItem="a0p-rj-jnV" firstAttribute="top" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="-25" id="upV-Ov-WWd"/>
+                <constraint firstItem="12P-pO-DHO" firstAttribute="centerX" secondItem="vf1-Kf-9uL" secondAttribute="centerX" id="xhm-Np-2Ua"/>
             </constraints>
-            <size key="customSize" width="416" height="489"/>
+            <size key="customSize" width="416" height="524"/>
             <connections>
                 <outlet property="buttonMore" destination="EJs-Ro-nbe" id="BdI-ay-LuX"/>
                 <outlet property="imageFavorite" destination="AYs-f2-vve" id="UeH-R7-bZr"/>
@@ -125,16 +138,19 @@
                 <outlet property="imageStatus" destination="a0p-rj-jnV" id="6Dg-tf-evd"/>
                 <outlet property="imageVisualEffect" destination="W0L-HY-al1" id="WDW-2d-Npa"/>
                 <outlet property="labelInfo" destination="2po-8g-XeS" id="FJ4-wI-9cW"/>
+                <outlet property="labelSubinfo" destination="12P-pO-DHO" id="K6d-7r-FGh"/>
                 <outlet property="labelTitle" destination="eU3-lY-fKr" id="0P7-yM-Asb"/>
-                <outlet property="progressView" destination="JQo-Vc-Ejk" id="cdf-7W-tao"/>
             </connections>
-            <point key="canvasLocation" x="244.80000000000001" y="244.6776611694153"/>
+            <point key="canvasLocation" x="233.59999999999999" y="242.42878560719643"/>
         </collectionViewCell>
     </objects>
     <resources>
-        <image name="more" width="425" height="425"/>
+        <image name="ellipsis" catalog="system" width="128" height="37"/>
         <systemColor name="systemGray2Color">
             <color red="0.68235294117647061" green="0.68235294117647061" blue="0.69803921568627447" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
         </systemColor>
+        <systemColor name="systemGrayColor">
+            <color red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
     </resources>
 </document>

+ 14 - 4
iOSClient/Main/Collection Common/NCListCell.swift

@@ -32,6 +32,7 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
     @IBOutlet weak var imageLocal: UIImageView!
     @IBOutlet weak var labelTitle: UILabel!
     @IBOutlet weak var labelInfo: UILabel!
+    @IBOutlet weak var labelSubinfo: UILabel!
     @IBOutlet weak var imageShared: UIImageView!
     @IBOutlet weak var buttonShared: UIButton!
     @IBOutlet weak var imageMore: UIImageView!
@@ -44,7 +45,7 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
     @IBOutlet weak var imageItemLeftConstraint: NSLayoutConstraint!
     @IBOutlet weak var separatorHeightConstraint: NSLayoutConstraint!
     @IBOutlet weak var titleTrailingConstraint: NSLayoutConstraint!
-    @IBOutlet weak var infoTrailingConstraint: NSLayoutConstraint!
+    @IBOutlet weak var subInfoTrailingConstraint: NSLayoutConstraint!
 
     private var objectId = ""
     private var user = ""
@@ -76,6 +77,10 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
         get { return labelInfo }
         set { labelInfo = newValue }
     }
+    var fileSubinfoLabel: UILabel? {
+        get { return labelSubinfo }
+        set { labelSubinfo = newValue }
+    }
     var fileProgressView: UIProgressView? {
         get { return progressView }
         set { progressView = newValue }
@@ -144,6 +149,7 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
         labelInfo.text = ""
         labelTitle.textColor = .label
         labelInfo.textColor = .systemGray
+        labelSubinfo.textColor = .systemGray
     }
 
     override func prepareForReuse() {
@@ -190,12 +196,12 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
 
     func titleInfoTrailingFull() {
         titleTrailingConstraint.constant = 10
-        infoTrailingConstraint.constant = 10
+        subInfoTrailingConstraint.constant = 10
     }
 
     func titleInfoTrailingDefault() {
         titleTrailingConstraint.constant = 90
-        infoTrailingConstraint.constant = 90
+        subInfoTrailingConstraint.constant = 90
     }
 
     func setButtonMore(named: String, image: UIImage) {
@@ -271,7 +277,8 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
     }
 
     func writeInfoDateSize(date: NSDate, size: Int64) {
-        labelInfo.text = NCUtility().dateDiff(date as Date) + " · " + NCUtilityFileSystem().transformedSize(size)
+        labelInfo.text = NCUtility().dateDiff(date as Date)
+        labelSubinfo.text = " · " + NCUtilityFileSystem().transformedSize(size)
     }
 
     func setAccessibility(label: String, value: String) {
@@ -284,10 +291,13 @@ class NCListCell: UICollectionViewCell, UIGestureRecognizerDelegate, NCCellProto
             tag0.isHidden = true
             tag1.isHidden = true
             labelInfo.isHidden = false
+            labelSubinfo.isHidden = false
         } else {
             tag0.isHidden = false
             tag1.isHidden = true
             labelInfo.isHidden = true
+            labelSubinfo.isHidden = true
+
             if let tag = tags.first {
                 tag0.text = tag
                 if tags.count > 1 {

+ 19 - 10
iOSClient/Main/Collection Common/NCListCell.xib

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
     <device id="retina6_0" orientation="landscape" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -60,7 +60,7 @@
                         <nil key="highlightedColor"/>
                     </label>
                     <label opaque="NO" userInteractionEnabled="NO" tag="102" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AXX-71-9Q6" userLabel="labelInfo">
-                        <rect key="frame" x="107" y="120" width="425" height="15"/>
+                        <rect key="frame" x="107" y="120" width="31" height="15"/>
                         <fontDescription key="fontDescription" type="system" pointSize="12"/>
                         <color key="textColor" red="0.59999999999999998" green="0.59999999999999998" blue="0.59999999999999998" alpha="1" colorSpace="calibratedRGB"/>
                         <nil key="highlightedColor"/>
@@ -112,7 +112,7 @@
                         </constraints>
                     </view>
                     <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="tag0" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qnc-hI-Z9r" customClass="PaddedAndBorderedLabel" customModule="Nextcloud" customModuleProvider="target">
-                        <rect key="frame" x="107" y="122.66666666666667" width="36" height="16.333333333333329"/>
+                        <rect key="frame" x="107" y="124.66666666666667" width="26" height="14.333333333333329"/>
                         <fontDescription key="fontDescription" type="system" pointSize="12"/>
                         <color key="textColor" systemColor="systemGrayColor"/>
                         <nil key="highlightedColor"/>
@@ -141,7 +141,7 @@
                         </userDefinedRuntimeAttributes>
                     </label>
                     <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="tag1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jUe-8q-VJd" customClass="PaddedAndBorderedLabel" customModule="Nextcloud" customModuleProvider="target">
-                        <rect key="frame" x="148" y="122.66666666666667" width="34" height="16.333333333333329"/>
+                        <rect key="frame" x="138" y="124.66666666666667" width="24" height="14.333333333333329"/>
                         <fontDescription key="fontDescription" type="system" pointSize="12"/>
                         <color key="textColor" systemColor="systemGrayColor"/>
                         <nil key="highlightedColor"/>
@@ -169,12 +169,19 @@
                             </userDefinedRuntimeAttribute>
                         </userDefinedRuntimeAttributes>
                     </label>
+                    <label opaque="NO" userInteractionEnabled="NO" tag="102" contentMode="left" verticalHuggingPriority="251" text="Label" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fu0-vg-6GU" userLabel="labelSubinfo">
+                        <rect key="frame" x="138" y="120" width="394" height="15"/>
+                        <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                        <color key="textColor" red="0.59999999999999998" green="0.59999999999999998" blue="0.59999999999999998" alpha="1" colorSpace="calibratedRGB"/>
+                        <nil key="highlightedColor"/>
+                    </label>
                 </subviews>
             </view>
             <viewLayoutGuide key="safeArea" id="Gu8-oz-zWa"/>
             <constraints>
                 <constraint firstItem="jUe-8q-VJd" firstAttribute="centerY" secondItem="qnc-hI-Z9r" secondAttribute="centerY" id="2Z4-Yh-1lR"/>
                 <constraint firstAttribute="trailing" secondItem="m2p-oJ-j15" secondAttribute="trailing" constant="90" id="2zI-li-v77"/>
+                <constraint firstAttribute="bottom" secondItem="Fu0-vg-6GU" secondAttribute="bottom" constant="13" id="4sQ-Uf-ovC"/>
                 <constraint firstItem="H4E-G2-C1H" firstAttribute="leading" secondItem="w2m-Vw-hpd" secondAttribute="trailing" constant="-10" id="6fN-Jc-WID"/>
                 <constraint firstAttribute="bottom" secondItem="Egg-cb-EhZ" secondAttribute="bottom" id="81D-sw-EaX"/>
                 <constraint firstItem="AXX-71-9Q6" firstAttribute="leading" secondItem="w2m-Vw-hpd" secondAttribute="trailing" constant="10" id="Bxx-kv-KT3"/>
@@ -191,6 +198,7 @@
                 <constraint firstItem="dgL-g5-Nkc" firstAttribute="centerX" secondItem="yhy-xd-w5C" secondAttribute="centerX" id="VSJ-7R-Srk"/>
                 <constraint firstAttribute="bottom" secondItem="qnc-hI-Z9r" secondAttribute="bottom" constant="9" id="XTs-Qg-kiX"/>
                 <constraint firstItem="7Q9-Tv-9yo" firstAttribute="top" secondItem="w2m-Vw-hpd" secondAttribute="bottom" constant="-10" id="XbB-4a-WpA"/>
+                <constraint firstAttribute="trailing" secondItem="Fu0-vg-6GU" secondAttribute="trailing" constant="137" id="Yv8-Ir-wv3"/>
                 <constraint firstItem="yhy-xd-w5C" firstAttribute="centerY" secondItem="jxV-Pk-fPt" secondAttribute="centerY" id="ZO7-Ny-L3I"/>
                 <constraint firstItem="m2p-oJ-j15" firstAttribute="leading" secondItem="w2m-Vw-hpd" secondAttribute="trailing" constant="10" id="Zyr-qM-9qP"/>
                 <constraint firstAttribute="bottom" secondItem="AXX-71-9Q6" secondAttribute="bottom" constant="13" id="d06-sn-I3Y"/>
@@ -201,7 +209,7 @@
                 <constraint firstItem="w2m-Vw-hpd" firstAttribute="leading" secondItem="jxV-Pk-fPt" secondAttribute="leading" constant="57" id="mBb-ff-7HD"/>
                 <constraint firstItem="w2m-Vw-hpd" firstAttribute="leading" secondItem="7Q9-Tv-9yo" secondAttribute="trailing" constant="-10" id="mon-aq-gcP"/>
                 <constraint firstItem="UtT-L6-mgW" firstAttribute="top" secondItem="jxV-Pk-fPt" secondAttribute="top" constant="13" id="nrY-2F-QZ2"/>
-                <constraint firstAttribute="trailing" secondItem="AXX-71-9Q6" secondAttribute="trailing" constant="137" id="p0M-zU-aDG"/>
+                <constraint firstItem="Fu0-vg-6GU" firstAttribute="leading" secondItem="AXX-71-9Q6" secondAttribute="trailing" id="oEf-wb-lZr"/>
                 <constraint firstItem="w2m-Vw-hpd" firstAttribute="centerY" secondItem="jxV-Pk-fPt" secondAttribute="centerY" id="qKl-4Y-m5t"/>
                 <constraint firstAttribute="trailing" secondItem="yhy-xd-w5C" secondAttribute="trailing" id="s2S-RP-cw5"/>
                 <constraint firstItem="AyA-hP-r6w" firstAttribute="centerY" secondItem="jxV-Pk-fPt" secondAttribute="centerY" id="sJp-0x-bdC"/>
@@ -220,25 +228,26 @@
                 <outlet property="imageSelect" destination="AyA-hP-r6w" id="c1t-yz-HBg"/>
                 <outlet property="imageShared" destination="jc6-Vg-TaS" id="6CL-wO-WaN"/>
                 <outlet property="imageStatus" destination="7Q9-Tv-9yo" id="Qug-Q7-rRZ"/>
-                <outlet property="infoTrailingConstraint" destination="p0M-zU-aDG" id="BJv-hA-VCb"/>
                 <outlet property="labelInfo" destination="AXX-71-9Q6" id="krb-tZ-UQ7"/>
+                <outlet property="labelSubinfo" destination="Fu0-vg-6GU" id="YVU-88-a4q"/>
                 <outlet property="labelTitle" destination="UtT-L6-mgW" id="Xv6-zM-2v1"/>
                 <outlet property="progressView" destination="m2p-oJ-j15" id="yFv-KS-nEy"/>
                 <outlet property="separator" destination="Egg-cb-EhZ" id="uhq-Nc-z8K"/>
                 <outlet property="separatorHeightConstraint" destination="G5S-67-boG" id="B6g-qe-MTb"/>
+                <outlet property="subInfoTrailingConstraint" destination="Yv8-Ir-wv3" id="jkR-EH-b2e"/>
                 <outlet property="tag0" destination="qnc-hI-Z9r" id="6jJ-lV-0ck"/>
                 <outlet property="tag1" destination="jUe-8q-VJd" id="Wcm-rS-rEd"/>
                 <outlet property="titleTrailingConstraint" destination="Tq4-bB-YMV" id="v4n-j5-ZWT"/>
             </connections>
-            <point key="canvasLocation" x="128.18590704647679" y="198.40000000000001"/>
+            <point key="canvasLocation" x="127.60663507109004" y="196.92307692307691"/>
         </collectionViewCell>
     </objects>
     <designables>
         <designable name="jUe-8q-VJd">
-            <size key="intrinsicContentSize" width="34" height="16.333333333333336"/>
+            <size key="intrinsicContentSize" width="24" height="14.333333333333334"/>
         </designable>
         <designable name="qnc-hI-Z9r">
-            <size key="intrinsicContentSize" width="36" height="16.333333333333336"/>
+            <size key="intrinsicContentSize" width="26" height="14.333333333333334"/>
         </designable>
     </designables>
     <resources>

+ 11 - 11
iOSClient/Main/Collection Common/NCSelectableNavigationView.swift

@@ -54,33 +54,33 @@ protocol NCSelectableNavigationView: AnyObject {
     var tabBarSelect: NCSelectableViewTabBar? { get set }
 
     func reloadDataSource(withQueryDB: Bool)
-    func setNavigationItems()
-    func setNavigationRightItems()
+    func setNavigationLeftItems()
+    func setNavigationRightItems(enableMoreMenu: Bool)
     func createMenuActions() -> [UIMenuElement]
 
-    func toggleSelect()
+    func toggleSelect(isOn: Bool?)
     func onListSelected()
     func onGridSelected()
 }
 
 extension NCSelectableNavigationView {
-    func setNavigationItems() {
-        setNavigationRightItems()
-    }
+    func setNavigationLeftItems() {}
 
     func saveLayout(_ layoutForView: NCDBLayoutForView) {
         NCManageDatabase.shared.setLayoutForView(layoutForView: layoutForView)
         NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource)
 
-        setNavigationRightItems()
+        setNavigationRightItems(enableMoreMenu: true)
     }
 
-    func toggleSelect() {
+    /// If explicit `isOn` is not set, it will invert `isEditMode`
+    func toggleSelect(isOn: Bool? = nil) {
         DispatchQueue.main.async {
-            self.isEditMode = !self.isEditMode
+            self.isEditMode = isOn ?? !self.isEditMode
             self.selectOcId.removeAll()
             self.selectIndexPath.removeAll()
-            self.setNavigationItems()
+            self.setNavigationLeftItems()
+            self.setNavigationRightItems(enableMoreMenu: true)
             self.collectionView.reloadData()
         }
     }
@@ -88,7 +88,7 @@ extension NCSelectableNavigationView {
     func collectionViewSelectAll() {
         selectOcId = selectableDataSource.compactMap({ $0.primaryKeyValue })
         collectionView.reloadData()
-        self.setNavigationRightItems()
+        setNavigationRightItems(enableMoreMenu: true)
     }
 
     func tapNotification() {

+ 5 - 0
iOSClient/Main/NCCellProtocol.swift

@@ -31,6 +31,7 @@ protocol NCCellProtocol {
     var fileUser: String? { get set }
     var fileTitleLabel: UILabel? { get set }
     var fileInfoLabel: UILabel? { get set }
+    var fileSubinfoLabel: UILabel? { get set }
     var fileProgressView: UIProgressView? { get set }
     var fileSelectImage: UIImageView? { get set }
     var fileStatusImage: UIImageView? { get set }
@@ -74,6 +75,10 @@ extension NCCellProtocol {
         get { return nil }
         set { }
     }
+    var fileSubinfoLabel: UILabel? {
+        get { return nil }
+        set { }
+    }
     var fileProgressView: UIProgressView? {
         get { return nil }
         set {}

+ 3 - 3
iOSClient/Main/Section Header Footer/NCSectionFooter.xib

@@ -33,7 +33,7 @@
                     <rect key="frame" x="177.66666666666666" y="5" width="20" height="20"/>
                 </activityIndicatorView>
                 <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="s2m-yO-4x0" userLabel="separator">
-                    <rect key="frame" x="60" y="30" width="265" height="1"/>
+                    <rect key="frame" x="50" y="30" width="275" height="1"/>
                     <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                     <color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                     <constraints>
@@ -52,7 +52,7 @@
                 <constraint firstItem="qWG-SR-Qly" firstAttribute="centerX" secondItem="EFn-SN-cxu" secondAttribute="centerX" id="18M-RP-YIn"/>
                 <constraint firstItem="EFn-SN-cxu" firstAttribute="trailing" secondItem="TK1-KX-Qe0" secondAttribute="trailing" constant="10" id="PoY-CD-99O"/>
                 <constraint firstAttribute="trailing" secondItem="gzy-cT-Gjn" secondAttribute="trailing" constant="10" id="QzY-ac-CRO"/>
-                <constraint firstItem="EFn-SN-cxu" firstAttribute="leading" secondItem="s2m-yO-4x0" secondAttribute="leading" constant="-10" id="ai4-Qy-YWi"/>
+                <constraint firstItem="EFn-SN-cxu" firstAttribute="leading" secondItem="s2m-yO-4x0" secondAttribute="leading" id="ai4-Qy-YWi"/>
                 <constraint firstItem="gzy-cT-Gjn" firstAttribute="centerY" secondItem="Vin-9E-7nW" secondAttribute="centerY" constant="-30" id="avP-sX-JB5">
                     <variation key="heightClass=compact-widthClass=regular" constant="-15"/>
                     <variation key="heightClass=regular-widthClass=compact" constant="-15"/>
@@ -77,7 +77,7 @@
     </objects>
     <resources>
         <systemColor name="linkColor">
-            <color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+            <color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
         </systemColor>
     </resources>
 </document>

+ 2 - 2
iOSClient/Main/Section Header Footer/NCSectionHeaderMenu.swift

@@ -55,8 +55,8 @@ class NCSectionHeaderMenu: UICollectionReusableView, UIGestureRecognizerDelegate
         backgroundColor = .clear
 
         // Gradient
-        gradient.startPoint = CGPoint(x: 0, y: 0.50)
-        gradient.endPoint = CGPoint(x: 0, y: 1)
+        gradient.startPoint = CGPoint(x: 0, y: 0.8)
+        gradient.endPoint = CGPoint(x: 0, y: 0.9)
         viewRichWorkspace.layer.addSublayer(gradient)
 
         let tap = UITapGestureRecognizer(target: self, action: #selector(touchUpInsideViewRichWorkspace(_:)))

+ 3 - 3
iOSClient/Main/Section Header Footer/NCSectionHeaderMenu.xib

@@ -19,7 +19,7 @@
                     <rect key="frame" x="0.0" y="318" width="574" height="50"/>
                     <subviews>
                         <textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="pYo-pF-MGv">
-                            <rect key="frame" x="5" y="0.0" width="564" height="50"/>
+                            <rect key="frame" x="12" y="0.0" width="550" height="50"/>
                             <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                             <color key="textColor" systemColor="labelColor"/>
                             <fontDescription key="fontDescription" type="system" pointSize="14"/>
@@ -29,8 +29,8 @@
                     <constraints>
                         <constraint firstItem="pYo-pF-MGv" firstAttribute="top" secondItem="NC1-5C-E5z" secondAttribute="top" id="PgU-fC-vEG"/>
                         <constraint firstAttribute="height" constant="50" id="eT3-4m-mJ6"/>
-                        <constraint firstAttribute="trailing" secondItem="pYo-pF-MGv" secondAttribute="trailing" constant="5" id="nSk-Jr-ufp"/>
-                        <constraint firstItem="pYo-pF-MGv" firstAttribute="leading" secondItem="NC1-5C-E5z" secondAttribute="leading" constant="5" id="qoB-Sw-ipc"/>
+                        <constraint firstAttribute="trailing" secondItem="pYo-pF-MGv" secondAttribute="trailing" constant="12" id="nSk-Jr-ufp"/>
+                        <constraint firstItem="pYo-pF-MGv" firstAttribute="leading" secondItem="NC1-5C-E5z" secondAttribute="leading" constant="12" id="qoB-Sw-ipc"/>
                         <constraint firstAttribute="bottom" secondItem="pYo-pF-MGv" secondAttribute="bottom" id="t4r-dA-VyW"/>
                     </constraints>
                 </view>

+ 19 - 13
iOSClient/Media/Cell/NCGridMediaCell.swift

@@ -29,26 +29,38 @@ class NCGridMediaCell: UICollectionViewCell, NCCellProtocol {
     @IBOutlet weak var imageVisualEffect: UIVisualEffectView!
     @IBOutlet weak var imageSelect: UIImageView!
     @IBOutlet weak var imageStatus: UIImageView!
+    @IBOutlet weak var label: UILabel!
 
     private var objectId: String = ""
     private var user: String = ""
     var indexPath = IndexPath()
-
-    var date: Date?
+    private var date: Date?
 
     var filePreviewImageView: UIImageView? {
         get { return imageItem }
         set {}
     }
+
     var fileObjectId: String? {
         get { return objectId }
         set { objectId = newValue ?? "" }
     }
+
     var fileUser: String? {
         get { return user }
         set { user = newValue ?? "" }
     }
 
+    var fileDate: Date? {
+        get { return date }
+        set {
+            date = newValue
+            if let date {
+                label.text = NCUtility().getTitleFromDate(date)
+            }
+        }
+    }
+
     override func awakeFromNib() {
         super.awakeFromNib()
         initCell()
@@ -63,22 +75,16 @@ class NCGridMediaCell: UICollectionViewCell, NCCellProtocol {
         imageItem.backgroundColor = .secondarySystemBackground
         imageStatus.image = nil
         imageItem.image = nil
-    }
-
-    func selectMode(_ status: Bool) {
-        if status {
-            imageSelect.isHidden = false
-        } else {
-            imageSelect.isHidden = true
-            imageVisualEffect.isHidden = true
-        }
+        imageVisualEffect.alpha = 0.4
+        imageSelect.image = NCImageCache.images.checkedYes
+        imageVisualEffect.isHidden = true
+        imageSelect.isHidden = true
     }
 
     func selected(_ status: Bool) {
         if status {
-            imageSelect.image = NCImageCache.images.checkedYes
+            imageSelect.isHidden = false
             imageVisualEffect.isHidden = false
-            imageVisualEffect.alpha = 0.4
         } else {
             imageSelect.isHidden = true
             imageVisualEffect.isHidden = true

+ 40 - 27
iOSClient/Media/Cell/NCGridMediaCell.xib

@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
-    <device id="retina4_7" orientation="portrait" appearance="light"/>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+    <device id="retina5_5" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
-        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <objects>
@@ -17,13 +16,17 @@
                 <rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
                 <autoresizingMask key="autoresizingMask"/>
                 <subviews>
-                    <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5Ci-V1-hf5" userLabel="imageItem">
-                        <rect key="frame" x="-1" y="-1" width="222" height="222"/>
+                    <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5Ci-V1-hf5" userLabel="imageItem">
+                        <rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
                     </imageView>
                     <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="a0p-rj-jnV" userLabel="imageStatus">
-                        <rect key="frame" x="5" y="192" width="23" height="23"/>
+                        <rect key="frame" x="5" y="205" width="10" height="10"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="10" id="iE4-ba-cXj"/>
+                            <constraint firstAttribute="width" constant="10" id="uNx-Cr-iBO"/>
+                        </constraints>
                     </imageView>
-                    <visualEffectView hidden="YES" opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="r1K-4X-gNd" userLabel="VisualEffect">
+                    <visualEffectView hidden="YES" contentMode="scaleAspectFill" translatesAutoresizingMaskIntoConstraints="NO" id="r1K-4X-gNd" userLabel="VisualEffect">
                         <rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
                         <view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="3h4-qt-b9E">
                             <rect key="frame" x="0.0" y="0.0" width="220" height="220"/>
@@ -33,28 +36,37 @@
                         <blurEffect style="extraLight"/>
                     </visualEffectView>
                     <imageView userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="DHy-Up-3Bh" userLabel="imageSelect">
-                        <rect key="frame" x="5" y="5" width="44.5" height="44.5"/>
+                        <rect key="frame" x="5" y="5" width="20" height="20"/>
+                        <constraints>
+                            <constraint firstAttribute="height" constant="20" id="Lkm-Tv-DDQ"/>
+                            <constraint firstAttribute="width" constant="20" id="PqO-qT-gfs"/>
+                        </constraints>
                     </imageView>
+                    <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QAA-9f-xaN">
+                        <rect key="frame" x="0.0" y="203" width="220" height="17"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                        <nil key="textColor"/>
+                        <nil key="highlightedColor"/>
+                    </label>
                 </subviews>
             </view>
-            <viewLayoutGuide key="safeArea" id="VXh-sQ-LeX"/>
             <constraints>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="r1K-4X-gNd" secondAttribute="trailing" id="1Hu-GT-dJv"/>
-                <constraint firstItem="DHy-Up-3Bh" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="1T3-8p-uIW"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="a0p-rj-jnV" secondAttribute="bottom" constant="5" id="2IN-4o-XSp"/>
-                <constraint firstItem="r1K-4X-gNd" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" id="3bv-Dh-iih"/>
-                <constraint firstItem="a0p-rj-jnV" firstAttribute="height" secondItem="5Ci-V1-hf5" secondAttribute="height" multiplier="0.1" constant="1" id="4IJ-uh-zvr"/>
-                <constraint firstItem="DHy-Up-3Bh" firstAttribute="height" secondItem="5Ci-V1-hf5" secondAttribute="height" multiplier="0.2" id="7FN-4V-ZAz"/>
-                <constraint firstItem="a0p-rj-jnV" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="5" id="DYA-5M-RZ8"/>
-                <constraint firstItem="a0p-rj-jnV" firstAttribute="width" secondItem="5Ci-V1-hf5" secondAttribute="width" multiplier="0.1" constant="1" id="DvH-0a-ncn"/>
-                <constraint firstItem="DHy-Up-3Bh" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" constant="5" id="ESV-qE-tbO"/>
-                <constraint firstItem="5Ci-V1-hf5" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" constant="-1" id="Ouj-ZD-UFm"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="r1K-4X-gNd" secondAttribute="bottom" id="QAj-Am-H9V"/>
-                <constraint firstItem="r1K-4X-gNd" firstAttribute="top" secondItem="VXh-sQ-LeX" secondAttribute="top" id="Rou-vT-GPt"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="trailing" secondItem="5Ci-V1-hf5" secondAttribute="trailing" constant="-1" id="cHT-cP-NN6"/>
-                <constraint firstItem="VXh-sQ-LeX" firstAttribute="bottom" secondItem="5Ci-V1-hf5" secondAttribute="bottom" constant="-1" id="eEC-eB-alE"/>
-                <constraint firstItem="DHy-Up-3Bh" firstAttribute="width" secondItem="5Ci-V1-hf5" secondAttribute="width" multiplier="0.2" id="ojv-2d-Xmj"/>
-                <constraint firstItem="5Ci-V1-hf5" firstAttribute="leading" secondItem="VXh-sQ-LeX" secondAttribute="leading" constant="-1" id="qT3-WD-iTV"/>
+                <constraint firstAttribute="trailing" secondItem="r1K-4X-gNd" secondAttribute="trailing" id="1Hu-GT-dJv"/>
+                <constraint firstItem="DHy-Up-3Bh" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" constant="5" id="1T3-8p-uIW"/>
+                <constraint firstItem="QAA-9f-xaN" firstAttribute="trailing" secondItem="vf1-Kf-9uL" secondAttribute="trailing" id="1dV-Sb-7U8"/>
+                <constraint firstAttribute="bottom" secondItem="a0p-rj-jnV" secondAttribute="bottom" constant="5" id="2IN-4o-XSp"/>
+                <constraint firstItem="r1K-4X-gNd" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" id="3bv-Dh-iih"/>
+                <constraint firstItem="QAA-9f-xaN" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" id="BlV-Di-gR1"/>
+                <constraint firstItem="a0p-rj-jnV" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" constant="5" id="DYA-5M-RZ8"/>
+                <constraint firstItem="DHy-Up-3Bh" firstAttribute="top" secondItem="vf1-Kf-9uL" secondAttribute="top" constant="5" id="ESV-qE-tbO"/>
+                <constraint firstItem="5Ci-V1-hf5" firstAttribute="top" secondItem="vf1-Kf-9uL" secondAttribute="top" id="Ouj-ZD-UFm"/>
+                <constraint firstAttribute="bottom" secondItem="r1K-4X-gNd" secondAttribute="bottom" id="QAj-Am-H9V"/>
+                <constraint firstItem="r1K-4X-gNd" firstAttribute="top" secondItem="vf1-Kf-9uL" secondAttribute="top" id="Rou-vT-GPt"/>
+                <constraint firstAttribute="trailing" secondItem="5Ci-V1-hf5" secondAttribute="trailing" id="cHT-cP-NN6"/>
+                <constraint firstAttribute="bottom" secondItem="5Ci-V1-hf5" secondAttribute="bottom" id="eEC-eB-alE"/>
+                <constraint firstItem="QAA-9f-xaN" firstAttribute="bottom" secondItem="vf1-Kf-9uL" secondAttribute="bottom" id="mcM-m0-XST"/>
+                <constraint firstItem="5Ci-V1-hf5" firstAttribute="leading" secondItem="vf1-Kf-9uL" secondAttribute="leading" id="qT3-WD-iTV"/>
             </constraints>
             <size key="customSize" width="220" height="260"/>
             <connections>
@@ -62,8 +74,9 @@
                 <outlet property="imageSelect" destination="DHy-Up-3Bh" id="mo9-rP-P4I"/>
                 <outlet property="imageStatus" destination="a0p-rj-jnV" id="6Dg-tf-evd"/>
                 <outlet property="imageVisualEffect" destination="r1K-4X-gNd" id="uf3-P1-F4o"/>
+                <outlet property="label" destination="QAA-9f-xaN" id="PZV-b1-tgG"/>
             </connections>
-            <point key="canvasLocation" x="88" y="141.67916041979012"/>
+            <point key="canvasLocation" x="86.956521739130437" y="141.03260869565219"/>
         </collectionViewCell>
     </objects>
 </document>

+ 216 - 0
iOSClient/Media/NCMedia+Command.swift

@@ -0,0 +1,216 @@
+//
+//  NCMedia+Command.swift
+//  Nextcloud
+//
+//  Created by Marino Faggiana on 24/02/24.
+//  Copyright © 2024 Marino Faggiana. All rights reserved.
+//
+
+import Foundation
+import NextcloudKit
+
+extension NCMedia {
+    @IBAction func selectOrCancelButtonPressed(_ sender: UIButton) {
+        isEditMode = !isEditMode
+        setSelectcancelButton()
+    }
+
+    func setSelectcancelButton() {
+        selectOcId.removeAll()
+        tabBarSelect?.selectCount = selectOcId.count
+        if let visibleCells = self.collectionView?.indexPathsForVisibleItems.compactMap({ self.collectionView?.cellForItem(at: $0) }) {
+            for case let cell as NCGridMediaCell in visibleCells {
+                cell.selected(false)
+            }
+        }
+        if isEditMode {
+            activityIndicatorTrailing.constant = 150
+            selectOrCancelButton.setTitle( NSLocalizedString("_cancel_", comment: ""), for: .normal)
+            selectOrCancelButtonTrailing.constant = 10
+            selectOrCancelButton.isHidden = false
+            menuButton.isHidden = true
+            tabBarSelect?.show()
+        } else {
+            activityIndicatorTrailing.constant = 150
+            selectOrCancelButton.setTitle( NSLocalizedString("_select_", comment: ""), for: .normal)
+            selectOrCancelButtonTrailing.constant = 50
+            selectOrCancelButton.isHidden = false
+            menuButton.isHidden = false
+            tabBarSelect?.hide()
+        }
+    }
+
+    func setTitleDate(_ offset: CGFloat = 10) {
+        titleDate?.text = ""
+        if let metadata = metadatas?.first {
+            let contentOffsetY = collectionView.contentOffset.y
+            let top = insetsTop + view.safeAreaInsets.top + offset
+            if insetsTop + view.safeAreaInsets.top + contentOffsetY < 10 {
+                titleDate?.text = utility.getTitleFromDate(metadata.date as Date)
+                return
+            }
+            let point = CGPoint(x: offset, y: top + contentOffsetY)
+            if let indexPath = collectionView.indexPathForItem(at: point) {
+                let cell = self.collectionView(collectionView, cellForItemAt: indexPath) as? NCGridMediaCell
+                if let date = cell?.fileDate {
+                    self.titleDate?.text = utility.getTitleFromDate(date)
+                }
+            } else {
+                if offset < 20 {
+                    self.setTitleDate(20)
+                }
+            }
+        }
+    }
+
+    func setColor() {
+        if isTop {
+            titleDate?.textColor = .label
+            activityIndicator.color = .label
+            selectOrCancelButton.setTitleColor(.label, for: .normal)
+            menuButton.setImage(UIImage(systemName: "ellipsis")?.withTintColor(.label, renderingMode: .alwaysOriginal), for: .normal)
+            gradientView.isHidden = true
+        } else {
+            titleDate?.textColor = .white
+            activityIndicator.color = .white
+            selectOrCancelButton.setTitleColor(.white, for: .normal)
+            menuButton.setImage(UIImage(systemName: "ellipsis")?.withTintColor(.white, renderingMode: .alwaysOriginal), for: .normal)
+            gradientView.isHidden = false
+        }
+    }
+
+    func createMenu() {
+        var columnCount = NCKeychain().mediaColumnCount
+        let layout = NCKeychain().mediaTypeLayout
+        let layoutTitle = (layout == NCGlobal.shared.mediaLayoutRatio) ? NSLocalizedString("_media_square_", comment: "") : NSLocalizedString("_media_ratio_", comment: "")
+        let layoutImage = (layout == NCGlobal.shared.mediaLayoutRatio) ? UIImage(systemName: "square.grid.3x3") : UIImage(systemName: "rectangle.grid.3x2")
+
+        if UIDevice.current.userInterfaceIdiom == .phone,
+           (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) {
+            columnCount += 2
+        }
+
+        if CGFloat(columnCount) >= maxImageGrid - 1 {
+            self.attributesZoomIn = []
+            self.attributesZoomOut = .disabled
+        } else if columnCount <= 1 {
+            self.attributesZoomIn = .disabled
+            self.attributesZoomOut = []
+        } else {
+            self.attributesZoomIn = []
+            self.attributesZoomOut = []
+        }
+
+        let viewFilterMenu = UIMenu(title: "", options: .displayInline, children: [
+            UIAction(title: NSLocalizedString("_media_viewimage_show_", comment: ""), image: UIImage(systemName: "photo")) { _ in
+                self.showOnlyImages = true
+                self.showOnlyVideos = false
+                self.reloadDataSource()
+            },
+            UIAction(title: NSLocalizedString("_media_viewvideo_show_", comment: ""), image: UIImage(systemName: "video")) { _ in
+                self.showOnlyImages = false
+                self.showOnlyVideos = true
+                self.reloadDataSource()
+            },
+            UIAction(title: NSLocalizedString("_media_show_all_", comment: ""), image: UIImage(systemName: "photo.on.rectangle")) { _ in
+                self.showOnlyImages = false
+                self.showOnlyVideos = false
+                self.reloadDataSource()
+            }
+        ])
+        let viewLayoutMenu = UIAction(title: layoutTitle, image: layoutImage) { _ in
+            if layout == NCGlobal.shared.mediaLayoutRatio {
+                NCKeychain().mediaTypeLayout = NCGlobal.shared.mediaLayoutSquare
+            } else {
+                NCKeychain().mediaTypeLayout = NCGlobal.shared.mediaLayoutRatio
+            }
+            self.createMenu()
+            self.collectionViewReloadData()
+        }
+
+        let zoomViewMediaFolder = UIMenu(title: "", options: .displayInline, children: [
+            UIMenu(title: NSLocalizedString("_zoom_", comment: ""), children: [
+                UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: UIImage(systemName: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in
+                    UIView.animate(withDuration: 0.0, animations: {
+                        NCKeychain().mediaColumnCount = columnCount + 1
+                        self.createMenu()
+                        self.collectionViewReloadData()
+                    })
+                },
+                UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: UIImage(systemName: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in
+                    UIView.animate(withDuration: 0.0, animations: {
+                        NCKeychain().mediaColumnCount = columnCount - 1
+                        self.createMenu()
+                        self.collectionViewReloadData()
+                    })
+                }
+            ]),
+            UIMenu(title: NSLocalizedString("_media_view_options_", comment: ""), children: [viewFilterMenu, viewLayoutMenu]),
+            UIAction(title: NSLocalizedString("_select_media_folder_", comment: ""), image: UIImage(systemName: "folder"), handler: { _ in
+                guard let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController,
+                      let viewController = navigationController.topViewController as? NCSelect else { return }
+                viewController.delegate = self
+                viewController.typeOfCommandView = .select
+                viewController.type = "mediaFolder"
+                self.present(navigationController, animated: true)
+            })
+        ])
+
+        let playFile = UIAction(title: NSLocalizedString("_play_from_files_", comment: ""), image: UIImage(systemName: "play.circle")) { _ in
+            guard let tabBarController = self.appDelegate.window?.rootViewController as? UITabBarController else { return }
+            self.documentPickerViewController = NCDocumentPickerViewController(tabBarController: tabBarController, isViewerMedia: true, allowsMultipleSelection: false, viewController: self)
+        }
+
+        let playURL = UIAction(title: NSLocalizedString("_play_from_url_", comment: ""), image: UIImage(systemName: "link")) { _ in
+            let alert = UIAlertController(title: NSLocalizedString("_valid_video_url_", comment: ""), message: nil, preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: nil))
+            alert.addTextField(configurationHandler: { textField in
+                textField.placeholder = "http://myserver.com/movie.mkv"
+            })
+            alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in
+                guard let stringUrl = alert.textFields?.first?.text, !stringUrl.isEmpty, let url = URL(string: stringUrl) else { return }
+                let fileName = url.lastPathComponent
+                let metadata = NCManageDatabase.shared.createMetadata(account: self.activeAccount.account, user: self.activeAccount.user, userId: self.activeAccount.userId, fileName: fileName, fileNameView: fileName, ocId: NSUUID().uuidString, serverUrl: "", urlBase: self.activeAccount.urlBase, url: stringUrl, contentType: "")
+                NCManageDatabase.shared.addMetadata(metadata)
+                NCViewer().view(viewController: self, metadata: metadata, metadatas: [metadata], imageIcon: nil)
+            }))
+            self.present(alert, animated: true)
+        }
+
+        menuButton.menu = UIMenu(title: "", children: [zoomViewMediaFolder, playFile, playURL])
+    }
+}
+
+extension NCMedia: NCMediaSelectTabBarDelegate {
+    func delete() {
+        let selectOcId = self.selectOcId.map { $0 }
+        if !selectOcId.isEmpty {
+            let alertController = UIAlertController(
+                title: NSLocalizedString("_delete_selected_photos_", comment: ""),
+                message: "",
+                preferredStyle: .alert)
+            alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default) { (_: UIAlertAction) in
+
+                Task {
+                    var error = NKError()
+                    var ocIds: [String] = []
+                    for ocId in selectOcId where error == .success {
+                        if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) {
+                            error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: false)
+                            if error == .success {
+                                ocIds.append(metadata.ocId)
+                            }
+                        }
+                    }
+                    NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocIds, "onlyLocalCache": false, "error": error])
+                }
+
+                self.isEditMode = false
+                self.setSelectcancelButton()
+            })
+            alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .default) { (_: UIAlertAction) in })
+
+            present(alertController, animated: true, completion: { })
+        }
+    }
+}

+ 65 - 4
iOSClient/Media/NCMedia.storyboard

@@ -17,7 +17,7 @@
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <subviews>
                             <collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="Zaz-Cl-qpZ">
-                                <rect key="frame" x="0.0" y="0.0" width="375" height="862"/>
+                                <rect key="frame" x="0.0" y="0.0" width="375" height="812"/>
                                 <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                 <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="fF1-wd-0xN">
                                     <size key="itemSize" width="0.0" height="0.0"/>
@@ -31,18 +31,79 @@
                                     <outlet property="delegate" destination="EFX-fO-Oip" id="s3n-CL-8X2"/>
                                 </connections>
                             </collectionView>
+                            <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7rV-YL-aM7">
+                                <rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
+                                <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="150" id="uAz-q2-42a"/>
+                                </constraints>
+                            </view>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rSH-l2-T1a">
+                                <rect key="frame" x="10" y="60" width="235" height="40"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="40" id="mcF-qd-xsP"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="9bK-ms-LxX">
+                                <rect key="frame" x="255" y="70" width="20" height="20"/>
+                                <color key="color" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                            </activityIndicatorView>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Enx-va-Bud">
+                                <rect key="frame" x="235" y="65" width="90" height="30"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" constant="90" id="N4t-Eb-vDt"/>
+                                    <constraint firstAttribute="height" constant="30" id="YfM-AZ-ws4"/>
+                                </constraints>
+                                <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                <state key="normal" title="Title"/>
+                                <connections>
+                                    <action selector="selectOrCancelButtonPressed:" destination="EFX-fO-Oip" eventType="touchUpInside" id="6DJ-3I-rxi"/>
+                                </connections>
+                            </button>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" adjustsImageWhenHighlighted="NO" adjustsImageWhenDisabled="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0dF-cq-2wr">
+                                <rect key="frame" x="335" y="65" width="30" height="30"/>
+                                <constraints>
+                                    <constraint firstAttribute="width" constant="30" id="6sk-2U-uUH"/>
+                                    <constraint firstAttribute="height" constant="30" id="SCp-D6-Vad"/>
+                                </constraints>
+                                <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                <inset key="imageEdgeInsets" minX="4" minY="4" maxX="4" maxY="4"/>
+                            </button>
                         </subviews>
                         <viewLayoutGuide key="safeArea" id="Meh-VD-wWh"/>
                         <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                         <constraints>
-                            <constraint firstItem="Zaz-Cl-qpZ" firstAttribute="leading" secondItem="Meh-VD-wWh" secondAttribute="leading" id="1bp-sm-u0X"/>
-                            <constraint firstItem="Meh-VD-wWh" firstAttribute="trailing" secondItem="Zaz-Cl-qpZ" secondAttribute="trailing" id="aNd-UL-hmu"/>
-                            <constraint firstItem="Meh-VD-wWh" firstAttribute="bottom" secondItem="Zaz-Cl-qpZ" secondAttribute="bottom" constant="-84" id="aNr-tf-2AH"/>
+                            <constraint firstItem="Zaz-Cl-qpZ" firstAttribute="leading" secondItem="QEs-gO-Cmp" secondAttribute="leading" id="1bp-sm-u0X"/>
+                            <constraint firstAttribute="trailing" secondItem="7rV-YL-aM7" secondAttribute="trailing" id="28S-fu-Qxj"/>
+                            <constraint firstItem="7rV-YL-aM7" firstAttribute="top" secondItem="QEs-gO-Cmp" secondAttribute="top" id="4hj-bC-66m"/>
+                            <constraint firstItem="9bK-ms-LxX" firstAttribute="leading" secondItem="rSH-l2-T1a" secondAttribute="trailing" constant="10" id="Tyh-tn-Ien"/>
+                            <constraint firstAttribute="trailing" secondItem="0dF-cq-2wr" secondAttribute="trailing" constant="10" id="a0d-uf-kZw"/>
+                            <constraint firstAttribute="trailing" secondItem="Zaz-Cl-qpZ" secondAttribute="trailing" id="aNd-UL-hmu"/>
+                            <constraint firstAttribute="bottom" secondItem="Zaz-Cl-qpZ" secondAttribute="bottom" id="aNr-tf-2AH"/>
+                            <constraint firstItem="9bK-ms-LxX" firstAttribute="centerY" secondItem="rSH-l2-T1a" secondAttribute="centerY" id="bAR-d1-xDL"/>
+                            <constraint firstItem="rSH-l2-T1a" firstAttribute="top" secondItem="Meh-VD-wWh" secondAttribute="top" constant="10" id="esh-9N-C49"/>
+                            <constraint firstAttribute="trailing" secondItem="Enx-va-Bud" secondAttribute="trailing" constant="50" id="exV-eQ-FmL"/>
+                            <constraint firstItem="7rV-YL-aM7" firstAttribute="leading" secondItem="QEs-gO-Cmp" secondAttribute="leading" id="gXT-fZ-dAC"/>
+                            <constraint firstItem="Enx-va-Bud" firstAttribute="centerY" secondItem="rSH-l2-T1a" secondAttribute="centerY" id="kAD-Y8-RFL"/>
                             <constraint firstItem="Zaz-Cl-qpZ" firstAttribute="top" secondItem="QEs-gO-Cmp" secondAttribute="top" id="nIB-3t-o2I"/>
+                            <constraint firstItem="0dF-cq-2wr" firstAttribute="centerY" secondItem="rSH-l2-T1a" secondAttribute="centerY" id="tEU-Gq-Tb8"/>
+                            <constraint firstAttribute="trailing" secondItem="9bK-ms-LxX" secondAttribute="trailing" constant="100" id="xM8-MC-pfg"/>
+                            <constraint firstAttribute="leading" secondItem="rSH-l2-T1a" secondAttribute="leading" constant="-10" id="xg2-fe-KR9"/>
                         </constraints>
                     </view>
                     <connections>
+                        <outlet property="activityIndicator" destination="9bK-ms-LxX" id="dpp-13-6UO"/>
+                        <outlet property="activityIndicatorTrailing" destination="xM8-MC-pfg" id="htW-4Z-2Uz"/>
                         <outlet property="collectionView" destination="Zaz-Cl-qpZ" id="8oA-Gx-z7T"/>
+                        <outlet property="gradientView" destination="7rV-YL-aM7" id="2QI-sZ-TeA"/>
+                        <outlet property="menuButton" destination="0dF-cq-2wr" id="AoT-Kf-eTR"/>
+                        <outlet property="selectOrCancelButton" destination="Enx-va-Bud" id="18I-LW-ruL"/>
+                        <outlet property="selectOrCancelButtonTrailing" destination="exV-eQ-FmL" id="zrA-Xq-fWL"/>
+                        <outlet property="titleDate" destination="rSH-l2-T1a" id="sRl-Sr-fph"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="JJ0-Le-6eT" userLabel="First Responder" sceneMemberID="firstResponder"/>

+ 222 - 139
iOSClient/Media/NCMedia.swift

@@ -28,42 +28,42 @@ import RealmSwift
 class NCMedia: UIViewController, NCEmptyDataSetDelegate {
 
     @IBOutlet weak var collectionView: UICollectionView!
-
+    @IBOutlet weak var titleDate: UILabel!
+    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
+    @IBOutlet weak var activityIndicatorTrailing: NSLayoutConstraint!
+    @IBOutlet weak var selectOrCancelButton: UIButton!
+    @IBOutlet weak var selectOrCancelButtonTrailing: NSLayoutConstraint!
+    @IBOutlet weak var menuButton: UIButton!
+    @IBOutlet weak var gradientView: UIView!
+
+    var activeAccount = tableAccount()
     var emptyDataSet: NCEmptyDataSet?
-    var mediaCommandView: NCMediaCommandView?
-    var layout: NCMediaGridLayout!
     var documentPickerViewController: NCDocumentPickerViewController?
     var tabBarSelect: NCMediaSelectTabBar?
-
     let appDelegate = (UIApplication.shared.delegate as? AppDelegate)!
     let utilityFileSystem = NCUtilityFileSystem()
     let utility = NCUtility()
-
+    let imageCache = NCImageCache.shared
     var metadatas: ThreadSafeArray<tableMetadata>?
+    let refreshControl = UIRefreshControl()
+    var loadingTask: Task<Void, any Error>?
+    var isTop: Bool = true
     var isEditMode = false
     var selectOcId: [String] = []
-
+    var attributesZoomIn: UIMenuElement.Attributes = []
+    var attributesZoomOut: UIMenuElement.Attributes = []
+    let gradient: CAGradientLayer = CAGradientLayer()
     var showOnlyImages = false
     var showOnlyVideos = false
-
-    let maxImageGrid: CGFloat = 7
-    var cellHeigth: CGFloat = 0
-
-    var loadingTask: Task<Void, any Error>?
-
     var lastContentOffsetY: CGFloat = 0
-    var mediaPath = ""
-
     var timeIntervalSearchNewMedia: TimeInterval = 2.0
     var timerSearchNewMedia: Timer?
-
     let insetsTop: CGFloat = 75
-
-    struct cacheImages {
-        static var cellLivePhotoImage = UIImage()
-        static var cellPlayImage = UIImage()
-        static var cellImage = UIImage()
-    }
+    let maxImageGrid: CGFloat = 7
+    var livePhotoImage = UIImage()
+    var playImage = UIImage()
+    var photoImage = UIImage()
+    var videoImage = UIImage()
 
     // MARK: - View Life Cycle
 
@@ -72,80 +72,122 @@ class NCMedia: UIViewController, NCEmptyDataSetDelegate {
 
         view.backgroundColor = .systemBackground
 
-        layout = NCMediaGridLayout()
-        layout.itemForLine = CGFloat(NCKeychain().mediaItemForLine)
-        layout.sectionHeadersPinToVisibleBounds = true
-
         collectionView.register(UINib(nibName: "NCGridMediaCell", bundle: nil), forCellWithReuseIdentifier: "gridCell")
         collectionView.alwaysBounceVertical = true
         collectionView.contentInset = UIEdgeInsets(top: insetsTop, left: 0, bottom: 50, right: 0)
         collectionView.backgroundColor = .systemBackground
         collectionView.prefetchDataSource = self
+
+        let layout = NCMediaLayout()
+        layout.sectionInset = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2)
+        layout.mediaViewController = self
         collectionView.collectionViewLayout = layout
 
         emptyDataSet = NCEmptyDataSet(view: collectionView, offset: 0, delegate: self)
 
-        mediaCommandView = Bundle.main.loadNibNamed("NCMediaCommandView", owner: self, options: nil)?.first as? NCMediaCommandView
-        self.view.addSubview(mediaCommandView!)
-        mediaCommandView?.mediaView = self
-        mediaCommandView?.tabBarController = tabBarController
-        mediaCommandView?.translatesAutoresizingMaskIntoConstraints = false
-        mediaCommandView?.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
-        mediaCommandView?.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
-        mediaCommandView?.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
-        mediaCommandView?.heightAnchor.constraint(equalToConstant: 150).isActive = true
+        tabBarSelect = NCMediaSelectTabBar(tabBarController: self.tabBarController, delegate: self)
+
+        livePhotoImage = utility.loadImage(named: "livephoto", color: .white)
+        playImage = utility.loadImage(named: "play.fill", color: .white)
+
+        titleDate.text = ""
+
+        selectOrCancelButton.backgroundColor = .clear
+        selectOrCancelButton.layer.cornerRadius = 15
+        selectOrCancelButton.layer.masksToBounds = true
+        selectOrCancelButton.setTitle( NSLocalizedString("_select_", comment: ""), for: .normal)
+        selectOrCancelButton.addBlur(style: .systemThinMaterial)
+
+        menuButton.backgroundColor = .clear
+        menuButton.layer.cornerRadius = 15
+        menuButton.layer.masksToBounds = true
+        menuButton.showsMenuAsPrimaryAction = true
+        menuButton.configuration = UIButton.Configuration.plain()
+        menuButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
+        menuButton.changesSelectionAsPrimaryAction = false
+        menuButton.addBlur(style: .systemThinMaterial)
 
-        tabBarSelect = NCMediaSelectTabBar(tabBarController: self.tabBarController, delegate: mediaCommandView)
+        gradient.startPoint = CGPoint(x: 0, y: 0.1)
+        gradient.endPoint = CGPoint(x: 0, y: 1)
+        gradient.colors = [UIColor.black.withAlphaComponent(UIAccessibility.isReduceTransparencyEnabled ? 0.8 : 0.4).cgColor, UIColor.clear.cgColor]
+        gradientView.layer.insertSublayer(gradient, at: 0)
 
-        cacheImages.cellLivePhotoImage = utility.loadImage(named: "livephoto", color: .white)
-        cacheImages.cellPlayImage = utility.loadImage(named: "play.fill", color: .white)
+        activeAccount = NCManageDatabase.shared.getActiveAccount() ?? tableAccount()
+
+        collectionView.refreshControl = refreshControl
+        refreshControl.action(for: .valueChanged) { _ in
+            DispatchQueue.global().async {
+                self.reloadDataSource()
+            }
+            self.refreshControl.endRefreshing()
+        }
+
+        NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeUser), object: nil, queue: nil) { _ in
+            self.activeAccount = NCManageDatabase.shared.getActiveAccount() ?? tableAccount()
+            if let metadatas = self.metadatas,
+               let metadata = metadatas.first {
+                if metadata.account != self.activeAccount.account {
+                    self.metadatas = nil
+                    self.collectionViewReloadData()
+                }
+            }
+        }
 
-        if let activeAccount = NCManageDatabase.shared.getActiveAccount() { self.mediaPath = activeAccount.mediaPath }
+        NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterCreateMediaCacheEnded), object: nil, queue: nil) { _ in
+            if let metadatas = self.imageCache.initialMetadatas() {
+                self.metadatas = metadatas
+            }
+            self.collectionViewReloadData()
+        }
     }
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
 
         appDelegate.activeViewController = self
-
         navigationController?.setMediaAppreance()
 
         NotificationCenter.default.addObserver(self, selector: #selector(deleteFile(_:)), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(enterForeground(_:)), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationWillEnterForeground), object: nil)
 
-        timerSearchNewMedia?.invalidate()
-        timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(searchMediaUI), userInfo: nil, repeats: false)
-
-        if let metadatas = NCImageCache.shared.initialMetadatas() {
-            self.metadatas = metadatas
-        }
-
-        collectionView.reloadData()
+        startTimer()
     }
 
     override func viewDidAppear(_ animated: Bool) {
         super.viewDidAppear(animated)
+        createMenu()
 
-        mediaCommandView?.setTitleDate()
-        mediaCommandView?.createMenu()
+        if imageCache.createMediaCacheInProgress {
+            self.metadatas = nil
+            self.collectionViewReloadData()
+        } else if let metadatas = imageCache.initialMetadatas() {
+            self.metadatas = metadatas
+            self.collectionViewReloadData()
+        } else {
+            DispatchQueue.global().async {
+                self.reloadDataSource()
+            }
+        }
     }
 
     override func viewWillDisappear(_ animated: Bool) {
         super.viewWillDisappear(animated)
 
         NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDeleteFile), object: nil)
+        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterApplicationWillEnterForeground), object: nil)
     }
 
     override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
         super.viewWillTransition(to: size, with: coordinator)
-
-        collectionView?.collectionViewLayout.invalidateLayout()
-        mediaCommandView?.setTitleDate()
+        coordinator.animate(alongsideTransition: nil) { _ in
+            self.setTitleDate()
+        }
     }
 
     override var preferredStatusBarStyle: UIStatusBarStyle {
         if self.traitCollection.userInterfaceStyle == .dark {
             return .lightContent
-        } else if let gradient = mediaCommandView?.gradient, gradient.isHidden {
+       } else if isTop {
             return .darkContent
         } else {
             return .lightContent
@@ -154,46 +196,42 @@ class NCMedia: UIViewController, NCEmptyDataSetDelegate {
 
     override func viewWillLayoutSubviews() {
         super.viewWillLayoutSubviews()
+
         if let frame = tabBarController?.tabBar.frame {
             tabBarSelect?.hostingController.view.frame = frame
         }
+        gradient.frame = gradientView.bounds
+    }
+
+    func startTimer() {
+        // don't start if media chage is in progress
+        if imageCache.createMediaCacheInProgress {
+            return
+        }
+        timerSearchNewMedia?.invalidate()
+        timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(searchMediaUI), userInfo: nil, repeats: false)
     }
 
     // MARK: - NotificationCenter
 
     @objc func deleteFile(_ notification: NSNotification) {
-
         guard let userInfo = notification.userInfo as NSDictionary?,
-              let ocIds = userInfo["ocId"] as? [String],
               let error = userInfo["error"] as? NKError else { return }
 
-        if !ocIds.isEmpty {
-            var items: [IndexPath] = []
-            self.metadatas = self.metadatas?.filter({ !ocIds.contains($0.ocId )})
-            if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }) {
-                for case let cell as NCGridMediaCell in visibleCells {
-                    if let ocId = cell.fileObjectId, ocIds.contains(ocId) {
-                        items.append(cell.indexPath)
-                    }
-                }
-                if !items.isEmpty {
-                    collectionView?.deleteItems(at: items)
-                }
-            }
-            self.collectionView?.reloadData()
-        }
-
+        self.reloadDataSource()
         if error != .success {
             NCContentPresenter().showError(error: error)
         }
     }
 
+    @objc func enterForeground(_ notification: NSNotification) {
+        startTimer()
+    }
     // MARK: - Empty
 
     func emptyDataSetView(_ view: NCEmptyView) {
-
         view.emptyImage.image = UIImage(named: "media")?.image(color: .gray, size: UIScreen.main.bounds.width)
-        if loadingTask != nil {
+        if loadingTask != nil || imageCache.createMediaCacheInProgress {
             view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "")
         } else {
             view.emptyTitle.text = NSLocalizedString("_tutorial_photo_view_", comment: "")
@@ -205,38 +243,64 @@ class NCMedia: UIViewController, NCEmptyDataSetDelegate {
 
     func getImage(metadata: tableMetadata) -> UIImage? {
 
-        if let cachedImage = NCImageCache.shared.getMediaImage(ocId: metadata.ocId, etag: metadata.etag), case let .actual(image) = cachedImage {
+        if let image = imageCache.getMediaImage(ocId: metadata.ocId, etag: metadata.etag) {
             return image
-        } else if FileManager().fileExists(atPath: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)) {
-            if let image = UIImage(contentsOfFile: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)) {
-                NCImageCache.shared.setMediaImage(ocId: metadata.ocId, etag: metadata.etag, image: .actual(image))
-                return image
-            }
-        } else {
-            if metadata.hasPreview && metadata.status == NCGlobal.shared.metadataStatusNormal && (!utilityFileSystem.fileProviderStoragePreviewIconExists(metadata.ocId, etag: metadata.etag)) {
-                if NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnaill)?.metadata.ocId == metadata.ocId }).isEmpty {
-                    NCNetworking.shared.downloadThumbnailQueue.addOperation(NCMediaDownloadThumbnaill(metadata: metadata, collectionView: collectionView))
-                }
+        } else if FileManager().fileExists(atPath: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)),
+                  let image = UIImage(contentsOfFile: utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)) {
+            imageCache.setMediaSize(ocId: metadata.ocId, etag: metadata.etag, size: image.size)
+            if imageCache.hasMediaImageEnoughSpace() {
+                imageCache.setMediaImage(ocId: metadata.ocId, etag: metadata.etag, image: image, date: metadata.date as Date)
             }
+            return image
+        } else if metadata.hasPreview && metadata.status == NCGlobal.shared.metadataStatusNormal,
+                  (!utilityFileSystem.fileProviderStoragePreviewIconExists(metadata.ocId, etag: metadata.etag)),
+                  NCNetworking.shared.downloadThumbnailQueue.operations.filter({ ($0 as? NCMediaDownloadThumbnaill)?.metadata.ocId == metadata.ocId }).isEmpty {
+            NCNetworking.shared.downloadThumbnailQueue.addOperation(NCMediaDownloadThumbnaill(metadata: metadata, media: self))
         }
         return nil
     }
+
+    func buildMediaPhotoVideo(columnCount: Int) {
+        var pointSize: CGFloat = 0
+
+        switch columnCount {
+        case 0...1: pointSize = 60
+        case 2...3: pointSize = 30
+        case 4...5: pointSize = 25
+        case 6...Int(maxImageGrid): pointSize = 20
+        default: pointSize = 20
+        }
+        if let image = UIImage(systemName: "photo.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))?.withTintColor(.systemGray4, renderingMode: .alwaysOriginal) {
+            photoImage = image
+        }
+        if let image = UIImage(systemName: "video.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))?.withTintColor(.systemGray4, renderingMode: .alwaysOriginal) {
+            videoImage = image
+        }
+    }
 }
 
 // MARK: - Collection View
 
 extension NCMedia: UICollectionViewDelegate {
-
     func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
-
+        var mediaCell: NCGridMediaCell?
         if let metadata = self.metadatas?[indexPath.row] {
+            if let visibleCells = self.collectionView?.indexPathsForVisibleItems.compactMap({ self.collectionView?.cellForItem(at: $0) }) {
+                for case let cell as NCGridMediaCell in visibleCells {
+                    if cell.fileObjectId == metadata.ocId {
+                        mediaCell = cell
+                    }
+                }
+            }
             if isEditMode {
                 if let index = selectOcId.firstIndex(of: metadata.ocId) {
                     selectOcId.remove(at: index)
+                    mediaCell?.selected(false)
                 } else {
                     selectOcId.append(metadata.ocId)
+                    mediaCell?.selected(true)
+
                 }
-                collectionView.reloadItems(at: [indexPath])
                 tabBarSelect?.selectCount = selectOcId.count
             } else {
                 // ACTIVE SERVERURL
@@ -249,7 +313,6 @@ extension NCMedia: UICollectionViewDelegate {
     }
 
     func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
-
         guard let cell = collectionView.cellForItem(at: indexPath) as? NCGridMediaCell,
               let metadata = self.metadatas?[indexPath.row] else { return nil }
         let identifier = indexPath as NSCopying
@@ -263,7 +326,6 @@ extension NCMedia: UICollectionViewDelegate {
     }
 
     func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
-
         animator.addCompletion {
             if let indexPath = configuration.identifier as? IndexPath {
                 self.collectionView(collectionView, didSelectItemAt: indexPath)
@@ -273,43 +335,37 @@ extension NCMedia: UICollectionViewDelegate {
 }
 
 extension NCMedia: UICollectionViewDataSourcePrefetching {
-
     func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
         // print("[LOG] n. " + String(indexPaths.count))
     }
 }
 
 extension NCMedia: UICollectionViewDataSource {
-
     func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
-
         var numberOfItemsInSection = 0
-
-        if let metadatas {
-            numberOfItemsInSection = metadatas.count
-        }
-
+        if let metadatas { numberOfItemsInSection = metadatas.count }
         if numberOfItemsInSection == 0 {
-            mediaCommandView?.selectOrCancelButton.isHidden = true
-            mediaCommandView?.menuButton.isHidden = false
-            mediaCommandView?.activityIndicatorTrailing.constant = 46
+            selectOrCancelButton.isHidden = true
+            menuButton.isHidden = false
+            gradientView.isHidden = true
+            activityIndicatorTrailing.constant = 50
         } else if isEditMode {
-            mediaCommandView?.selectOrCancelButton.isHidden = false
-            mediaCommandView?.menuButton.isHidden = true
-            mediaCommandView?.activityIndicatorTrailing.constant = 144
+            selectOrCancelButton.isHidden = false
+            menuButton.isHidden = true
+            activityIndicatorTrailing.constant = 150
         } else {
-            mediaCommandView?.selectOrCancelButton.isHidden = false
-            mediaCommandView?.menuButton.isHidden = false
-            mediaCommandView?.activityIndicatorTrailing.constant = 144
+            selectOrCancelButton.isHidden = false
+            menuButton.isHidden = false
+            activityIndicatorTrailing.constant = 150
         }
 
         emptyDataSet?.numberOfItemsInSection(numberOfItemsInSection, section: section)
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self.setTitleDate() }
 
         return numberOfItemsInSection
     }
 
     func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
-
         guard let metadatas else { return }
 
         if !collectionView.indexPathsForVisibleItems.contains(indexPath) && indexPath.row < metadatas.count {
@@ -324,22 +380,27 @@ extension NCMedia: UICollectionViewDataSource {
     }
 
     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
-
         guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "gridCell", for: indexPath) as? NCGridMediaCell,
               let metadatas = self.metadatas,
               let metadata = metadatas[indexPath.row] else { return UICollectionViewCell() }
 
-        self.cellHeigth = cell.frame.size.height
-
-        cell.date = metadata.date as Date
+        cell.fileDate = metadata.date as Date
         cell.fileObjectId = metadata.ocId
         cell.indexPath = indexPath
         cell.fileUser = metadata.ownerId
         cell.imageStatus.image = nil
+        cell.imageItem.contentMode = .scaleAspectFill
 
         if let image = getImage(metadata: metadata) {
-            cell.imageItem.backgroundColor = nil
             cell.imageItem.image = image
+        } else if !metadata.hasPreview {
+            cell.imageItem.backgroundColor = .clear
+            cell.imageItem.contentMode = .center
+            if metadata.isImage {
+                cell.imageItem.image = photoImage
+            } else {
+                cell.imageItem.image = videoImage
+            }
         }
 
         // Convert OLD Live Photo
@@ -348,43 +409,68 @@ extension NCMedia: UICollectionViewDataSource {
         }
 
         if metadata.isAudioOrVideo {
-            cell.imageStatus.image = cacheImages.cellPlayImage
+           cell.imageStatus.image = playImage
         } else if metadata.isLivePhoto {
-            cell.imageStatus.image = cacheImages.cellLivePhotoImage
+            cell.imageStatus.image = livePhotoImage
         } else {
             cell.imageStatus.image = nil
         }
 
-        if isEditMode {
-            cell.selectMode(true)
-            if selectOcId.contains(metadata.ocId) {
-                cell.selected(true)
-            } else {
-                cell.selected(false)
-            }
+        if isEditMode, selectOcId.contains(metadata.ocId) {
+            cell.selected(true)
         } else {
-            cell.selectMode(false)
+            cell.selected(false)
         }
 
         return cell
     }
 }
 
-// MARK: - ScrollView
+// MARK: -
 
-extension NCMedia: UIScrollViewDelegate {
+extension NCMedia: UICollectionViewDelegateFlowLayout {
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
+        return CGSize(width: collectionView.frame.width, height: 0)
+    }
+
+    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
+        return CGSize(width: collectionView.frame.width, height: 0)
+    }
+}
 
+// MARK: -
+
+extension NCMedia: NCMediaLayoutDelegate {
+    func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath, columnCount: Int, mediaLayout: String) -> CGSize {
+        let size = CGSize(width: collectionView.frame.width / CGFloat(columnCount), height: collectionView.frame.width / CGFloat(columnCount))
+        if mediaLayout == NCGlobal.shared.mediaLayoutRatio {
+            guard let metadatas = self.metadatas,
+                  let metadata = metadatas[indexPath.row] else { return size }
+
+            if metadata.imageSize != CGSize.zero {
+                return metadata.imageSize
+            } else if let size = imageCache.getMediaSize(ocId: metadata.ocId, etag: metadata.etag) {
+                return size
+            }
+        }
+        return size
+    }
+}
+
+// MARK: -
+
+extension NCMedia: UIScrollViewDelegate {
     func scrollViewDidScroll(_ scrollView: UIScrollView) {
         if let metadatas, !metadatas.isEmpty {
-            let isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 35)
-            mediaCommandView?.setColor(isTop: isTop)
+            isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25)
+            setColor()
             setNeedsStatusBarAppearanceUpdate()
-            if lastContentOffsetY == 0 || lastContentOffsetY + cellHeigth / 2 <= scrollView.contentOffset.y || lastContentOffsetY - cellHeigth / 2 >= scrollView.contentOffset.y {
-                mediaCommandView?.setTitleDate()
+            if lastContentOffsetY == 0 || lastContentOffsetY / 2 <= scrollView.contentOffset.y || lastContentOffsetY / 2 >= scrollView.contentOffset.y {
+                setTitleDate()
                 lastContentOffsetY = scrollView.contentOffset.y
             }
         } else {
-            mediaCommandView?.setColor(isTop: true)
+            setColor()
         }
     }
 
@@ -394,15 +480,13 @@ extension NCMedia: UIScrollViewDelegate {
     func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
         if !decelerate {
             if !decelerate {
-                timerSearchNewMedia?.invalidate()
-                timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(searchMediaUI), userInfo: nil, repeats: false)
+                startTimer()
             }
         }
     }
 
     func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
-        timerSearchNewMedia?.invalidate()
-        timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(searchMediaUI), userInfo: nil, repeats: false)
+        startTimer()
     }
 
     func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
@@ -411,17 +495,16 @@ extension NCMedia: UIScrollViewDelegate {
     }
 }
 
-// MARK: - NCSelect Delegate
+// MARK: -
 
 extension NCMedia: NCSelectDelegate {
-
     func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], indexPath: [IndexPath], overwrite: Bool, copy: Bool, move: Bool) {
         guard let serverUrl = serverUrl else { return }
         let home = utilityFileSystem.getHomeServer(urlBase: appDelegate.urlBase, userId: appDelegate.userId)
-        mediaPath = serverUrl.replacingOccurrences(of: home, with: "")
-        NCManageDatabase.shared.setAccountMediaPath(mediaPath, account: appDelegate.account)
+        let mediaPath = serverUrl.replacingOccurrences(of: home, with: "")
+        NCManageDatabase.shared.setAccountMediaPath(mediaPath, account: activeAccount.account)
+        activeAccount = NCManageDatabase.shared.getActiveAccount() ?? tableAccount()
         reloadDataSource()
-        timerSearchNewMedia?.invalidate()
-        timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(self.searchMediaUI), userInfo: nil, repeats: false)
+        startTimer()
     }
 }

+ 0 - 253
iOSClient/Media/NCMediaCommandView.swift

@@ -1,253 +0,0 @@
-//
-//  NCMediaCommandView.swift
-//  Nextcloud
-//
-//  Created by Marino Faggiana on 25/01/24.
-//  Copyright © 2024 Marino Faggiana. All rights reserved.
-//
-//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
-//
-//  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 <http://www.gnu.org/licenses/>.
-//
-
-import UIKit
-import NextcloudKit
-
-class NCMediaCommandView: UIView {
-
-    @IBOutlet weak var title: UILabel!
-    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
-    @IBOutlet weak var activityIndicatorTrailing: NSLayoutConstraint!
-    @IBOutlet weak var selectOrCancelButton: UIButton!
-    @IBOutlet weak var selectOrCancelButtonTrailing: NSLayoutConstraint!
-    @IBOutlet weak var menuButton: UIButton!
-
-    var mediaView: NCMedia!
-    var tabBarController: UITabBarController?
-    var attributesZoomIn: UIMenuElement.Attributes = []
-    var attributesZoomOut: UIMenuElement.Attributes = []
-    let gradient: CAGradientLayer = CAGradientLayer()
-
-    override func awakeFromNib() {
-        super.awakeFromNib()
-
-        title.text = ""
-
-        selectOrCancelButton.backgroundColor = nil
-        selectOrCancelButton.layer.cornerRadius = 15
-        selectOrCancelButton.layer.masksToBounds = true
-        selectOrCancelButton.setTitle( NSLocalizedString("_select_", comment: ""), for: .normal)
-        selectOrCancelButton.addBlur(style: .systemThinMaterial)
-
-        menuButton.backgroundColor = nil
-        menuButton.layer.cornerRadius = 15
-        menuButton.layer.masksToBounds = true
-        menuButton.showsMenuAsPrimaryAction = true
-        menuButton.configuration = UIButton.Configuration.plain()
-        menuButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
-        menuButton.changesSelectionAsPrimaryAction = false
-        menuButton.addBlur(style: .systemThinMaterial)
-
-        gradient.frame = bounds
-        gradient.startPoint = CGPoint(x: 0, y: 0.5)
-        gradient.endPoint = CGPoint(x: 0, y: 1)
-        gradient.colors = [UIColor.black.withAlphaComponent(UIAccessibility.isReduceTransparencyEnabled ? 0.8 : 0.4).cgColor, UIColor.clear.cgColor]
-        layer.insertSublayer(gradient, at: 0)
-    }
-
-    override func layoutSublayers(of layer: CALayer) {
-        super.layoutSublayers(of: layer)
-        gradient.frame = bounds
-    }
-
-    @IBAction func selectOrCancelButtonPressed(_ sender: UIButton) {
-
-        mediaView.isEditMode = !mediaView.isEditMode
-        setSelectcancelButton()
-    }
-
-    func setSelectcancelButton() {
-
-        mediaView.selectOcId.removeAll()
-        mediaView.tabBarSelect?.selectCount = mediaView.selectOcId.count
-
-        if mediaView.isEditMode {
-            selectOrCancelButton.setTitle( NSLocalizedString("_cancel_", comment: ""), for: .normal)
-            selectOrCancelButtonTrailing.constant = 8
-            mediaView.tabBarSelect?.show()
-        } else {
-            selectOrCancelButton.setTitle( NSLocalizedString("_select_", comment: ""), for: .normal)
-            selectOrCancelButtonTrailing.constant = 46
-            mediaView.tabBarSelect?.hide()
-        }
-
-        mediaView.collectionView.reloadData()
-    }
-
-    func setTitleDate() {
-
-        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
-            self.title.text = ""
-            if let visibleCells = self.mediaView.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.mediaView.collectionView?.cellForItem(at: $0) }) {
-                if let cell = visibleCells.first as? NCGridMediaCell {
-                    self.title.text = ""
-                    if let date = cell.date {
-                        self.title.text = self.mediaView.utility.getTitleFromDate(date)
-                    }
-                }
-            }
-        }
-    }
-
-    func setColor(isTop: Bool) {
-
-        if isTop {
-            title.textColor = .label
-            activityIndicator.color = .label
-            selectOrCancelButton.setTitleColor(.label, for: .normal)
-            menuButton.setImage(UIImage(systemName: "ellipsis")?.withTintColor(.label, renderingMode: .alwaysOriginal), for: .normal)
-            gradient.isHidden = true
-        } else {
-            title.textColor = .white
-            activityIndicator.color = .white
-            selectOrCancelButton.setTitleColor(.white, for: .normal)
-            menuButton.setImage(UIImage(systemName: "ellipsis")?.withTintColor(.white, renderingMode: .alwaysOriginal), for: .normal)
-            gradient.isHidden = false
-        }
-    }
-
-    func createMenu() {
-
-        if let itemForLine = mediaView?.layout.itemForLine, let maxImageGrid = mediaView?.maxImageGrid {
-            if itemForLine >= maxImageGrid - 1 {
-                self.attributesZoomIn = []
-                self.attributesZoomOut = .disabled
-            } else if itemForLine <= 1 {
-                self.attributesZoomIn = .disabled
-                self.attributesZoomOut = []
-            } else {
-                self.attributesZoomIn = []
-                self.attributesZoomOut = []
-            }
-        }
-
-        let topAction = UIMenu(title: "", options: .displayInline, children: [
-            UIMenu(title: NSLocalizedString("_zoom_", comment: ""), children: [
-                UIAction(title: NSLocalizedString("_zoom_out_", comment: ""), image: UIImage(systemName: "minus.magnifyingglass"), attributes: self.attributesZoomOut) { _ in
-                    guard let mediaView = self.mediaView else { return }
-                    UIView.animate(withDuration: 0.0, animations: {
-                        mediaView.layout.itemForLine += 1
-                        self.createMenu()
-                        mediaView.collectionView.collectionViewLayout.invalidateLayout()
-                        NCKeychain().mediaItemForLine = Int(mediaView.layout.itemForLine)
-                    })
-                },
-                UIAction(title: NSLocalizedString("_zoom_in_", comment: ""), image: UIImage(systemName: "plus.magnifyingglass"), attributes: self.attributesZoomIn) { _ in
-                    UIView.animate(withDuration: 0.0, animations: {
-                        self.mediaView.layout.itemForLine -= 1
-                        self.createMenu()
-                        self.mediaView.collectionView.collectionViewLayout.invalidateLayout()
-                        NCKeychain().mediaItemForLine = Int(self.mediaView.layout.itemForLine)
-                    })
-                }
-            ]),
-            UIMenu(title: NSLocalizedString("_media_view_options_", comment: ""), children: [
-                UIAction(title: NSLocalizedString("_media_viewimage_show_", comment: ""), image: UIImage(systemName: "photo")) { _ in
-                    self.mediaView.showOnlyImages = true
-                    self.mediaView.showOnlyVideos = false
-                    self.mediaView.reloadDataSource()
-                },
-                UIAction(title: NSLocalizedString("_media_viewvideo_show_", comment: ""), image: UIImage(systemName: "video")) { _ in
-                    self.mediaView.showOnlyImages = false
-                    self.mediaView.showOnlyVideos = true
-                    self.mediaView.reloadDataSource()
-                },
-                UIAction(title: NSLocalizedString("_media_show_all_", comment: ""), image: UIImage(systemName: "photo.on.rectangle")) { _ in
-                    self.mediaView.showOnlyImages = false
-                    self.mediaView.showOnlyVideos = false
-                    self.mediaView.reloadDataSource()
-                }
-            ]),
-            UIAction(title: NSLocalizedString("_select_media_folder_", comment: ""), image: UIImage(systemName: "folder"), handler: { _ in
-                guard let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController,
-                      let viewController = navigationController.topViewController as? NCSelect else { return }
-                viewController.delegate = self.mediaView
-                viewController.typeOfCommandView = .select
-                viewController.type = "mediaFolder"
-                self.mediaView.present(navigationController, animated: true, completion: nil)
-            })
-        ])
-
-        let playFile = UIAction(title: NSLocalizedString("_play_from_files_", comment: ""), image: UIImage(systemName: "play.circle")) { _ in
-            guard let tabBarController = self.mediaView.appDelegate.window?.rootViewController as? UITabBarController else { return }
-            self.mediaView.documentPickerViewController = NCDocumentPickerViewController(tabBarController: tabBarController, isViewerMedia: true, allowsMultipleSelection: false, viewController: self.mediaView)
-        }
-        let playURL = UIAction(title: NSLocalizedString("_play_from_url_", comment: ""), image: UIImage(systemName: "link")) { _ in
-            let alert = UIAlertController(title: NSLocalizedString("_valid_video_url_", comment: ""), message: nil, preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: nil))
-            alert.addTextField(configurationHandler: { textField in
-                textField.placeholder = "http://myserver.com/movie.mkv"
-            })
-            alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in
-                guard let stringUrl = alert.textFields?.first?.text, !stringUrl.isEmpty, let url = URL(string: stringUrl) else { return }
-                let fileName = url.lastPathComponent
-                let appDelegate = (UIApplication.shared.delegate as? AppDelegate)!
-                let metadata = NCManageDatabase.shared.createMetadata(account: appDelegate.account, user: appDelegate.user, userId: appDelegate.userId, fileName: fileName, fileNameView: fileName, ocId: NSUUID().uuidString, serverUrl: "", urlBase: appDelegate.urlBase, url: stringUrl, contentType: "")
-                NCManageDatabase.shared.addMetadata(metadata)
-                NCViewer().view(viewController: self.mediaView, metadata: metadata, metadatas: [metadata], imageIcon: nil)
-            }))
-            self.mediaView.present(alert, animated: true)
-        }
-
-        menuButton.menu = UIMenu(title: "", children: [topAction, playFile, playURL])
-    }
-}
-
-// MARK: - NCMediaTabBarSelectDelegate
-
-extension NCMediaCommandView: NCMediaSelectTabBarDelegate {
-
-    func delete() {
-
-        if !mediaView.selectOcId.isEmpty {
-            let selectOcId = mediaView.selectOcId
-            let alertController = UIAlertController(
-                title: NSLocalizedString("_delete_selected_photos_", comment: ""),
-                message: "",
-                preferredStyle: .alert)
-            alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_", comment: ""), style: .default) { (_: UIAlertAction) in
-
-                Task {
-                    var error = NKError()
-                    var ocIds: [String] = []
-                    for ocId in selectOcId where error == .success {
-                        if let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) {
-                            error = await NCNetworking.shared.deleteMetadata(metadata, onlyLocalCache: false)
-                            if error == .success {
-                                ocIds.append(metadata.ocId)
-                            }
-                        }
-                    }
-                    NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDeleteFile, userInfo: ["ocId": ocIds, "onlyLocalCache": false, "error": error])
-                }
-
-                self.mediaView.isEditMode = false
-                self.setSelectcancelButton()
-            })
-            alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .default) { (_: UIAlertAction) in })
-
-            mediaView.present(alertController, animated: true, completion: { })
-        }
-    }
-}

+ 0 - 77
iOSClient/Media/NCMediaCommandView.xib

@@ -1,77 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
-    <device id="retina6_12" orientation="portrait" appearance="light"/>
-    <dependencies>
-        <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
-        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
-        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
-    </dependencies>
-    <objects>
-        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
-        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
-        <view contentMode="scaleToFill" id="iN0-l3-epB" customClass="NCMediaCommandView" customModule="Nextcloud" customModuleProvider="target">
-            <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
-            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-            <subviews>
-                <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IxY-xH-yZQ">
-                    <rect key="frame" x="8" y="67" width="213" height="40"/>
-                    <constraints>
-                        <constraint firstAttribute="height" constant="40" id="S6o-Pa-sxy"/>
-                    </constraints>
-                    <fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
-                    <nil key="textColor"/>
-                    <nil key="highlightedColor"/>
-                </label>
-                <activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="XVj-jD-9KA">
-                    <rect key="frame" x="229" y="77" width="20" height="20"/>
-                    <color key="color" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                </activityIndicatorView>
-                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="EFV-eb-pFF">
-                    <rect key="frame" x="257" y="72" width="90" height="30"/>
-                    <constraints>
-                        <constraint firstAttribute="width" constant="90" id="Hf1-Hv-Jpi"/>
-                        <constraint firstAttribute="height" constant="30" id="tTh-bW-DMw"/>
-                    </constraints>
-                    <fontDescription key="fontDescription" type="system" pointSize="14"/>
-                    <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
-                    <state key="normal" title="Title"/>
-                    <connections>
-                        <action selector="selectOrCancelButtonPressed:" destination="iN0-l3-epB" eventType="touchUpInside" id="0dX-pC-icF"/>
-                    </connections>
-                </button>
-                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" adjustsImageWhenHighlighted="NO" adjustsImageWhenDisabled="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3qs-Hm-qLL">
-                    <rect key="frame" x="355" y="72" width="30" height="30"/>
-                    <constraints>
-                        <constraint firstAttribute="height" constant="30" id="l6a-hf-7l2"/>
-                        <constraint firstAttribute="width" constant="30" id="uVw-bC-TZq"/>
-                    </constraints>
-                    <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                    <inset key="imageEdgeInsets" minX="4" minY="4" maxX="4" maxY="4"/>
-                </button>
-            </subviews>
-            <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
-            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-            <constraints>
-                <constraint firstItem="XVj-jD-9KA" firstAttribute="centerY" secondItem="IxY-xH-yZQ" secondAttribute="centerY" id="4nZ-Ea-KMB"/>
-                <constraint firstItem="3qs-Hm-qLL" firstAttribute="centerY" secondItem="IxY-xH-yZQ" secondAttribute="centerY" id="AgJ-WD-mqU"/>
-                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="3qs-Hm-qLL" secondAttribute="trailing" constant="8" id="FtF-ES-dyl"/>
-                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="EFV-eb-pFF" secondAttribute="trailing" constant="46" id="OIB-Zp-XkO"/>
-                <constraint firstItem="IxY-xH-yZQ" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="8" id="ZhO-pY-Qwi"/>
-                <constraint firstItem="IxY-xH-yZQ" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" constant="8" id="mX3-Fm-K1m"/>
-                <constraint firstItem="EFV-eb-pFF" firstAttribute="centerY" secondItem="IxY-xH-yZQ" secondAttribute="centerY" id="ozT-m6-dct"/>
-                <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="XVj-jD-9KA" secondAttribute="trailing" constant="144" id="uEq-dt-udC"/>
-                <constraint firstItem="XVj-jD-9KA" firstAttribute="leading" secondItem="IxY-xH-yZQ" secondAttribute="trailing" constant="8" id="ztz-0d-9Mr"/>
-            </constraints>
-            <connections>
-                <outlet property="activityIndicator" destination="XVj-jD-9KA" id="cSB-RJ-RCZ"/>
-                <outlet property="activityIndicatorTrailing" destination="uEq-dt-udC" id="pvB-8X-Jpb"/>
-                <outlet property="menuButton" destination="3qs-Hm-qLL" id="7uo-w1-pml"/>
-                <outlet property="selectOrCancelButton" destination="EFV-eb-pFF" id="2ve-si-IjY"/>
-                <outlet property="selectOrCancelButtonTrailing" destination="OIB-Zp-XkO" id="2gp-In-Snh"/>
-                <outlet property="title" destination="IxY-xH-yZQ" id="ZNZ-Jy-JbH"/>
-            </connections>
-            <point key="canvasLocation" x="140" y="148"/>
-        </view>
-    </objects>
-</document>

+ 85 - 65
iOSClient/Media/NCMediaDataSource.swift

@@ -26,112 +26,132 @@ import NextcloudKit
 
 extension NCMedia {
 
-    func getPredicate(showAll: Bool = false) -> NSPredicate {
-
-        let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: appDelegate.urlBase, userId: appDelegate.userId) + mediaPath
-
-        if showAll {
-            return NSPredicate(format: NCImageCache.shared.showAllPredicateMediaString, appDelegate.account, startServerUrl)
-        } else if showOnlyImages {
-            return NSPredicate(format: NCImageCache.shared.showOnlyPredicateMediaString, appDelegate.account, startServerUrl, NKCommon.TypeClassFile.image.rawValue)
-        } else if showOnlyVideos {
-            return NSPredicate(format: NCImageCache.shared.showOnlyPredicateMediaString, appDelegate.account, startServerUrl, NKCommon.TypeClassFile.video.rawValue)
-        } else {
-           return NSPredicate(format: NCImageCache.shared.showBothPredicateMediaString, appDelegate.account, startServerUrl)
-        }
+    func reloadDataSource() {
+        self.metadatas = imageCache.getMediaMetadatas(account: activeAccount.account, predicate: self.getPredicate())
+        self.collectionViewReloadData()
     }
 
-    @objc func reloadDataSource() {
-        guard !appDelegate.account.isEmpty else { return }
-
-        self.metadatas = NCImageCache.shared.getMediaMetadatas(account: self.appDelegate.account, predicate: self.getPredicate())
-        DispatchQueue.main.async {
-            self.collectionView?.reloadData()
-            self.mediaCommandView?.setTitleDate()
+    func collectionViewReloadData() {
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+            self.collectionView.reloadData()
+            self.setTitleDate()
         }
     }
 
     // MARK: - Search media
 
     @objc func searchMediaUI() {
-
         var lessDate: Date?
         var greaterDate: Date?
         let firstMetadataDate = metadatas?.first?.date as? Date
         let lastMetadataDate = metadatas?.last?.date as? Date
+        let countMetadatas = self.metadatas?.count ?? 0
 
-        guard loadingTask == nil, !isEditMode else {
+        guard loadingTask == nil,
+              !isEditMode,
+              self.viewIfLoaded?.window != nil,
+              let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) })
+        else {
             return
         }
 
-        if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }) {
-            // first date
-            let firstCellDate = (visibleCells.first as? NCGridMediaCell)?.date
-            if firstCellDate == firstMetadataDate {
-                lessDate = Date.distantFuture
+        // first date
+        let firstCellDate = (visibleCells.first as? NCGridMediaCell)?.fileDate
+        if firstCellDate == firstMetadataDate {
+            lessDate = Date.distantFuture
+        } else {
+            if let date = firstCellDate {
+                lessDate = Calendar.current.date(byAdding: .second, value: 1, to: date)!
             } else {
-                if let date = firstCellDate {
-                    lessDate = Calendar.current.date(byAdding: .second, value: 1, to: date)!
-                } else {
-                    lessDate = Date.distantFuture
-                }
+                lessDate = Date.distantFuture
             }
-            // last date
-            let lastCellDate = (visibleCells.last as? NCGridMediaCell)?.date
-            if lastCellDate == lastMetadataDate {
-                greaterDate = Date.distantPast
+        }
+        // last date
+        let lastCellDate = (visibleCells.last as? NCGridMediaCell)?.fileDate
+        if lastCellDate == lastMetadataDate {
+            greaterDate = Date.distantPast
+        } else {
+            if let date = lastCellDate {
+                greaterDate = Calendar.current.date(byAdding: .second, value: -1, to: date)!
             } else {
-                if let date = lastCellDate {
-                    greaterDate = Calendar.current.date(byAdding: .second, value: -1, to: date)!
-                } else {
-                    greaterDate = Date.distantPast
-                }
+                greaterDate = Date.distantPast
             }
+        }
+
+        if lessDate == Date.distantFuture,
+           greaterDate == Date.distantPast,
+           (self.metadatas?.count ?? 0) > visibleCells.count {
+            NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media oops. something is bad (distantFuture, distantPast): \(self.activeAccount.account), \(self.appDelegate.account), \(self.metadatas?.count ?? 0)")
+            return
+        }
 
-            if let lessDate, let greaterDate {
-                mediaCommandView?.activityIndicator.startAnimating()
-                loadingTask = Task.detached {
-                    await self.collectionView.reloadData()
-                    let results = await self.searchMedia(account: self.appDelegate.account, lessDate: lessDate, greaterDate: greaterDate)
-                    print("Media results changed items: \(results.isChanged)")
-                    await self.mediaCommandView?.activityIndicator.stopAnimating()
+        if let lessDate, let greaterDate {
+            activityIndicator.startAnimating()
+            loadingTask = Task.detached {
+                if countMetadatas == 0 {
+                    await self.collectionViewReloadData()
+                }
+                let results = await self.searchMedia(lessDate: lessDate, greaterDate: greaterDate)
+                if results.error == .success {
                     Task { @MainActor in
-                        self.loadingTask = nil
-                    }
-                    if results.error != .success {
-                        NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media error code \(results.error.errorCode) " + results.error.errorDescription)
-                    } else if results.error == .success, results.lessDate == Date.distantFuture, results.greaterDate == Date.distantPast, !results.isChanged, results.metadatasCount == 0 {
-                        Task { @MainActor in
+                        if results.lessDate == Date.distantFuture, results.greaterDate == Date.distantPast, !results.isChanged, results.metadatasCount == 0 {
                             self.metadatas = nil
+                            self.collectionViewReloadData()
+                            print("searchMediaUI: metadatacount 0")
+                        } else if results.isChanged {
+                            self.reloadDataSource()
+                            print("searchMediaUI: changed")
+                        } else {
+                            print("searchMediaUI: nothing")
                         }
                     }
-                    if results.isChanged {
-                        await self.reloadDataSource()
-                    } else {
-                        await self.collectionView.reloadData()
-                    }
+                } else {
+                    NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Media search new media error code \(results.error.errorCode) " + results.error.errorDescription)
+                }
+                Task { @MainActor in
+                    self.loadingTask = nil
+                    self.activityIndicator.stopAnimating()
                 }
             }
         }
     }
 
-    func searchMedia(account: String, lessDate: Date, greaterDate: Date, limit: Int = 120, timeout: TimeInterval = 60) async -> (account: String, lessDate: Date?, greaterDate: Date?, metadatasCount: Int, isChanged: Bool, error: NKError) {
+    private func searchMedia(lessDate: Date, greaterDate: Date, limit: Int = 300, timeout: TimeInterval = 120) async -> (lessDate: Date?, greaterDate: Date?, metadatasCount: Int, isChanged: Bool, error: NKError) {
 
         guard let mediaPath = NCManageDatabase.shared.getActiveAccount()?.mediaPath else {
-            return(account, lessDate, greaterDate, 0, false, NKError())
+            return(lessDate, greaterDate, 0, false, NKError())
         }
+        NextcloudKit.shared.nkCommonInstance.writeLog("Start searchMedia with lessDate \(lessDate), greaterDate \(greaterDate)")
         let options = NKRequestOptions(timeout: timeout, queue: NextcloudKit.shared.nkCommonInstance.backgroundQueue)
         let results = await NextcloudKit.shared.searchMedia(path: mediaPath, lessDate: lessDate, greaterDate: greaterDate, elementDate: "d:getlastmodified/", limit: limit, showHiddenFiles: NCKeychain().showHiddenFiles, includeHiddenFiles: [], options: options)
 
-        if results.account == account, results.error == .success {
+        if results.account != self.activeAccount.account {
+            let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "User changed")
+            return(lessDate, greaterDate, 0, false, error)
+        } else if results.error == .success {
             let metadatas = await NCManageDatabase.shared.convertFilesToMetadatas(results.files, useMetadataFolder: false).metadatas
             var predicate = NSPredicate(format: "date > %@ AND date < %@", greaterDate as NSDate, lessDate as NSDate)
-            predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, self.getPredicate(showAll: true)])
+            predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, getPredicate(showAll: true)])
             let resultsUpdate = NCManageDatabase.shared.updateMetadatas(metadatas, predicate: predicate)
             let isChaged: Bool = resultsUpdate.metadatasChanged || resultsUpdate.metadatasChangedCount != 0
-            return(account, lessDate, greaterDate, metadatas.count, isChaged, results.error)
+            NextcloudKit.shared.nkCommonInstance.writeLog("End searchMedia UpdateMetadatas with metadatasChanged \(resultsUpdate.metadatasChanged), ChangedCount \(resultsUpdate.metadatasChangedCount)")
+            return(lessDate, greaterDate, metadatas.count, isChaged, results.error)
+        } else {
+            return(lessDate, greaterDate, 0, false, results.error)
+        }
+    }
+
+    private func getPredicate(showAll: Bool = false) -> NSPredicate {
+        let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: activeAccount.urlBase, userId: activeAccount.userId) + activeAccount.mediaPath
+
+        if showAll {
+            return NSPredicate(format: imageCache.showAllPredicateMediaString, activeAccount.account, startServerUrl)
+        } else if showOnlyImages {
+            return NSPredicate(format: imageCache.showOnlyPredicateMediaString, activeAccount.account, startServerUrl, NKCommon.TypeClassFile.image.rawValue)
+        } else if showOnlyVideos {
+            return NSPredicate(format: imageCache.showOnlyPredicateMediaString, activeAccount.account, startServerUrl, NKCommon.TypeClassFile.video.rawValue)
         } else {
-            return(account, lessDate, greaterDate, 0, false, results.error)
+            return NSPredicate(format: imageCache.showBothPredicateMediaString, activeAccount.account, startServerUrl)
         }
     }
 }

+ 16 - 8
iOSClient/Media/NCMediaDownloadThumbnaill.swift

@@ -28,33 +28,34 @@ import Queuer
 class NCMediaDownloadThumbnaill: ConcurrentOperation {
 
     var metadata: tableMetadata
-    var collectionView: UICollectionView?
+    var media: NCMedia
     var fileNamePath: String
     var fileNamePreviewLocalPath: String
     var fileNameIconLocalPath: String
     let utilityFileSystem = NCUtilityFileSystem()
 
-    init(metadata: tableMetadata, collectionView: UICollectionView?) {
+    init(metadata: tableMetadata, media: NCMedia) {
         self.metadata = tableMetadata.init(value: metadata)
-        self.collectionView = collectionView
+        self.media = media
         self.fileNamePath = utilityFileSystem.getFileNamePath(metadata.fileName, serverUrl: metadata.serverUrl, urlBase: metadata.urlBase, userId: metadata.userId)
         self.fileNamePreviewLocalPath = utilityFileSystem.getDirectoryProviderStoragePreviewOcId(metadata.ocId, etag: metadata.etag)
         self.fileNameIconLocalPath = utilityFileSystem.getDirectoryProviderStorageIconOcId(metadata.ocId, etag: metadata.etag)
     }
 
     override func start() {
-
         guard !isCancelled else { return self.finish() }
 
         var etagResource: String?
+        let sizePreview = NCUtility().getSizePreview(width: metadata.width, height: metadata.height)
+
         if FileManager.default.fileExists(atPath: fileNameIconLocalPath) && FileManager.default.fileExists(atPath: fileNamePreviewLocalPath) {
             etagResource = metadata.etagResource
         }
 
         NextcloudKit.shared.downloadPreview(fileNamePathOrFileId: fileNamePath,
                                             fileNamePreviewLocalPath: fileNamePreviewLocalPath,
-                                            widthPreview: NCGlobal.shared.sizePreview,
-                                            heightPreview: NCGlobal.shared.sizePreview,
+                                            widthPreview: Int(sizePreview.width),
+                                            heightPreview: Int(sizePreview.height),
                                             fileNameIconLocalPath: fileNameIconLocalPath,
                                             sizeIcon: NCGlobal.shared.sizeIcon,
                                             etag: etagResource,
@@ -63,7 +64,7 @@ class NCMediaDownloadThumbnaill: ConcurrentOperation {
             if error == .success, let image = imagePreview {
                 NCManageDatabase.shared.setMetadataEtagResource(ocId: self.metadata.ocId, etagResource: etag)
                 DispatchQueue.main.async {
-                    if let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) }) {
+                    if let visibleCells = self.media.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.media.collectionView?.cellForItem(at: $0) }) {
                         for case let cell as NCGridMediaCell in visibleCells {
                             if cell.fileObjectId == self.metadata.ocId, let filePreviewImageView = cell.filePreviewImageView {
                                 UIView.transition(with: filePreviewImageView,
@@ -76,9 +77,16 @@ class NCMediaDownloadThumbnaill: ConcurrentOperation {
                         }
                     }
                 }
-                NCImageCache.shared.setMediaImage(ocId: self.metadata.ocId, etag: self.metadata.etag, image: .actual(image))
+                NCImageCache.shared.setMediaSize(ocId: self.metadata.ocId, etag: self.metadata.etag, size: image.size)
             }
             self.finish()
         }
     }
+
+    override func finish(success: Bool = true) {
+        super.finish(success: success)
+        if (metadata.width == 0 && metadata.height == 0) || (NCNetworking.shared.downloadThumbnailQueue.operationCount == 0) {
+            self.media.collectionViewReloadData()
+        }
+    }
 }

+ 0 - 79
iOSClient/Media/NCMediaGridLayout.swift

@@ -1,79 +0,0 @@
-//
-//  NCMediaGridLayout.swift
-//  Nextcloud
-//
-//  Created by Marino Faggiana on 27/01/24.
-//  Copyright © 2024 Marino Faggiana. All rights reserved.
-//
-//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
-//
-//  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 <http://www.gnu.org/licenses/>.
-//
-
-import UIKit
-
-class NCMediaGridLayout: UICollectionViewFlowLayout {
-
-    var marginLeftRight: CGFloat = 2
-    var itemForLine: CGFloat = 3
-
-    override init() {
-        super.init()
-
-        sectionHeadersPinToVisibleBounds = false
-
-        minimumInteritemSpacing = 0
-        minimumLineSpacing = marginLeftRight
-
-        self.scrollDirection = .vertical
-        self.sectionInset = UIEdgeInsets(top: 0, left: marginLeftRight, bottom: 0, right: marginLeftRight)
-    }
-
-    required init?(coder aDecoder: NSCoder) {
-        fatalError("init(coder:) has not been implemented")
-    }
-
-    override var itemSize: CGSize {
-        get {
-            if let collectionView = collectionView {
-
-                let itemWidth: CGFloat = (collectionView.frame.width - marginLeftRight * 2 - marginLeftRight * (itemForLine - 1)) / itemForLine
-                let itemHeight: CGFloat = itemWidth
-
-                return CGSize(width: itemWidth, height: itemHeight)
-            }
-
-            // Default fallback
-            return CGSize(width: 100, height: 100)
-        }
-        set {
-            super.itemSize = newValue
-        }
-    }
-
-    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
-        return proposedContentOffset
-    }
-}
-
-extension NCMedia: UICollectionViewDelegateFlowLayout {
-
-    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
-        return CGSize(width: collectionView.frame.width, height: 0)
-    }
-
-    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
-        return CGSize(width: collectionView.frame.width, height: 0)
-    }
-}

+ 320 - 0
iOSClient/Media/NCMediaLayout.swift

@@ -0,0 +1,320 @@
+//
+//  NCMediaLayout.swift
+//
+//  Created by Marino Faggiana on 26/02/24.
+//  Based on CHTCollectionViewWaterfallLayout by Nelson Tai
+//  Copyright © 2024 Marino Faggiana. All rights reserved.
+//
+//  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 <http://www.gnu.org/licenses/>.
+//
+
+import UIKit
+
+public let collectionViewMediaElementKindSectionHeader = "collectionViewMediaElementKindSectionHeader"
+public let collectionViewMediaElementKindSectionFooter = "collectionViewMediaElementKindSectionFooter"
+
+protocol NCMediaLayoutDelegate: UICollectionViewDelegate {
+    func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath, columnCount: Int, mediaLayout: String) -> CGSize
+}
+
+public class NCMediaLayout: UICollectionViewLayout {
+
+    // MARK: - Private constants
+    /// How many items to be union into a single rectangle
+    private let unionSize = 20
+
+    // MARK: - Public Properties
+    public var columnCount: Int = 0 {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: columnCount)
+        }
+    }
+    public var minimumColumnSpacing: Float = 2.0 {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: minimumColumnSpacing)
+        }
+    }
+    public var minimumInteritemSpacing: Float = 2.0 {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: minimumInteritemSpacing)
+        }
+    }
+    public var headerHeight: Float = 0.0 {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: headerHeight)
+        }
+    }
+    public var footerHeight: Float = 0.0 {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: footerHeight)
+        }
+    }
+    public var headerInset: UIEdgeInsets = .zero {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: headerInset)
+        }
+    }
+    public var footerInset: UIEdgeInsets = .zero {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: footerInset)
+        }
+    }
+    public var sectionInset: UIEdgeInsets = .zero {
+        didSet {
+            invalidateIfNotEqual(oldValue, newValue: sectionInset)
+        }
+    }
+    var mediaViewController: NCMedia?
+    var mediaLayout = ""
+
+    public override var collectionViewContentSize: CGSize {
+        let numberOfSections = collectionView?.numberOfSections
+        if numberOfSections == 0 {
+            return CGSize.zero
+        }
+
+        var contentSize = collectionView?.bounds.size
+        contentSize?.height = CGFloat(columnHeights[0])
+
+        return contentSize!
+    }
+
+    // MARK: - Private Properties
+    private weak var delegate: NCMediaLayoutDelegate? {
+        return collectionView?.delegate as? NCMediaLayoutDelegate
+    }
+    private var columnHeights = [Float]()
+    private var sectionItemAttributes = [[UICollectionViewLayoutAttributes]]()
+    private var allItemAttributes = [UICollectionViewLayoutAttributes]()
+    private var headersAttribute = [Int: UICollectionViewLayoutAttributes]()
+    private var footersAttribute = [Int: UICollectionViewLayoutAttributes]()
+    private var unionRects = [CGRect]()
+
+    // MARK: - UICollectionViewLayout Methods
+    public override func prepare() {
+        super.prepare()
+
+        guard let numberOfSections = collectionView?.numberOfSections,
+              let collectionView = collectionView,
+              let delegate = delegate else { return }
+
+        mediaLayout = NCKeychain().mediaTypeLayout
+        columnCount = NCKeychain().mediaColumnCount
+        mediaViewController?.buildMediaPhotoVideo(columnCount: columnCount)
+        if UIDevice.current.userInterfaceIdiom == .phone,
+           (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) {
+            columnCount += 2
+        }
+
+        // Initialize variables
+        headersAttribute.removeAll(keepingCapacity: false)
+        footersAttribute.removeAll(keepingCapacity: false)
+        unionRects.removeAll(keepingCapacity: false)
+        columnHeights.removeAll(keepingCapacity: false)
+        allItemAttributes.removeAll(keepingCapacity: false)
+        sectionItemAttributes.removeAll(keepingCapacity: false)
+
+        for _ in 0..<columnCount {
+            self.columnHeights.append(0)
+        }
+
+        // Create attributes
+        var top: Float = 0
+        var attributes: UICollectionViewLayoutAttributes
+
+        for section in 0..<numberOfSections {
+            /*
+            * 1. Get section-specific metrics (minimumInteritemSpacing, sectionInset)
+            */
+            let width = Float(collectionView.frame.size.width - sectionInset.left - sectionInset.right)
+            let itemWidth = floorf((width - Float(columnCount - 1) * Float(minimumColumnSpacing)) / Float(columnCount))
+
+            /*
+            * 2. Section header
+            */
+            top += Float(headerInset.top)
+
+            if headerHeight > 0 {
+                attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: collectionViewMediaElementKindSectionHeader, with: NSIndexPath(item: 0, section: section) as IndexPath)
+                attributes.frame = CGRect(x: headerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (headerInset.left + headerInset.right), height: CGFloat(headerHeight))
+
+                headersAttribute[section] = attributes
+                allItemAttributes.append(attributes)
+
+                top = Float(attributes.frame.maxY) + Float(headerInset.bottom)
+            }
+
+            top += Float(sectionInset.top)
+            for idx in 0..<columnCount {
+                columnHeights[idx] = top
+            }
+
+            /*
+            * 3. Section items
+            */
+            let itemCount = collectionView.numberOfItems(inSection: section)
+            var itemAttributes = [UICollectionViewLayoutAttributes]()
+
+            // Item will be put into shortest column.
+            for idx in 0..<itemCount {
+                let indexPath = NSIndexPath(item: idx, section: section)
+                let columnIndex = shortestColumnIndex()
+
+                let xOffset = Float(sectionInset.left) + Float(itemWidth + minimumColumnSpacing) * Float(columnIndex)
+                let yOffset = columnHeights[columnIndex]
+                let itemSize = delegate.collectionView(collectionView, layout: self, sizeForItemAtIndexPath: indexPath, columnCount: self.columnCount, mediaLayout: self.mediaLayout)
+                var itemHeight: Float = 0.0
+                if itemSize.height > 0 && itemSize.width > 0 {
+                    itemHeight = Float(itemSize.height) * itemWidth / Float(itemSize.width)
+                }
+
+                attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
+                attributes.frame = CGRect(x: CGFloat(xOffset), y: CGFloat(yOffset), width: CGFloat(itemWidth), height: CGFloat(itemHeight))
+                itemAttributes.append(attributes)
+                allItemAttributes.append(attributes)
+                columnHeights[columnIndex] = Float(attributes.frame.maxY) + minimumInteritemSpacing
+            }
+
+            sectionItemAttributes.append(itemAttributes)
+
+            /*
+            * 4. Section footer
+            */
+            let columnIndex = longestColumnIndex()
+            top = columnHeights[columnIndex] - minimumInteritemSpacing + Float(sectionInset.bottom)
+            top += Float(footerInset.top)
+
+            if footerHeight > 0 {
+                attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: collectionViewMediaElementKindSectionFooter, with: NSIndexPath(item: 0, section: section) as IndexPath)
+                attributes.frame = CGRect(x: footerInset.left, y: CGFloat(top), width: collectionView.frame.size.width - (footerInset.left + footerInset.right), height: CGFloat(footerHeight))
+
+                footersAttribute[section] = attributes
+                allItemAttributes.append(attributes)
+
+                top = Float(attributes.frame.maxY) + Float(footerInset.bottom)
+            }
+
+            for idx in 0..<columnCount {
+                columnHeights[idx] = top
+            }
+        }
+
+        // Build union rects
+        var idx = 0
+        let itemCounts = allItemAttributes.count
+
+        while idx < itemCounts {
+            let rect1 = allItemAttributes[idx].frame
+            idx = min(idx + unionSize, itemCounts) - 1
+            let rect2 = allItemAttributes[idx].frame
+            unionRects.append(rect1.union(rect2))
+            idx += 1
+        }
+    }
+
+    public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
+        if indexPath.section >= sectionItemAttributes.count {
+            return nil
+        }
+
+        if indexPath.item >= sectionItemAttributes[indexPath.section].count {
+            return nil
+        }
+
+        return sectionItemAttributes[indexPath.section][indexPath.item]
+    }
+
+    public override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
+        var attribute: UICollectionViewLayoutAttributes?
+
+        if elementKind == collectionViewMediaElementKindSectionHeader {
+            attribute = headersAttribute[indexPath.section]
+        } else if elementKind == collectionViewMediaElementKindSectionFooter {
+            attribute = footersAttribute[indexPath.section]
+        }
+
+        return attribute
+    }
+
+    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
+        var begin: Int = 0
+        var end: Int = unionRects.count
+        var attrs = [UICollectionViewLayoutAttributes]()
+
+        for i in 0..<unionRects.count {
+            if rect.intersects(unionRects[i]) {
+                begin = i * unionSize
+                break
+            }
+        }
+        for i in (0..<unionRects.count).reversed() {
+            if rect.intersects(unionRects[i]) {
+                end = min((i + 1) * unionSize, allItemAttributes.count)
+                break
+            }
+        }
+        for i in begin..<end {
+            let attr = allItemAttributes[i]
+            if rect.intersects(attr.frame) {
+                attrs.append(attr)
+            }
+        }
+
+        return Array(attrs)
+    }
+
+    public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
+        let oldBounds = collectionView?.bounds
+        if newBounds.width != oldBounds?.width {
+            return true
+        }
+
+        return false
+    }
+}
+
+// MARK: - Private Methods
+private extension NCMediaLayout {
+    func shortestColumnIndex() -> Int {
+        var index: Int = 0
+        var shortestHeight = MAXFLOAT
+
+        for (idx, height) in columnHeights.enumerated() {
+            if height < shortestHeight {
+                shortestHeight = height
+                index = idx
+            }
+        }
+
+        return index
+    }
+
+    func longestColumnIndex() -> Int {
+        var index: Int = 0
+        var longestHeight: Float = 0
+        for (idx, height) in columnHeights.enumerated() {
+            if height > longestHeight {
+                longestHeight = height
+                index = idx
+            }
+        }
+        return index
+    }
+
+    func invalidateIfNotEqual<T: Equatable>(_ oldValue: T, newValue: T) {
+        if oldValue != newValue {
+            invalidateLayout()
+        }
+    }
+}

+ 1 - 1
iOSClient/Menu/NCCollectionViewCommon+Menu.swift

@@ -180,7 +180,7 @@ extension NCCollectionViewCommon {
                         NextcloudKit.shared.markE2EEFolder(fileId: metadata.fileId, delete: true) { _, error in
                             if error == .success {
                                 NCManageDatabase.shared.deleteE2eEncryption(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", self.appDelegate.account, serverUrl))
-                                NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, serverUrlTo: nil, etag: nil, ocId: nil, fileId: nil, encrypted: false, richWorkspace: nil, account: metadata.account)
+                                NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, encrypted: false, account: metadata.account)
                                 NCManageDatabase.shared.setMetadataEncrypted(ocId: metadata.ocId, encrypted: false)
 
                                 NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeStatusFolderE2EE, userInfo: ["serverUrl": metadata.serverUrl])

+ 5 - 4
iOSClient/NCGlobal.swift

@@ -198,10 +198,6 @@ class NCGlobal: NSObject {
     //
     let fileNameRichWorkspace = "Readme.md"
 
-    // Extension
-    //
-    @objc let extensionPreview = "ico"
-
     // ContentPresenter
     //
     @objc let dismissAfterSecond: TimeInterval      = 4
@@ -352,6 +348,7 @@ class NCGlobal: NSObject {
     let notificationCenterReloadDataNCShare                     = "reloadDataNCShare"
     let notificationCenterCloseRichWorkspaceWebView             = "closeRichWorkspaceWebView"
     let notificationCenterReloadAvatar                          = "reloadAvatar"
+    let notificationCenterCreateMediaCacheEnded                 = "createMediaCacheEnded"
 
     @objc let notificationCenterReloadDataSource                = "reloadDataSource"
     let notificationCenterReloadDataSourceNetwork               = "reloadDataSourceNetwork"
@@ -501,4 +498,8 @@ class NCGlobal: NSObject {
     let diagnosticProblemsBadResponse       = "BAD_SERVER_RESPONSE"
     let diagnosticProblemsUploadServerError = "UploadError.SERVER_ERROR"
 
+    // MEDIA LAYOUT
+    //
+    let mediaLayoutRatio                    = "mediaLayoutRatio"
+    let mediaLayoutSquare                   = "mediaLayoutSquare"
 }

+ 88 - 38
iOSClient/NCImageCache.swift

@@ -35,41 +35,51 @@ import RealmSwift
     // MARK: -
 
     private let limit: Int = 1000
-    private var account: String = ""
     private var brandElementColor: UIColor?
-
-    enum ImageType {
-        case placeholder
-        case actual(_ image: UIImage)
-    }
+    private var totalSize: Int64 = 0
 
     struct metadataInfo {
         var etag: String
         var date: NSDate
+        var width: Int
+        var height: Int
+    }
+
+    struct imageInfo {
+        var image: UIImage?
+        var size: CGSize?
+        var date: Date
     }
 
-    private typealias ThumbnailLRUCache = LRUCache<String, ImageType>
-    private lazy var cache: ThumbnailLRUCache = {
-        return ThumbnailLRUCache(countLimit: limit)
+    private typealias ThumbnailImageLRUCache = LRUCache<String, imageInfo>
+    private typealias ThumbnailSizeLRUCache = LRUCache<String, CGSize?>
+
+    private lazy var cacheImage: ThumbnailImageLRUCache = {
+        return ThumbnailImageLRUCache(countLimit: limit)
+    }()
+    private lazy var cacheSize: ThumbnailSizeLRUCache = {
+        return ThumbnailSizeLRUCache()
     }()
     private var metadatasInfo: [String: metadataInfo] = [:]
     private var metadatas: ThreadSafeArray<tableMetadata>?
 
+    var createMediaCacheInProgress: Bool = false
     let showAllPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload')"
     let showBothPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')"
     let showOnlyPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND classFile == %@ AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')"
 
     override private init() {}
 
-    func createMediaCache(account: String) {
-
-        guard account != self.account, !account.isEmpty else { return }
-        self.account = account
+    @objc func createMediaCache(account: String, withCacheSize: Bool) {
+        if createMediaCacheInProgress {
+            NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] ThumbnailLRUCache image process already in progress")
+            return
+        }
+        createMediaCacheInProgress = true
 
         self.metadatasInfo.removeAll()
         self.metadatas = nil
         self.metadatas = getMediaMetadatas(account: account)
-        guard let metadatas = self.metadatas, !metadatas.isEmpty else { return }
         let ext = ".preview.ico"
         let manager = FileManager.default
         let resourceKeys = Set<URLResourceKey>([.nameKey, .pathKey, .fileSizeKey, .creationDateKey])
@@ -77,12 +87,17 @@ import RealmSwift
             var path: URL
             var ocIdEtag: String
             var date: Date
+            var fileSize: Int
+            var width: Int
+            var height: Int
         }
         var files: [FileInfo] = []
         let startDate = Date()
 
-        metadatas.forEach { metadata in
-            metadatasInfo[metadata.ocId] = metadataInfo(etag: metadata.etag, date: metadata.date)
+        if let metadatas = metadatas {
+            metadatas.forEach { metadata in
+                metadatasInfo[metadata.ocId] = metadataInfo(etag: metadata.etag, date: metadata.date, width: metadata.width, height: metadata.height)
+            }
         }
 
         if let enumerator = manager.enumerator(at: URL(fileURLWithPath: NCUtilityFileSystem().directoryProviderStorage), includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]) {
@@ -90,12 +105,24 @@ import RealmSwift
                 let fileName = fileURL.lastPathComponent
                 let ocId = fileURL.deletingLastPathComponent().lastPathComponent
                 guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys),
-                      let size = resourceValues.fileSize,
-                      size > 0,
-                      let date = metadatasInfo[ocId]?.date,
-                      let etag = metadatasInfo[ocId]?.etag,
-                      fileName == etag + ext else { continue }
-                files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: date as Date))
+                      let fileSize = resourceValues.fileSize,
+                      fileSize > 0 else { continue }
+                let width = metadatasInfo[ocId]?.width ?? 0
+                let height = metadatasInfo[ocId]?.height ?? 0
+                if withCacheSize {
+                    if let date = metadatasInfo[ocId]?.date,
+                       let etag = metadatasInfo[ocId]?.etag,
+                       fileName == etag + ext {
+                        files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: date as Date, fileSize: fileSize, width: width, height: height))
+                    } else {
+                        let etag = fileName.replacingOccurrences(of: ".preview.ico", with: "")
+                        files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: Date.distantPast, fileSize: fileSize, width: width, height: height))
+                    }
+                } else if let date = metadatasInfo[ocId]?.date, let etag = metadatasInfo[ocId]?.etag, fileName == etag + ext {
+                    files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: date as Date, fileSize: fileSize, width: width, height: height))
+                } else {
+                    print("Nothing")
+                }
             }
         }
 
@@ -105,23 +132,37 @@ import RealmSwift
             print("Last date: \(lastDate)")
         }
 
-        cache.removeAllValues()
+        cacheImage.removeAllValues()
+        cacheSize.removeAllValues()
         var counter: Int = 0
         for file in files {
-            counter += 1
-            if counter > (limit - 100) { break }
+            if !withCacheSize, counter > limit {
+                break
+            }
             autoreleasepool {
                 if let image = UIImage(contentsOfFile: file.path.path) {
-                    cache.setValue(.actual(image), forKey: file.ocIdEtag)
+                    if counter < limit {
+                        cacheImage.setValue(imageInfo(image: image, size: image.size, date: file.date), forKey: file.ocIdEtag)
+                        totalSize = totalSize + Int64(file.fileSize)
+                    }
+                    if file.width == 0, file.height == 0 {
+                        cacheSize.setValue(image.size, forKey: file.ocIdEtag)
+                    }
                 }
             }
+            counter += 1
         }
 
         let diffDate = Date().timeIntervalSinceReferenceDate - startDate.timeIntervalSinceReferenceDate
         NextcloudKit.shared.nkCommonInstance.writeLog("--------- ThumbnailLRUCache image process ---------")
-        NextcloudKit.shared.nkCommonInstance.writeLog("Counter process: \(cache.count)")
+        NextcloudKit.shared.nkCommonInstance.writeLog("Counter cache image: \(cacheImage.count)")
+        NextcloudKit.shared.nkCommonInstance.writeLog("Counter cache size: \(cacheSize.count)")
+        NextcloudKit.shared.nkCommonInstance.writeLog("Total size images process: " + NCUtilityFileSystem().transformedSize(totalSize))
         NextcloudKit.shared.nkCommonInstance.writeLog("Time process: \(diffDate)")
         NextcloudKit.shared.nkCommonInstance.writeLog("--------- ThumbnailLRUCache image process ---------")
+
+        createMediaCacheInProgress = false
+        NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateMediaCacheEnded)
     }
 
     func initialMetadatas() -> ThreadSafeArray<tableMetadata>? {
@@ -129,24 +170,33 @@ import RealmSwift
         return self.metadatas
     }
 
-    func getMediaImage(ocId: String, etag: String) -> ImageType? {
-        return cache.value(forKey: ocId + etag)
+    func setMediaImage(ocId: String, etag: String, image: UIImage, date: Date) {
+        cacheImage.setValue(imageInfo(image: image, size: image.size, date: date), forKey: ocId + etag)
     }
 
-    func setMediaImage(ocId: String, etag: String, image: ImageType) {
-        cache.setValue(image, forKey: ocId + etag)
+    func getMediaImage(ocId: String, etag: String) -> UIImage? {
+        if let cache = cacheImage.value(forKey: ocId + etag) {
+            return cache.image
+        }
+        return nil
     }
 
-    @objc func clearMediaCache() {
-        self.metadatasInfo.removeAll()
-        self.metadatas = nil
-        cache.removeAllValues()
+    func hasMediaImageEnoughSpace() -> Bool {
+        return limit > cacheImage.count
+    }
+
+    func setMediaSize(ocId: String, etag: String, size: CGSize) {
+        cacheSize.setValue(size, forKey: ocId + etag)
+    }
+
+    func getMediaSize(ocId: String, etag: String) -> CGSize? {
+        return cacheSize.value(forKey: ocId + etag) ?? nil
     }
 
     func getMediaMetadatas(account: String, predicate: NSPredicate? = nil) -> ThreadSafeArray<tableMetadata>? {
-        guard let account = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)) else { return nil }
-        let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: account.urlBase, userId: account.userId) + account.mediaPath
-        let predicateBoth = NSPredicate(format: showBothPredicateMediaString, account.account, startServerUrl)
+        guard let tableAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)) else { return nil }
+        let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: tableAccount.urlBase, userId: tableAccount.userId) + tableAccount.mediaPath
+        let predicateBoth = NSPredicate(format: showBothPredicateMediaString, account, startServerUrl)
         return NCManageDatabase.shared.getMediaMetadatas(predicate: predicate ?? predicateBoth)
     }
 

+ 3 - 3
iOSClient/Networking/E2EE/NCEndToEndMetadataV1.swift

@@ -259,7 +259,7 @@ extension NCEndToEndMetadata {
                                 object.key = encrypted.key
                                 object.initializationVector = initializationVector
                                 object.metadataKey = metadataKey
-                                object.metadataVersion = metadataVersion
+                                object.version = "\(metadataVersion)"
                                 object.mimeType = encrypted.mimetype
                                 object.serverUrl = serverUrl
 
@@ -317,7 +317,7 @@ extension NCEndToEndMetadata {
                                 object.key = encrypted.key
                                 object.initializationVector = filedrop.initializationVector
                                 object.metadataKey = metadataKey
-                                object.metadataVersion = metadataVersion
+                                object.version = "\(metadataVersion)"
                                 object.mimeType = encrypted.mimetype
                                 object.serverUrl = serverUrl
 
@@ -434,7 +434,7 @@ extension NCEndToEndMetadata {
                                 object.initializationVector = initializationVector
                                 object.metadataKey = metadataKey
                                 object.metadataKeyIndex = metadataKeyIndex
-                                object.metadataVersion = metadataVersion
+                                object.version = "\(metadataVersion)"
                                 object.mimeType = encrypted.mimetype
                                 object.serverUrl = serverUrl
 

+ 8 - 8
iOSClient/Networking/E2EE/NCEndToEndMetadataV20.swift

@@ -185,11 +185,11 @@ extension NCEndToEndMetadata {
         let e2eEncryptions = NCManageDatabase.shared.getE2eEncryptions(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", account, serverUrl))
 
         for e2eEncryption in e2eEncryptions {
-            if e2eEncryption.blob == "files" {
+            if e2eEncryption.mimeType == "httpd/unix-directory" {
+                folders[e2eEncryption.fileNameIdentifier] = e2eEncryption.fileName
+            } else {
                 let file = E2eeV20.Metadata.ciphertext.Files(authenticationTag: e2eEncryption.authenticationTag, filename: e2eEncryption.fileName, key: e2eEncryption.key, mimetype: e2eEncryption.mimeType, nonce: e2eEncryption.initializationVector)
                 filesCodable.updateValue(file, forKey: e2eEncryption.fileNameIdentifier)
-            } else if e2eEncryption.blob == "folders" {
-                folders[e2eEncryption.fileNameIdentifier] = e2eEncryption.fileName
             }
         }
 
@@ -234,20 +234,20 @@ extension NCEndToEndMetadata {
 
         let isDirectoryTop = utilityFileSystem.isDirectoryE2EETop(account: account, serverUrl: serverUrl)
 
-        func addE2eEncryption(fileNameIdentifier: String, filename: String, authenticationTag: String, key: String, initializationVector: String, metadataKey: String, mimetype: String, blob: String) {
+        func addE2eEncryption(fileNameIdentifier: String, filename: String, authenticationTag: String, key: String, initializationVector: String, metadataKey: String, mimetype: String) {
 
             if let metadata = NCManageDatabase.shared.getMetadata(predicate: NSPredicate(format: "account == %@ AND fileName == %@", account, fileNameIdentifier)) {
 
                 let object = tableE2eEncryption.init(account: account, ocIdServerUrl: ocIdServerUrl, fileNameIdentifier: fileNameIdentifier)
 
                 object.authenticationTag = authenticationTag
-                object.blob = blob
                 object.fileName = filename
                 object.key = key
                 object.initializationVector = initializationVector
                 object.metadataKey = metadataKey
                 object.mimeType = mimetype
                 object.serverUrl = serverUrl
+                object.version = NCGlobal.shared.e2eeVersionV20
 
                 // Write file parameter for decrypted on DB
                 NCManageDatabase.shared.addE2eEncryption(object)
@@ -327,7 +327,7 @@ extension NCEndToEndMetadata {
                             if let jsonText = String(data: data, encoding: .utf8) { print(jsonText) }
                             let file = try JSONDecoder().decode(E2eeV20.Metadata.ciphertext.Files.self, from: data)
                             print(file)
-                            addE2eEncryption(fileNameIdentifier: fileNameIdentifier, filename: file.filename, authenticationTag: file.authenticationTag, key: file.key, initializationVector: file.nonce, metadataKey: filedropKey, mimetype: file.mimetype, blob: "files")
+                            addE2eEncryption(fileNameIdentifier: fileNameIdentifier, filename: file.filename, authenticationTag: file.authenticationTag, key: file.key, initializationVector: file.nonce, metadataKey: filedropKey, mimetype: file.mimetype)
                         }
                     }
                 }
@@ -389,7 +389,7 @@ extension NCEndToEndMetadata {
             if let files = jsonCiphertextMetadata.files {
                 print("\nFILES ---------------------------------\n")
                 for file in files {
-                    addE2eEncryption(fileNameIdentifier: file.key, filename: file.value.filename, authenticationTag: file.value.authenticationTag, key: file.value.key, initializationVector: file.value.nonce, metadataKey: metadataKey, mimetype: file.value.mimetype, blob: "files")
+                    addE2eEncryption(fileNameIdentifier: file.key, filename: file.value.filename, authenticationTag: file.value.authenticationTag, key: file.value.key, initializationVector: file.value.nonce, metadataKey: metadataKey, mimetype: file.value.mimetype)
 
                     print("filename: \(file.value.filename)")
                     print("fileNameIdentifier: \(file.key)")
@@ -401,7 +401,7 @@ extension NCEndToEndMetadata {
             if let folders = jsonCiphertextMetadata.folders {
                 print("FOLDERS--------------------------------\n")
                 for folder in folders {
-                    addE2eEncryption(fileNameIdentifier: folder.key, filename: folder.value, authenticationTag: metadata.authenticationTag, key: metadataKey, initializationVector: metadata.nonce, metadataKey: metadataKey, mimetype: "httpd/unix-directory", blob: "folders")
+                    addE2eEncryption(fileNameIdentifier: folder.key, filename: folder.value, authenticationTag: metadata.authenticationTag, key: metadataKey, initializationVector: metadata.nonce, metadataKey: metadataKey, mimetype: "httpd/unix-directory")
 
                     print("filename: \(folder.value)")
                     print("fileNameIdentifier: \(folder.key)")

+ 89 - 29
iOSClient/Networking/E2EE/NCNetworkingE2EE.swift

@@ -24,6 +24,9 @@ import NextcloudKit
 
 class NCNetworkingE2EE: NSObject {
 
+    let e2EEApiVersion1 = "v1"
+    let e2EEApiVersion2 = "v2"
+
     func isInUpload(account: String, serverUrl: String) -> Bool {
 
         let counter = NCManageDatabase.shared.getMetadatas(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@ AND (status == %d OR status == %d)", account, serverUrl, NCGlobal.shared.metadataStatusWaitUpload, NCGlobal.shared.metadataStatusUploading)).count
@@ -40,11 +43,58 @@ class NCNetworkingE2EE: NSObject {
 
     func getOptions() -> NKRequestOptions {
 
-        let version = NCGlobal.shared.capabilityE2EEApiVersion == "2.0" ? "v2" : "v1"
+        let version = NCGlobal.shared.capabilityE2EEApiVersion == NCGlobal.shared.e2eeVersionV20 ? e2EEApiVersion2 : e2EEApiVersion1
         return NKRequestOptions(version: version)
     }
 
-    func uploadMetadata(account: String, serverUrl: String, userId: String, addUserId: String? = nil, removeUserId: String? = nil) async -> NKError {
+    // MARK: -
+
+    func getMetadata(fileId: String,
+                     e2eToken: String?,
+                     completion: @escaping (_ account: String, _ version: String?, _ e2eMetadata: String?, _ signature: String?, _ data: Data?, _ error: NKError) -> Void) {
+
+        switch NCGlobal.shared.capabilityE2EEApiVersion {
+        case NCGlobal.shared.e2eeVersionV11, NCGlobal.shared.e2eeVersionV12:
+            NextcloudKit.shared.getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: NKRequestOptions(version: e2EEApiVersion1)) { account, e2eMetadata, signature, data, error in
+                return completion(account, self.e2EEApiVersion1, e2eMetadata, signature, data, error)
+            }
+        case NCGlobal.shared.e2eeVersionV20:
+            var options = NKRequestOptions(version: e2EEApiVersion2)
+            NextcloudKit.shared.getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: options) { account, e2eMetadata, signature, data, error in
+                if error == .success {
+                    return completion(account, self.e2EEApiVersion2, e2eMetadata, signature, data, error)
+                } else if error.errorCode == NCGlobal.shared.errorResourceNotFound {
+                    return completion(account, self.e2EEApiVersion2, e2eMetadata, signature, data, error)
+                } else {
+                    options = NKRequestOptions(version: self.e2EEApiVersion1)
+                    NextcloudKit.shared.getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: options) { account, e2eMetadata, signature, data, error in
+                        completion(account, self.e2EEApiVersion1, e2eMetadata, signature, data, error)
+                    }
+                }
+            }
+        default:
+            completion("", "", nil, nil, nil, NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "version e2ee not available"))
+        }
+    }
+
+    func getMetadata(fileId: String,
+                     e2eToken: String?) async -> (account: String, version: String?, e2eMetadata: String?, signature: String?, data: Data?, error: NKError) {
+
+        await withUnsafeContinuation({ continuation in
+            getMetadata(fileId: fileId, e2eToken: e2eToken) { account, version, e2eMetadata, signature, data, error in
+                continuation.resume(returning: (account: account, version: version, e2eMetadata: e2eMetadata, signature: signature, data: data, error: error))
+            }
+        })
+    }
+
+    // MARK: -
+
+    func uploadMetadata(account: String,
+                        serverUrl: String,
+                        userId: String,
+                        addUserId: String? = nil,
+                        removeUserId: String? = nil,
+                        updateVersionV1V2: Bool = false) async -> NKError {
 
         var addCertificate: String?
         var method = "POST"
@@ -70,11 +120,15 @@ class NCNetworkingE2EE: NSObject {
 
         // METHOD
         //
-        let resultsGetE2EEMetadata = await NextcloudKit.shared.getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: NCNetworkingE2EE().getOptions())
-        if resultsGetE2EEMetadata.error == .success {
+        if updateVersionV1V2 {
             method = "PUT"
-        } else if resultsGetE2EEMetadata.error.errorCode != NCGlobal.shared.errorResourceNotFound {
-            return resultsGetE2EEMetadata.error
+        } else {
+            let resultsGetE2EEMetadata = await getMetadata(fileId: fileId, e2eToken: e2eToken)
+            if resultsGetE2EEMetadata.error == .success {
+                method = "PUT"
+            } else if resultsGetE2EEMetadata.error.errorCode != NCGlobal.shared.errorResourceNotFound {
+                return resultsGetE2EEMetadata.error
+            }
         }
 
         // UPLOAD METADATA
@@ -89,6 +143,7 @@ class NCNetworkingE2EE: NSObject {
                                                        addUserId: addUserId,
                                                        addCertificate: addCertificate,
                                                        removeUserId: removeUserId)
+
         guard uploadMetadataError == .success else {
             await unlock(account: account, serverUrl: serverUrl)
             return uploadMetadataError
@@ -101,28 +156,6 @@ class NCNetworkingE2EE: NSObject {
         return NKError()
     }
 
-    func downloadMetadata(account: String,
-                          serverUrl: String,
-                          urlBase: String,
-                          userId: String,
-                          fileId: String,
-                          e2eToken: String) async -> NKError {
-
-        let resultsGetE2EEMetadata = await NextcloudKit.shared.getE2EEMetadata(fileId: fileId, e2eToken: e2eToken, options: NCNetworkingE2EE().getOptions())
-        guard resultsGetE2EEMetadata.error == .success, let e2eMetadata = resultsGetE2EEMetadata.e2eMetadata else {
-            return resultsGetE2EEMetadata.error
-        }
-
-        let resultsDecodeMetadataError = NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: resultsGetE2EEMetadata.signature, serverUrl: serverUrl, account: account, urlBase: urlBase, userId: userId)
-        guard resultsDecodeMetadataError == .success else {
-            // Client Diagnostic
-            NCManageDatabase.shared.addDiagnostic(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors)
-            return resultsDecodeMetadataError
-        }
-
-        return NKError()
-    }
-
     func uploadMetadata(account: String,
                         serverUrl: String,
                         ocIdServerUrl: String,
@@ -155,7 +188,34 @@ class NCNetworkingE2EE: NSObject {
         return NKError()
     }
 
-    func lock(account: String, serverUrl: String) async -> (fileId: String?, e2eToken: String?, error: NKError) {
+    // MARK: -
+
+    func downloadMetadata(account: String,
+                          serverUrl: String,
+                          urlBase: String,
+                          userId: String,
+                          fileId: String,
+                          e2eToken: String) async -> NKError {
+
+        let resultsGetE2EEMetadata = await getMetadata(fileId: fileId, e2eToken: e2eToken)
+        guard resultsGetE2EEMetadata.error == .success, let e2eMetadata = resultsGetE2EEMetadata.e2eMetadata else {
+            return resultsGetE2EEMetadata.error
+        }
+
+        let resultsDecodeMetadataError = NCEndToEndMetadata().decodeMetadata(e2eMetadata, signature: resultsGetE2EEMetadata.signature, serverUrl: serverUrl, account: account, urlBase: urlBase, userId: userId)
+        guard resultsDecodeMetadataError == .success else {
+            // Client Diagnostic
+            NCManageDatabase.shared.addDiagnostic(account: account, issue: NCGlobal.shared.diagnosticIssueE2eeErrors)
+            return resultsDecodeMetadataError
+        }
+
+        return NKError()
+    }
+
+    // MARK: -
+
+    func lock(account: String,
+              serverUrl: String) async -> (fileId: String?, e2eToken: String?, error: NKError) {
 
         var e2eToken: String?
         var e2eCounter = "1"

+ 1 - 1
iOSClient/Networking/E2EE/NCNetworkingE2EECreateFolder.swift

@@ -153,7 +153,7 @@ class NCNetworkingE2EECreateFolder: NSObject {
         }
         let metadata = NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: true)
         NCManageDatabase.shared.addMetadata(metadata)
-        NCManageDatabase.shared.addDirectory(encrypted: true, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
+        NCManageDatabase.shared.addDirectory(e2eEncrypted: true, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
 
         NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateFolder, userInfo: ["ocId": ocId, "serverUrl": serverUrl, "account": account, "withPush": withPush])
 

+ 1 - 1
iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift

@@ -38,7 +38,7 @@ class NCNetworkingE2EEMarkFolder: NSObject {
         guard let metadata = NCManageDatabase.shared.addMetadata(NCManageDatabase.shared.convertFileToMetadata(file, isDirectoryE2EE: false)) else {
             return NKError(errorCode: NCGlobal.shared.errorUnexpectedResponseFromDB, errorDescription: "_e2e_error_")
         }
-        NCManageDatabase.shared.addDirectory(encrypted: true, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
+        NCManageDatabase.shared.addDirectory(e2eEncrypted: true, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrlFileName, account: metadata.account)
         NCManageDatabase.shared.deleteE2eEncryption(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, serverUrlFileName))
         if NCGlobal.shared.capabilityE2EEApiVersion == NCGlobal.shared.e2eeVersionV20 {
             NCManageDatabase.shared.updateCounterE2eMetadata(account: account, ocIdServerUrl: metadata.ocId, counter: 0)

+ 12 - 10
iOSClient/Networking/NCNetworking+Download.swift

@@ -113,19 +113,21 @@ extension NCNetworking {
         }) { _, etag, date, length, allHeaderFields, afError, error in
 
             var error = error
-            self.downloadRequest.removeValue(forKey: fileNameLocalPath)
-
             var dateLastModified: NSDate?
-            if let downloadTask = downloadTask {
-                if let header = allHeaderFields, let dateString = header["Last-Modified"] as? String {
-                    dateLastModified = NextcloudKit.shared.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz")
-                }
-                if afError?.isExplicitlyCancelledError ?? false {
-                    error = NKError(errorCode: NCGlobal.shared.errorRequestExplicityCancelled, errorDescription: "error request explicity cancelled")
+            self.downloadRequest.removeValue(forKey: fileNameLocalPath)
+            // this delay was added because for small file the "taskHandler: { task" is not called, so this part of code is not executed
+            NextcloudKit.shared.nkCommonInstance.backgroundQueue.asyncAfter(deadline: .now() + 0.5) {
+                if let downloadTask = downloadTask {
+                    if let header = allHeaderFields, let dateString = header["Last-Modified"] as? String {
+                        dateLastModified = NextcloudKit.shared.nkCommonInstance.convertDate(dateString, format: "EEE, dd MMM y HH:mm:ss zzz")
+                    }
+                    if afError?.isExplicitlyCancelledError ?? false {
+                        error = NKError(errorCode: NCGlobal.shared.errorRequestExplicityCancelled, errorDescription: "error request explicity cancelled")
+                    }
+                    self.downloadComplete(fileName: metadata.fileName, serverUrl: metadata.serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, fileNameLocalPath: fileNameLocalPath, task: downloadTask, error: error)
                 }
-                self.downloadComplete(fileName: metadata.fileName, serverUrl: metadata.serverUrl, etag: etag, date: date, dateLastModified: dateLastModified, length: length, fileNameLocalPath: fileNameLocalPath, task: downloadTask, error: error)
+                completion(afError, error)
             }
-            completion(afError, error)
         }
     }
 

+ 5 - 6
iOSClient/Networking/NCNetworking+WebDAV.swift

@@ -53,13 +53,12 @@ extension NCNetworking {
                 NCManageDatabase.shared.addMetadata(tableMetadata.init(value: metadataFolder))
 
                 // Update directory
-                NCManageDatabase.shared.addDirectory(encrypted: metadataFolder.e2eEncrypted, favorite: metadataFolder.favorite, ocId: metadataFolder.ocId, fileId: metadataFolder.fileId, etag: metadataFolder.etag, permissions: metadataFolder.permissions, serverUrl: serverUrl, account: metadataFolder.account)
-                NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, richWorkspace: metadataFolder.richWorkspace, account: metadataFolder.account)
+                NCManageDatabase.shared.addDirectory(e2eEncrypted: metadataFolder.e2eEncrypted, favorite: metadataFolder.favorite, ocId: metadataFolder.ocId, fileId: metadataFolder.fileId, etag: metadataFolder.etag, permissions: metadataFolder.permissions, richWorkspace: metadataFolder.richWorkspace, serverUrl: serverUrl, account: metadataFolder.account)
 
                 // Update sub directories NO Update richWorkspace
                 for metadata in metadatasFolder {
                     let serverUrl = metadata.serverUrl + "/" + metadata.fileName
-                    NCManageDatabase.shared.addDirectory(encrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrl, account: account)
+                    NCManageDatabase.shared.addDirectory(e2eEncrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrl, account: account)
                 }
 
 #if !EXTENSION
@@ -255,7 +254,7 @@ extension NCNetworking {
                 if error == .success {
                     if let metadata = metadataFolder {
                         NCManageDatabase.shared.addMetadata(metadata)
-                        NCManageDatabase.shared.addDirectory(encrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: fileNameFolderUrl, account: account)
+                        NCManageDatabase.shared.addDirectory(e2eEncrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: fileNameFolderUrl, account: account)
                     }
                     if let metadata = NCManageDatabase.shared.getMetadataFromOcId(metadataFolder?.ocId) {
                         NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateFolder, userInfo: ["ocId": metadata.ocId, "serverUrl": metadata.serverUrl, "account": metadata.account, "withPush": withPush])
@@ -517,7 +516,7 @@ extension NCNetworking {
                     let serverUrl = self.utilityFileSystem.stringAppendServerUrl(metadata.serverUrl, addFileName: metadata.fileName)
                     let serverUrlTo = self.utilityFileSystem.stringAppendServerUrl(metadata.serverUrl, addFileName: fileNameNew)
                     if let directory = NCManageDatabase.shared.getTableDirectory(predicate: NSPredicate(format: "account == %@ AND serverUrl == %@", metadata.account, metadata.serverUrl)) {
-                        NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, serverUrlTo: serverUrlTo, etag: "", ocId: nil, fileId: nil, encrypted: directory.e2eEncrypted, richWorkspace: nil, account: metadata.account)
+                        NCManageDatabase.shared.setDirectory(serverUrl: serverUrl, serverUrlTo: serverUrlTo, etag: "", encrypted: directory.e2eEncrypted, account: metadata.account)
                     }
                 } else {
                     if (metadata.fileName as NSString).pathExtension != (fileNameNew as NSString).pathExtension {
@@ -749,7 +748,7 @@ extension NCNetworking {
                 // Update sub directories
                 for folder in metadatasFolder {
                     let serverUrl = folder.serverUrl + "/" + folder.fileName
-                    NCManageDatabase.shared.addDirectory(encrypted: folder.e2eEncrypted, favorite: folder.favorite, ocId: folder.ocId, fileId: folder.fileId, etag: nil, permissions: folder.permissions, serverUrl: serverUrl, account: account)
+                    NCManageDatabase.shared.addDirectory(e2eEncrypted: folder.e2eEncrypted, favorite: folder.favorite, ocId: folder.ocId, fileId: folder.fileId, permissions: folder.permissions, serverUrl: serverUrl, account: account)
                 }
 
                 NCManageDatabase.shared.addMetadatas(metadatas)

+ 1 - 1
iOSClient/Recent/NCRecent.swift

@@ -142,7 +142,7 @@ class NCRecent: NCCollectionViewCommon {
                     // Update sub directories
                     for metadata in metadatasFolder {
                         let serverUrl = metadata.serverUrl + "/" + metadata.fileName
-                        NCManageDatabase.shared.addDirectory(encrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, etag: nil, permissions: metadata.permissions, serverUrl: serverUrl, account: account)
+                        NCManageDatabase.shared.addDirectory(e2eEncrypted: metadata.e2eEncrypted, favorite: metadata.favorite, ocId: metadata.ocId, fileId: metadata.fileId, permissions: metadata.permissions, serverUrl: serverUrl, account: account)
                     }
                     // Add metadatas
                     NCManageDatabase.shared.addMetadatas(metadatas)

+ 1 - 1
iOSClient/Settings/CCAdvanced.m

@@ -375,7 +375,7 @@
 
         [[NCAutoUpload shared] alignPhotoLibraryWithViewController:self];
 
-        [[NCImageCache shared] clearMediaCache];
+        [[NCImageCache shared] createMediaCacheWithAccount:appDelegate.account withCacheSize:true];
 
         [[NCActivityIndicator shared] stop];
         [self calculateSize];

+ 41 - 17
iOSClient/Settings/NCKeychain.swift

@@ -28,6 +28,18 @@ import KeychainAccess
 
     let keychain = Keychain(service: "com.nextcloud.keychain")
 
+    var showDescription: Bool {
+        get {
+            if let value = try? keychain.get("showDescription"), let result = Bool(value) {
+                return result
+            }
+            return true
+        }
+        set {
+            keychain["showDescription"] = String(newValue)
+        }
+    }
+
     var typeFilterScanDocument: NCGlobal.TypeFilterScanDocument {
         get {
             if let rawValue = try? keychain.get("ScanDocumentTypeFilter"), let value = NCGlobal.TypeFilterScanDocument(rawValue: rawValue) {
@@ -267,15 +279,27 @@ import KeychainAccess
         }
     }
 
-    var mediaItemForLine: Int {
+    var mediaColumnCount: Int {
         get {
-            if let value = try? keychain.get("itemForLine"), let result = Int(value) {
+            if let value = try? keychain.get("mediaColumnCount"), let result = Int(value) {
                 return result
             }
             return 3
         }
         set {
-            keychain["itemForLine"] = String(newValue)
+            keychain["mediaColumnCount"] = String(newValue)
+        }
+    }
+
+    var mediaTypeLayout: String {
+        get {
+            if let value = try? keychain.get("mediaTypeLayout") {
+                return value
+            }
+            return NCGlobal.shared.mediaLayoutRatio
+        }
+        set {
+            keychain["mediaTypeLayout"] = String(newValue)
         }
     }
 
@@ -320,20 +344,6 @@ import KeychainAccess
 
     // MARK: -
 
-    private func migrate(key: String) {
-        let keychainOLD = Keychain(service: "Crypto Cloud")
-        if let value = keychainOLD[key], !value.isEmpty {
-            keychain[key] = value
-            keychainOLD[key] = nil
-        }
-    }
-
-    @objc func removeAll() {
-        try? keychain.removeAll()
-    }
-
-    // MARK: -
-
     @objc func getPassword(account: String) -> String {
         let key = "password" + account
         migrate(key: key)
@@ -515,4 +525,18 @@ import KeychainAccess
         setPushNotificationDeviceIdentifier(account: account, deviceIdentifier: nil)
         setPushNotificationDeviceIdentifierSignature(account: account, deviceIdentifierSignature: nil)
     }
+
+    // MARK: -
+
+    private func migrate(key: String) {
+        let keychainOLD = Keychain(service: "Crypto Cloud")
+        if let value = keychainOLD[key], !value.isEmpty {
+            keychain[key] = value
+            keychainOLD[key] = nil
+        }
+    }
+
+    @objc func removeAll() {
+        try? keychain.removeAll()
+    }
 }

二進制
iOSClient/Supporting Files/af.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/an.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/ar.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/ast.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/az.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/be.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/bg_BG.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/bn_BD.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/br.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/bs.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/ca.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/cs-CZ.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/cy_GB.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/da.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/de.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/el.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/en-GB.lproj/Localizable.strings


+ 4 - 0
iOSClient/Supporting Files/en.lproj/Localizable.strings

@@ -562,6 +562,8 @@
 "_file_saved_cameraroll_"       = "File saved in photo album";
 "_directory_on_top_yes_"        = "✓ Folders on top";
 "_directory_on_top_no_"         = "Folders on top";
+"_show_description_"            = "Show description";
+"_no_description_available_"    = "No description available for this folder";
 "_folder_automatic_upload_"     = "Folder for \"Auto upload\"";
 "_search_no_record_found_"      = "No result";
 "_search_in_progress_"          = "Search in progress …";
@@ -967,6 +969,8 @@
 "_selected_photo_"          = "selected photo";
 "_selected_photos_"         = "selected photos";
 "_delete_selected_photos_"  = "Delete selected photos";
+"_media_square_"            = "Square photo grid";
+"_media_ratio_"             = "Aspect ratio grid";
 
 // Video
 "_select_trace_"            = "Select the trace";

二進制
iOSClient/Supporting Files/eo.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-419.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-AR.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-CL.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-CO.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-CR.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-DO.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-EC.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-GT.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-HN.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-MX.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-NI.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-PA.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-PE.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-PR.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-PY.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-SV.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es-UY.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/es.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/et_EE.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/eu.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/fa.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/fi-FI.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/fo.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/fr.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/gd.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/gl.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/he.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/hi_IN.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/hr.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/hsb.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/hu.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/hy.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/ia.lproj/Localizable.strings


二進制
iOSClient/Supporting Files/id.lproj/Localizable.strings


部分文件因文件數量過多而無法顯示