NCImageCache.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. //
  2. // NCImageCache.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 18/10/23.
  6. // Copyright © 2021 Marino Faggiana. All rights reserved.
  7. //
  8. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  9. //
  10. // This program is free software: you can redistribute it and/or modify
  11. // it under the terms of the GNU General Public License as published by
  12. // the Free Software Foundation, either version 3 of the License, or
  13. // (at your option) any later version.
  14. //
  15. // This program is distributed in the hope that it will be useful,
  16. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. // GNU General Public License for more details.
  19. //
  20. // You should have received a copy of the GNU General Public License
  21. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. //
  23. import UIKit
  24. import LRUCache
  25. import NextcloudKit
  26. import RealmSwift
  27. class NCImageCache: NSObject {
  28. public static let shared: NCImageCache = {
  29. let instance = NCImageCache()
  30. return instance
  31. }()
  32. // MARK: -
  33. private let limitCacheImagePreview: Int = 1000
  34. private let limitSizeImagePreview: Int = 100000
  35. private let limitSizeImageIcon: Int = 100000
  36. private var brandElementColor: UIColor?
  37. private var totalSize: Int64 = 0
  38. struct metadataInfo {
  39. var etag: String
  40. var date: NSDate
  41. var width: Int
  42. var height: Int
  43. }
  44. struct imageInfo {
  45. var image: UIImage?
  46. var size: CGSize?
  47. var date: Date
  48. }
  49. private typealias ThumbnailImagePreviewLRUCache = LRUCache<String, imageInfo>
  50. private typealias ThumbnailImageIconLRUCache = LRUCache<String, UIImage>
  51. private typealias ThumbnailSizePreviewLRUCache = LRUCache<String, CGSize?>
  52. private lazy var cacheImagePreview: ThumbnailImagePreviewLRUCache = {
  53. return ThumbnailImagePreviewLRUCache(countLimit: limitCacheImagePreview)
  54. }()
  55. private lazy var cacheImageIcon: ThumbnailImageIconLRUCache = {
  56. return ThumbnailImageIconLRUCache()
  57. }()
  58. private lazy var cacheSizePreview: ThumbnailSizePreviewLRUCache = {
  59. return ThumbnailSizePreviewLRUCache()
  60. }()
  61. private var metadatasInfo: [String: metadataInfo] = [:]
  62. private var metadatas: ThreadSafeArray<tableMetadata>?
  63. var createMediaCacheInProgress: Bool = false
  64. let showAllPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND (classFile == '\(NKCommon.TypeClassFile.image.rawValue)' OR classFile == '\(NKCommon.TypeClassFile.video.rawValue)') AND NOT (session CONTAINS[c] 'upload')"
  65. 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)')"
  66. let showOnlyPredicateMediaString = "account == %@ AND serverUrl BEGINSWITH %@ AND classFile == %@ AND NOT (session CONTAINS[c] 'upload') AND NOT (livePhotoFile != '' AND classFile == '\(NKCommon.TypeClassFile.video.rawValue)')"
  67. override private init() {}
  68. ///
  69. /// MEDIA CACHE
  70. ///
  71. func createMediaCache(account: String, withCacheSize: Bool) {
  72. if createMediaCacheInProgress {
  73. NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] ThumbnailLRUCache image process already in progress")
  74. return
  75. }
  76. createMediaCacheInProgress = true
  77. self.metadatasInfo.removeAll()
  78. self.metadatas = nil
  79. self.metadatas = getMediaMetadatas(account: account)
  80. let manager = FileManager.default
  81. let resourceKeys = Set<URLResourceKey>([.nameKey, .pathKey, .fileSizeKey, .creationDateKey])
  82. struct FileInfo {
  83. var path: URL
  84. var ocIdEtag: String
  85. var date: Date
  86. var fileSize: Int
  87. var width: Int
  88. var height: Int
  89. }
  90. var files: [FileInfo] = []
  91. let startDate = Date()
  92. if let metadatas = metadatas {
  93. metadatas.forEach { metadata in
  94. metadatasInfo[metadata.ocId] = metadataInfo(etag: metadata.etag, date: metadata.date, width: metadata.width, height: metadata.height)
  95. }
  96. }
  97. if let enumerator = manager.enumerator(at: URL(fileURLWithPath: NCUtilityFileSystem().directoryProviderStorage), includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles]) {
  98. for case let fileURL as URL in enumerator where fileURL.lastPathComponent.hasSuffix(NCGlobal.shared.storageExtPreview) {
  99. let fileName = fileURL.lastPathComponent
  100. let ocId = fileURL.deletingLastPathComponent().lastPathComponent
  101. guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys),
  102. let fileSize = resourceValues.fileSize,
  103. fileSize > 0 else { continue }
  104. let width = metadatasInfo[ocId]?.width ?? 0
  105. let height = metadatasInfo[ocId]?.height ?? 0
  106. if withCacheSize {
  107. if let date = metadatasInfo[ocId]?.date,
  108. let etag = metadatasInfo[ocId]?.etag,
  109. fileName == etag + NCGlobal.shared.storageExtPreview {
  110. files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: date as Date, fileSize: fileSize, width: width, height: height))
  111. } else {
  112. let etag = fileName.replacingOccurrences(of: NCGlobal.shared.storageExtPreview, with: "")
  113. files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: Date.distantPast, fileSize: fileSize, width: width, height: height))
  114. }
  115. } else if let date = metadatasInfo[ocId]?.date, let etag = metadatasInfo[ocId]?.etag, fileName == etag + NCGlobal.shared.storageExtPreview {
  116. files.append(FileInfo(path: fileURL, ocIdEtag: ocId + etag, date: date as Date, fileSize: fileSize, width: width, height: height))
  117. }
  118. }
  119. }
  120. files.sort(by: { $0.date > $1.date })
  121. if let firstDate = files.first?.date, let lastDate = files.last?.date {
  122. print("First date: \(firstDate)")
  123. print("Last date: \(lastDate)")
  124. }
  125. cacheImagePreview.removeAllValues()
  126. cacheSizePreview.removeAllValues()
  127. var counter: Int = 0
  128. for file in files {
  129. if !withCacheSize, counter > limitCacheImagePreview {
  130. break
  131. }
  132. autoreleasepool {
  133. if let image = UIImage(contentsOfFile: file.path.path) {
  134. if counter < limitCacheImagePreview, file.fileSize > limitSizeImagePreview {
  135. cacheImagePreview.setValue(imageInfo(image: image, size: image.size, date: file.date), forKey: file.ocIdEtag)
  136. totalSize = totalSize + Int64(file.fileSize)
  137. counter += 1
  138. }
  139. if file.width == 0, file.height == 0 {
  140. cacheSizePreview.setValue(image.size, forKey: file.ocIdEtag)
  141. }
  142. }
  143. }
  144. }
  145. let diffDate = Date().timeIntervalSinceReferenceDate - startDate.timeIntervalSinceReferenceDate
  146. NextcloudKit.shared.nkCommonInstance.writeLog("--------- ThumbnailLRUCache image process ---------")
  147. NextcloudKit.shared.nkCommonInstance.writeLog("Counter cache image: \(cacheImagePreview.count)")
  148. NextcloudKit.shared.nkCommonInstance.writeLog("Counter cache size: \(cacheSizePreview.count)")
  149. NextcloudKit.shared.nkCommonInstance.writeLog("Total size images process: " + NCUtilityFileSystem().transformedSize(totalSize))
  150. NextcloudKit.shared.nkCommonInstance.writeLog("Time process: \(diffDate)")
  151. NextcloudKit.shared.nkCommonInstance.writeLog("--------- ThumbnailLRUCache image process ---------")
  152. createMediaCacheInProgress = false
  153. NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterCreateMediaCacheEnded)
  154. }
  155. func initialMetadatas() -> ThreadSafeArray<tableMetadata>? {
  156. defer { self.metadatas = nil }
  157. return self.metadatas
  158. }
  159. ///
  160. /// MEDIA PREVIEW CACHE
  161. ///
  162. func setMediaImage(ocId: String, etag: String, image: UIImage, date: Date) {
  163. cacheImagePreview.setValue(imageInfo(image: image, size: image.size, date: date), forKey: ocId + etag)
  164. }
  165. func getMediaImage(ocId: String, etag: String) -> UIImage? {
  166. if let cache = cacheImagePreview.value(forKey: ocId + etag) {
  167. return cache.image
  168. }
  169. return nil
  170. }
  171. func hasMediaImageEnoughSpace() -> Bool {
  172. return limitCacheImagePreview > cacheImagePreview.count
  173. }
  174. func hasMediaImageEnoughSize(_ size: Int64) -> Bool {
  175. return limitSizeImagePreview < size
  176. }
  177. ///
  178. /// MEDIA SIZE CACHE
  179. ///
  180. func setMediaSize(ocId: String, etag: String, size: CGSize) {
  181. cacheSizePreview.setValue(size, forKey: ocId + etag)
  182. }
  183. func getMediaSize(ocId: String, etag: String) -> CGSize? {
  184. return cacheSizePreview.value(forKey: ocId + etag) ?? nil
  185. }
  186. func getMediaMetadatas(account: String, predicate: NSPredicate? = nil) -> ThreadSafeArray<tableMetadata>? {
  187. guard let tableAccount = NCManageDatabase.shared.getAccount(predicate: NSPredicate(format: "account == %@", account)) else { return nil }
  188. let startServerUrl = NCUtilityFileSystem().getHomeServer(urlBase: tableAccount.urlBase, userId: tableAccount.userId) + tableAccount.mediaPath
  189. let predicateBoth = NSPredicate(format: showBothPredicateMediaString, account, startServerUrl)
  190. return NCManageDatabase.shared.getMediaMetadatas(predicate: predicate ?? predicateBoth)
  191. }
  192. ///
  193. /// ICON CACHE
  194. ///
  195. func hasIconImageEnoughSize(_ size: Int64) -> Bool {
  196. return limitSizeImageIcon < size
  197. }
  198. func setIconImage(ocId: String, etag: String, image: UIImage) {
  199. cacheImageIcon.setValue(image, forKey: ocId + etag)
  200. }
  201. func getIconImage(ocId: String, etag: String) -> UIImage? {
  202. return cacheImageIcon.value(forKey: ocId + etag)
  203. }
  204. // MARK: -
  205. struct images {
  206. static var file = UIImage()
  207. static var shared = UIImage()
  208. static var canShare = UIImage()
  209. static var shareByLink = UIImage()
  210. static var favorite = UIImage()
  211. static var comment = UIImage()
  212. static var livePhoto = UIImage()
  213. static var offlineFlag = UIImage()
  214. static var local = UIImage()
  215. static var folderEncrypted = UIImage()
  216. static var folderSharedWithMe = UIImage()
  217. static var folderPublic = UIImage()
  218. static var folderGroup = UIImage()
  219. static var folderExternal = UIImage()
  220. static var folderAutomaticUpload = UIImage()
  221. static var folder = UIImage()
  222. static var checkedYes = UIImage()
  223. static var checkedNo = UIImage()
  224. static var buttonMore = UIImage()
  225. static var buttonStop = UIImage()
  226. static var buttonMoreLock = UIImage()
  227. static var iconContacts = UIImage()
  228. static var iconTalk = UIImage()
  229. static var iconCalendar = UIImage()
  230. static var iconDeck = UIImage()
  231. static var iconMail = UIImage()
  232. static var iconConfirm = UIImage()
  233. static var iconPages = UIImage()
  234. static var iconFile = UIImage()
  235. }
  236. func createImagesCache() {
  237. let utility = NCUtility()
  238. images.file = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor2])
  239. images.shared = utility.loadImage(named: "person.fill.badge.plus", colors: NCBrandColor.shared.iconImageMultiColors)
  240. images.canShare = utility.loadImage(named: "person.fill.badge.plus", colors: NCBrandColor.shared.iconImageMultiColors)
  241. images.shareByLink = utility.loadImage(named: "link", colors: [NCBrandColor.shared.iconImageColor])
  242. images.favorite = utility.loadImage(named: "star.fill", colors: [NCBrandColor.shared.yellowFavorite])
  243. images.livePhoto = utility.loadImage(named: "livephoto", colors: [NCBrandColor.shared.iconImageColor])
  244. images.offlineFlag = utility.loadImage(named: "arrow.down.circle.fill", colors: [.systemGreen])
  245. images.local = utility.loadImage(named: "checkmark.circle.fill", colors: [.systemGreen])
  246. images.checkedYes = utility.loadImage(named: "checkmark.circle.fill", colors: [NCBrandColor.shared.brandElement])
  247. images.checkedNo = utility.loadImage(named: "circle", colors: [NCBrandColor.shared.brandElement])
  248. images.buttonMore = utility.loadImage(named: "ellipsis", colors: [NCBrandColor.shared.iconImageColor])
  249. images.buttonStop = utility.loadImage(named: "stop.circle", colors: [NCBrandColor.shared.iconImageColor])
  250. images.buttonMoreLock = utility.loadImage(named: "lock.fill", colors: [NCBrandColor.shared.iconImageColor])
  251. createImagesBrandCache()
  252. }
  253. func createImagesBrandCache() {
  254. let brandElement = NCBrandColor.shared.brandElement
  255. guard brandElement != self.brandElementColor else { return }
  256. self.brandElementColor = brandElement
  257. let utility = NCUtility()
  258. images.folderEncrypted = UIImage(named: "folderEncrypted")!.image(color: brandElement)
  259. images.folderSharedWithMe = UIImage(named: "folder_shared_with_me")!.image(color: brandElement)
  260. images.folderPublic = UIImage(named: "folder_public")!.image(color: brandElement)
  261. images.folderGroup = UIImage(named: "folder_group")!.image(color: brandElement)
  262. images.folderExternal = UIImage(named: "folder_external")!.image(color: brandElement)
  263. images.folderAutomaticUpload = UIImage(named: "folderAutomaticUpload")!.image(color: brandElement)
  264. images.folder = UIImage(named: "folder")!.image(color: brandElement)
  265. images.iconContacts = utility.loadImage(named: "person.crop.rectangle.stack", colors: [NCBrandColor.shared.iconImageColor])
  266. images.iconTalk = UIImage(named: "talk-template")!.image(color: brandElement)
  267. images.iconCalendar = utility.loadImage(named: "calendar", colors: [NCBrandColor.shared.iconImageColor])
  268. images.iconDeck = utility.loadImage(named: "square.stack.fill", colors: [NCBrandColor.shared.iconImageColor])
  269. images.iconMail = utility.loadImage(named: "mail", colors: [NCBrandColor.shared.iconImageColor])
  270. images.iconConfirm = utility.loadImage(named: "arrow.right", colors: [NCBrandColor.shared.iconImageColor])
  271. images.iconPages = utility.loadImage(named: "doc.richtext", colors: [NCBrandColor.shared.iconImageColor])
  272. images.iconFile = utility.loadImage(named: "doc", colors: [NCBrandColor.shared.iconImageColor])
  273. NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterChangeTheming)
  274. }
  275. }