diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 1c48742422..9d36a485de 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -709,6 +709,8 @@ F7A03E2F2D425A14007AA677 /* NCFavoriteNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A03E2E2D425A14007AA677 /* NCFavoriteNavigationController.swift */; }; F7A03E332D426115007AA677 /* NCMoreNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A03E322D426115007AA677 /* NCMoreNavigationController.swift */; }; F7A03E352D427312007AA677 /* NCMainNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A03E342D427308007AA677 /* NCMainNavigationController.swift */; }; + F7A08A7A3017439900470AD3 /* NCMedia+ScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A08A793017438800470AD3 /* NCMedia+ScrollViewDelegate.swift */; }; + F7A08A7C3017441D00470AD3 /* NCMedia+SelectDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A08A7B3017440C00470AD3 /* NCMedia+SelectDelegate.swift */; }; F7A0D1352591FBC5008F8A13 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7A0D1362591FBC5008F8A13 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7A1050E29E587AF00FFD92B /* TagListView in Frameworks */ = {isa = PBXBuildFile; productRef = F7A1050D29E587AF00FFD92B /* TagListView */; }; @@ -1696,6 +1698,8 @@ F7A03E2E2D425A14007AA677 /* NCFavoriteNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFavoriteNavigationController.swift; sourceTree = ""; }; F7A03E322D426115007AA677 /* NCMoreNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMoreNavigationController.swift; sourceTree = ""; }; F7A03E342D427308007AA677 /* NCMainNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCMainNavigationController.swift; sourceTree = ""; }; + F7A08A793017438800470AD3 /* NCMedia+ScrollViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+ScrollViewDelegate.swift"; sourceTree = ""; }; + F7A08A7B3017440C00470AD3 /* NCMedia+SelectDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCMedia+SelectDelegate.swift"; sourceTree = ""; }; F7A0D1342591FBC5008F8A13 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; F7A3DB8F2DDE238C008F7EC8 /* NCDebouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDebouncer.swift; sourceTree = ""; }; F7A48414297028FC00BD1B49 /* Nextcloud Hub.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Nextcloud Hub.png"; sourceTree = SOURCE_ROOT; }; @@ -3465,6 +3469,8 @@ F72408322B8A27C900F128E2 /* NCMedia+Command.swift */, F7802B312BD5584F00D74270 /* NCMedia+DragDrop.swift */, F7BD0A032C4689E9003A4A6D /* NCMedia+MediaLayout.swift */, + F7A08A793017438800470AD3 /* NCMedia+ScrollViewDelegate.swift */, + F7A08A7B3017440C00470AD3 /* NCMedia+SelectDelegate.swift */, F7E2B64E2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift */, F78B87E62B62527100C65ADC /* NCMediaDataSource.swift */, F755CB3F2B8CB13C00CE27E9 /* NCMediaLayout.swift */, @@ -4931,6 +4937,7 @@ F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, F7132C722D085AD200B42D6A /* NCTermOfServiceModel.swift in Sources */, F7132C732D085AD200B42D6A /* NCTermOfServiceView.swift in Sources */, + F7A08A7C3017441D00470AD3 /* NCMedia+SelectDelegate.swift in Sources */, F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, F70898692EDDB51700EF85BD /* NCSelectOpen+SelectDelegate.swift in Sources */, @@ -5075,6 +5082,7 @@ F70CAE3A1F8CF31A008125FD /* NCEndToEndEncryption.m in Sources */, AA8D316E2D4123B200FE2775 /* NCShareDownloadLimitTableViewControllerDelegate.swift in Sources */, F36C514F2E89393C0097E5F7 /* UIView+BlurVibrancy.swift in Sources */, + F7A08A7A3017439900470AD3 /* NCMedia+ScrollViewDelegate.swift in Sources */, AA8D316F2D4123B200FE2775 /* NCShareDownloadLimitTableViewController.swift in Sources */, F78448BE2FB1C33B00F2909A /* NCVideoVLCViewController.swift in Sources */, AA8D31702D4123B200FE2775 /* DownloadLimitViewModel.swift in Sources */, diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 70ef10ed1e..55e5b0092d 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -302,45 +302,38 @@ extension NCActivity: UITableViewDataSource { cell.configureAvatarMenu() let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: activity.user) - if let image = NCImageCache.shared.getImageCache(key: fileName) { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { cell.avatar?.image = image - } else { - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if let image = UIImage(contentsOfFile: fileNameLocalPath) { - cell.avatar?.image = image - NCImageCache.shared.addImageCache(image: image, key: fileName) - } - - let user = activity.user - let idActivity = activity.idActivity - let account = session.account + } + let user = activity.user + let idActivity = activity.idActivity + let account = session.account - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard - let cell = self.tableView.cellForRow(at: indexPath) as? NCActivityTableViewCell, - cell.idActivity == idActivity else { - return - } - cell.avatar?.image = image + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard + let cell = self.tableView.cellForRow(at: indexPath) as? NCActivityTableViewCell, + cell.idActivity == idActivity else { + return } + cell.avatar?.image = image } } } diff --git a/iOSClient/Favorites/NCFavorite.swift b/iOSClient/Favorites/NCFavorite.swift index 4a020f53af..538232bbaf 100644 --- a/iOSClient/Favorites/NCFavorite.swift +++ b/iOSClient/Favorites/NCFavorite.swift @@ -65,8 +65,6 @@ class NCFavorite: NCCollectionViewCommon { layoutForView: layoutForView, account: session.account) await super.reloadDataSource() - - cachingAsync(metadatas: metadatas) } override func getServerData(forced: Bool = false) async { diff --git a/iOSClient/Files/NCFiles.swift b/iOSClient/Files/NCFiles.swift index a5d7d6a7fa..16e6c0ff9e 100644 --- a/iOSClient/Files/NCFiles.swift +++ b/iOSClient/Files/NCFiles.swift @@ -152,8 +152,6 @@ class NCFiles: NCCollectionViewCommon { layoutForView: layoutForView, account: session.account) await super.reloadDataSource() - - cachingAsync(metadatas: metadatas) } override func getServerData(forced: Bool = false) async { diff --git a/iOSClient/Groupfolders/NCGroupfolders.swift b/iOSClient/Groupfolders/NCGroupfolders.swift index 5709711ddd..101275d45a 100644 --- a/iOSClient/Groupfolders/NCGroupfolders.swift +++ b/iOSClient/Groupfolders/NCGroupfolders.swift @@ -83,8 +83,6 @@ class NCGroupfolders: NCCollectionViewCommon { layoutForView: layoutForView, account: session.account) await super.reloadDataSource() - - cachingAsync(metadatas: metadatas) } override func getServerData(forced: Bool = false) async { diff --git a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift index ca8b5ce2e6..400a8f634b 100644 --- a/iOSClient/Main/Collection Common/Cell/NCCellMain.swift +++ b/iOSClient/Main/Collection Common/Cell/NCCellMain.swift @@ -179,34 +179,29 @@ extension NCCollectionViewCommon { if let user = getAvatarFromIconUrl(metadata: metadata) { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: user) let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if let image = NCImageCache.shared.getImageCache(key: fileName) { - cell.previewImg?.image = image - } else { - let account = metadata.account - let ocId = metadata.ocId - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard cell.metadata?.ocId == ocId else { - return - } - cell.previewImg?.image = image + let account = metadata.account + let ocId = metadata.ocId + + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard cell.metadata?.ocId == ocId else { + return } + cell.previewImg?.image = image } } } diff --git a/iOSClient/Main/Collection Common/Cell/NCListCell.swift b/iOSClient/Main/Collection Common/Cell/NCListCell.swift index 850f1c17fe..fde6c658dc 100755 --- a/iOSClient/Main/Collection Common/Cell/NCListCell.swift +++ b/iOSClient/Main/Collection Common/Cell/NCListCell.swift @@ -501,45 +501,38 @@ extension NCCollectionViewCommon { // AVATAR if !metadata.ownerId.isEmpty, metadata.ownerId != metadata.userId { let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: metadata.ownerId) - if let image = NCImageCache.shared.getImageCache(key: fileName) { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { cell.setSharedAvatarImage(image) - } else { - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if let image = UIImage(contentsOfFile: fileNameLocalPath) { - cell.setSharedAvatarImage(image) - NCImageCache.shared.addImageCache(image: image, key: fileName) - } - - let user = metadata.ownerId - let ocId = metadata.ocId - let account = metadata.account - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard - let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, - cell.metadata?.ocId == ocId else { - return - } - cell.setSharedAvatarImage(image) + } + let user = metadata.ownerId + let ocId = metadata.ocId + let account = metadata.account + + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard + let cell = self.collectionView.cellForItem(at: indexPath) as? NCListCell, + cell.metadata?.ocId == ocId else { + return } + cell.setSharedAvatarImage(image) } } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift index 69eba664b6..05bb80eb97 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDataSource.swift @@ -304,45 +304,4 @@ extension NCCollectionViewCommon: UICollectionViewDataSource { } return ownerId } - - /// Caches preview images asynchronously for the provided metadata entries. - /// - Parameters: - /// - metadatas: The list of metadata entries to cache. - /// - priority: The task priority to use (default is `.utility`). - func cachingAsync(metadatas: [tableMetadata], priority: TaskPriority = .utility) { - let previewExt = global.previewExt256 - - Task.detached(priority: priority) { [utility] in - for metadata in metadatas { - guard !Task.isCancelled, - metadata.isImageOrVideo, - NCImageCache.shared.getImageCache(ocId: metadata.ocId, - etag: metadata.etag, - ext: previewExt) == nil else { - continue - } - - guard let image = utility.getImage(ocId: metadata.ocId, - etag: metadata.etag, - ext: previewExt, - userId: metadata.userId, - urlBase: metadata.urlBase) else { - continue - } - - NCImageCache.shared.addImageCache(ocId: metadata.ocId, - etag: metadata.etag, - image: image, - ext: previewExt) - } - } - } - - func removeImageCache(metadatas: [tableMetadata]) { - DispatchQueue.global().async { - for metadata in metadatas { - NCImageCache.shared.removeImageCache(ocId: metadata.ocId, etag: metadata.etag) - } - } - } } diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift index 1fde191f4f..7333784f8e 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon.swift @@ -375,8 +375,6 @@ class NCCollectionViewCommon: UIViewController, NCAccountSettingsModelDelegate, NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCloseRichWorkspaceWebView), object: nil) - - removeImageCache(metadatas: self.dataSource.getMetadatas()) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/iOSClient/Media/NCMedia+Command.swift b/iOSClient/Media/NCMedia+Command.swift index 90ce2d97b3..e4c5aa80e9 100644 --- a/iOSClient/Media/NCMedia+Command.swift +++ b/iOSClient/Media/NCMedia+Command.swift @@ -49,7 +49,7 @@ extension NCMedia { if lastVisibleDateRange?.first == firstIndexPath, lastVisibleDateRange?.last == lastIndexPath, - navigationItem.leftBarButtonItem === buttonDateBarItem { + navigationItem.leftBarButtonItems?.contains(where: { $0 === buttonDateBarItem }) == true { return } @@ -194,8 +194,7 @@ extension NCMedia { collectionView.layoutIfNeeded() - lastCacheCenterIndex = nil - updateImageCacheWindow() + updateImageCacheWindow(force: true) } @MainActor diff --git a/iOSClient/Media/NCMedia+ScrollViewDelegate.swift b/iOSClient/Media/NCMedia+ScrollViewDelegate.swift new file mode 100644 index 0000000000..a209cc194f --- /dev/null +++ b/iOSClient/Media/NCMedia+ScrollViewDelegate.swift @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit + +extension NCMedia: UIScrollViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + setTitleDate() + + if !dataSource.compactMetadatas.isEmpty { + setNeedsStatusBarAppearanceUpdate() + } + } + + func scrollViewDidEndDragging( + _ scrollView: UIScrollView, + willDecelerate decelerate: Bool + ) { + if !decelerate { + updateImageCacheWindow() + searchNewMedia() + } + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + updateImageCacheWindow() + searchNewMedia() + } + + func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { + updateImageCacheWindow(force: true) + } +} diff --git a/iOSClient/Media/NCMedia+SelectDelegate.swift b/iOSClient/Media/NCMedia+SelectDelegate.swift new file mode 100644 index 0000000000..09a2968063 --- /dev/null +++ b/iOSClient/Media/NCMedia+SelectDelegate.swift @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import UIKit + +extension NCMedia: NCSelectDelegate { + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session, controller: NCMainTabBarController?) { + guard let serverUrl else { return } + + Task { + let home = utilityFileSystem.getHomeServer(session: session) + let mediaPath = serverUrl.replacingOccurrences(of: home, with: "") + + await database.setAccountMediaPathAsync(mediaPath, account: session.account) + + self.imageCache.removeAll() + + await self.debouncerLoadDataSource.call { + await self.loadDataSource() + } + + searchNewMedia() + } + } +} diff --git a/iOSClient/Media/NCMedia.swift b/iOSClient/Media/NCMedia.swift index 6ba3b202db..b7ada94693 100644 --- a/iOSClient/Media/NCMedia.swift +++ b/iOSClient/Media/NCMedia.swift @@ -60,28 +60,10 @@ class NCMedia: UIViewController { return } - cacheWindowTask?.cancel() - cacheWindowTask = nil - lastCacheCenterIndex = nil - imageCache.removeAll() - missingImageCacheKeys.removeAll() } } - let cacheWindowRadius = NCImageCache.shared.maximumCachedImages / 2 - let cacheWindowUpdateThreshold = NCImageCache.shared.maximumCachedImages / 6 - var lastCacheCenterIndex: Int? - var cacheWindowTask: Task? - var missingImageCacheKeys: Set = [] var imageLoadingTasks: [String: Task] = [:] - struct ImageCacheWindowItem: Sendable { - let ocId: String - let etag: String - } - private func imageCacheKey(ocId: String, etag: String, ext: String) -> String { - "\(ocId)-\(etag)-\(ext)" - } - let debouncerLoadDataSource = NCDebouncer(delay: .seconds(3), maxEventCount: 10) let debouncerSearch = NCDebouncer(delay: .seconds(2), maxEventCount: 10) @@ -210,11 +192,7 @@ class NCMedia: UIViewController { self.layoutType = self.database.getLayoutForView(account: account, key: self.global.layoutViewMedia, serverUrl: "").layout - self.cacheWindowTask?.cancel() - self.cacheWindowTask = nil - self.lastCacheCenterIndex = nil self.imageCache.removeAll() - self.missingImageCacheKeys.removeAll() await self.searchMediaUI(true) } @@ -226,11 +204,7 @@ class NCMedia: UIViewController { return } - self.cacheWindowTask?.cancel() - self.cacheWindowTask = nil - self.lastCacheCenterIndex = nil - self.imageCache.removeAll() - self.missingImageCacheKeys.removeAll() + imageCache.removeAll() self.dataSource.clearCompactMetadatas() await self.searchMediaUI(true) @@ -312,9 +286,7 @@ class NCMedia: UIViewController { buildDataSourceTask?.cancel() buildDataSourceTask = nil - cacheWindowTask?.cancel() - cacheWindowTask = nil - lastCacheCenterIndex = nil + imageCache.removeAll() Task { [weak self] in guard let self else { @@ -393,173 +365,34 @@ class NCMedia: UIViewController { } } - // MARK: - Image Cache - @MainActor - func updateImageCacheWindow() { - let visibleIndexPaths = collectionView.indexPathsForVisibleItems.sorted() - - guard !visibleIndexPaths.isEmpty else { + func updateImageCacheWindow(force: Bool = false) { + guard !dataSource.compactMetadatas.isEmpty else { return } - let centerIndexPath = visibleIndexPaths[visibleIndexPaths.count / 2] - guard let centerIndex = dataSource.globalIndex(for: centerIndexPath) else { - return - } - - if let lastCacheCenterIndex, - abs(centerIndex - lastCacheCenterIndex) < cacheWindowUpdateThreshold { - return - } - - lastCacheCenterIndex = centerIndex - - cacheWindowTask?.cancel() - - cacheWindowTask = Task { [weak self] in - guard let self else { - return - } - - await self.loadImageCacheWindow(around: centerIndex) - } - } - - @MainActor - private func loadImageCacheWindow(around centerIndex: Int) async { - let metadataCount = dataSource.compactMetadatas.count - - guard metadataCount > 0 else { - return - } - - let lowerBound = max(0, centerIndex - cacheWindowRadius) - let upperBound = min(metadataCount, centerIndex + cacheWindowRadius + 1) - let ext = global.getSizeExtension(column: numberOfColumns) - let userId = session.userId - let urlBase = session.urlBase - - let items = dataSource.compactMetadatas[lowerBound.. ( compactMetadatas: [NCCompactMetadata], - sections: [NCMediaSection] + sections: [NCMediaSection], + imageCacheWindowItems: [NCImageCache.ImageCacheWindowItem] ) { guard !compactMetadatas.isEmpty else { - return ([], []) + return ([], [], []) } var sections: [NCMediaSection] = [] @@ -674,7 +678,16 @@ public class NCMediaDataSource: NSObject { var currentYearMonth: NCYearMonth? var currentSectionMetadatas: [NCCompactMetadata] = [] + var imageCacheWindowItems: [NCImageCache.ImageCacheWindowItem] = [] + for compactMetadata in compactMetadatas { + imageCacheWindowItems.append( + NCImageCache.ImageCacheWindowItem( + ocId: compactMetadata.ocId, + etag: compactMetadata.etag + ) + ) + guard let yearMonth = NCYearMonth(date: compactMetadata.date) else { continue } @@ -709,7 +722,8 @@ public class NCMediaDataSource: NSObject { return ( compactMetadatas, - sections + sections, + imageCacheWindowItems ) } diff --git a/iOSClient/NCImageCache.swift b/iOSClient/NCImageCache.swift index a642a640a7..cb56a264e3 100644 --- a/iOSClient/NCImageCache.swift +++ b/iOSClient/NCImageCache.swift @@ -10,11 +10,19 @@ final class NCImageCache: @unchecked Sendable { private let utility = NCUtility() private let cache = NSCache() + private let maximumCachedImages = 510 - public var maximumCachedImages: Int = 500 { - didSet { - cache.countLimit = maximumCachedImages - } + private lazy var mediaWindowCache = MediaWindowCache( + maximumCachedImages: maximumCachedImages, + imageCache: self + ) + + struct ImageCacheWindowItem: Sendable { + let ocId: String + let etag: String + } + private func imageCacheKey(ocId: String, etag: String, ext: String) -> String { + "\(ocId)-\(etag)-\(ext)" } private init() { @@ -25,7 +33,7 @@ final class NCImageCache: @unchecked Sendable { object: nil, queue: nil ) { [weak self] _ in - self?.cache.removeAllObjects() + self?.removeAll() } NotificationCenter.default.addObserver( @@ -33,44 +41,48 @@ final class NCImageCache: @unchecked Sendable { object: nil, queue: nil ) { [weak self] _ in - self?.cache.removeAllObjects() + self?.removeAll() } } private func cacheKey(ocId: String, etag: String, ext: String) -> NSString { - "\(ocId)\(etag)\(ext)" as NSString - } - - func addImageCache(ocId: String, etag: String, data: Data, ext: String) { - guard let image = UIImage(data: data) else { return } - - cache.setObject(image, forKey: cacheKey(ocId: ocId, etag: etag, ext: ext)) + imageCacheKey(ocId: ocId, etag: etag, ext: ext) as NSString } func addImageCache(ocId: String, etag: String, image: UIImage, ext: String) { cache.setObject(image, forKey: cacheKey(ocId: ocId, etag: etag, ext: ext)) } - func addImageCache(image: UIImage, key: String) { - cache.setObject(image, forKey: key as NSString) - } - func getImageCache(ocId: String, etag: String, ext: String) -> UIImage? { cache.object(forKey: cacheKey(ocId: ocId, etag: etag, ext: ext)) } - func getImageCache(key: String) -> UIImage? { - cache.object(forKey: key as NSString) - } + func removeAll() { + cache.removeAllObjects() - func removeImageCache(ocId: String, etag: String) { - cache.removeObject(forKey: cacheKey(ocId: ocId, etag: etag, ext: NCGlobal.shared.previewExt256)) - cache.removeObject(forKey: cacheKey(ocId: ocId, etag: etag, ext: NCGlobal.shared.previewExt512)) - cache.removeObject(forKey: cacheKey(ocId: ocId, etag: etag, ext: NCGlobal.shared.previewExt1024)) + Task { + await mediaWindowCache.removeAll() + } } - func removeAll() { - cache.removeAllObjects() + // MARK: - + + func updateImageCacheWindow( + imageCacheWindowItems: [ImageCacheWindowItem], + centerIndex: Int, + numberOfColumns: Int, + session: NCSession.Session, + force: Bool = false + ) { + Task { + await mediaWindowCache.update( + imageCacheWindowItems: imageCacheWindowItems, + centerIndex: centerIndex, + numberOfColumns: numberOfColumns, + session: session, + force: force + ) + } } // MARK: - @@ -155,3 +167,161 @@ final class NCImageCache: @unchecked Sendable { UIImage(named: "folderAutomaticUpload")!.image(color: NCBrandColor.shared.getElement(account: account)) } } + +private actor MediaWindowCache { + private let maximumCachedImages: Int + private unowned let imageCache: NCImageCache + + private var lastCacheCenterIndex: Int? + private var lastCacheExtension: String? + private var cacheWindowTask: Task? + private var missingImageCacheKeys: Set = [] + + private var cacheWindowRadius: Int { + maximumCachedImages / 2 + } + + private var cacheWindowUpdateThreshold: Int { + maximumCachedImages / 6 + } + + init( + maximumCachedImages: Int, + imageCache: NCImageCache + ) { + self.maximumCachedImages = maximumCachedImages + self.imageCache = imageCache + } + + func removeAll() { + cacheWindowTask?.cancel() + cacheWindowTask = nil + lastCacheCenterIndex = nil + lastCacheExtension = nil + missingImageCacheKeys.removeAll() + } + + func update( + imageCacheWindowItems: [NCImageCache.ImageCacheWindowItem], + centerIndex: Int, + numberOfColumns: Int, + session: NCSession.Session, + force: Bool + ) { + guard imageCacheWindowItems.indices.contains(centerIndex) else { + return + } + + let ext = NCGlobal.shared.getSizeExtension(column: numberOfColumns) + + if !force, + lastCacheExtension == ext, + let lastCacheCenterIndex, + abs(centerIndex - lastCacheCenterIndex) < cacheWindowUpdateThreshold { + return + } + + lastCacheCenterIndex = centerIndex + lastCacheExtension = ext + + cacheWindowTask?.cancel() + + cacheWindowTask = Task { [weak self] in + guard let self else { + return + } + + await self.load(imageCacheWindowItems: imageCacheWindowItems, centerIndex: centerIndex, ext: ext, session: session) + } + } + + private func load(imageCacheWindowItems: [NCImageCache.ImageCacheWindowItem], centerIndex: Int, ext: String,session: NCSession.Session) async { + let itemCount = imageCacheWindowItems.count + guard imageCacheWindowItems.indices.contains(centerIndex) else { + return + } + let lowerBound = max(0, centerIndex - cacheWindowRadius) + let upperBound = min( + itemCount, + centerIndex + cacheWindowRadius + 1 + ) + let items = Array(imageCacheWindowItems[lowerBound.. String { + "\(ocId)-\(etag)-\(ext)" + } +} diff --git a/iOSClient/Notification/NCNotification.swift b/iOSClient/Notification/NCNotification.swift index 865f5e5b67..c0cd254f0b 100644 --- a/iOSClient/Notification/NCNotification.swift +++ b/iOSClient/Notification/NCNotification.swift @@ -150,45 +150,37 @@ class NCNotification: UITableViewController, NCNotificationCellDelegate { cell.avatarLeadingMargin.constant = 50 let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: user) - if let image = NCImageCache.shared.getImageCache(key: fileName) { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { cell.avatar?.image = image - } else { - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - - if let image = UIImage(contentsOfFile: fileNameLocalPath) { - cell.avatar?.image = image - NCImageCache.shared.addImageCache(image: image, key: fileName) - } - - let account = session.account - let identifier = notification.idNotification + } + let account = session.account + let identifier = notification.idNotification - Task { - let etagResource = await NCManageDatabase.shared.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await NCManageDatabase.shared.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - guard - let cell = self.tableView.cellForRow(at: indexPath) as? NCNotificationCell, - cell.identifier == identifier else { - return - } - cell.avatar?.image = image + Task { + let etagResource = await NCManageDatabase.shared.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await NCManageDatabase.shared.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + guard + let cell = self.tableView.cellForRow(at: indexPath) as? NCNotificationCell, + cell.identifier == identifier else { + return } + cell.avatar?.image = image } } } diff --git a/iOSClient/Offline/NCOffline.swift b/iOSClient/Offline/NCOffline.swift index c677762068..8ebd6a2ea1 100644 --- a/iOSClient/Offline/NCOffline.swift +++ b/iOSClient/Offline/NCOffline.swift @@ -81,8 +81,6 @@ class NCOffline: NCCollectionViewCommon { layoutForView: layoutForView, account: session.account) await super.reloadDataSource() - - cachingAsync(metadatas: metadatas) } override func getServerData(forced: Bool = false) async { diff --git a/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift index 8e03a4274b..0bfe833ad5 100644 --- a/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift +++ b/iOSClient/Processor/NCMediaMetadataBackfillProcessor.swift @@ -46,6 +46,9 @@ final class NCMediaMetadataBackfillProcessor { } /// Processes the remote media archive page by page and creates missing metadata placeholders. + /// + /// An interrupted cycle resumes immediately from the stored offset. + /// A completed cycle starts again after the configured interval. func runBackfill( account: tableAccount, limit: Int, @@ -53,17 +56,19 @@ final class NCMediaMetadataBackfillProcessor { ) async -> BackfillStatus { let database = NCManageDatabase.shared let state = await database.getMediaMetadataBackfillAsync(account: account.account) - - guard state?.lastCompletedCycleDate == nil else { - return .skippedAlreadyCompleted(account: account.account) - } - + let cycleInterval: TimeInterval = 7 * 24 * 60 * 60 // week var offset = state?.offset ?? 0 var token: String? var processed = 0 var inserted = 0 var updated = 0 + if state?.offset == 0, + let lastCompletedCycleDate = state?.lastCompletedCycleDate, + Date().timeIntervalSince(lastCompletedCycleDate) < cycleInterval { + return .skippedAlreadyCompleted(account: account.account) + } + while !Task.isCancelled { let result = await runSearch( mediaPath: account.mediaPath, diff --git a/iOSClient/Share/NCShare.swift b/iOSClient/Share/NCShare.swift index db3f95f5d9..073e8ff1a3 100644 --- a/iOSClient/Share/NCShare.swift +++ b/iOSClient/Share/NCShare.swift @@ -163,41 +163,32 @@ class NCShare: UIViewController, NCSharePagingContent { avatarButton.menu = NCContextMenuProfile(userId: metadata.ownerId, session: session, viewController: self).viewMenu() let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: metadata.ownerId) - - if let image = NCImageCache.shared.getImageCache(key: fileName) { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { self.sharedWithYouByImage.image = image - } else { - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - - if let image = UIImage(contentsOfFile: fileNameLocalPath) { - self.sharedWithYouByImage.image = image - NCImageCache.shared.addImageCache(image: image, key: fileName) - } + } + let user = metadata.ownerId + let account = metadata.account - let user = metadata.ownerId - let account = metadata.account - - Task { - let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await self.database.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - self.sharedWithYouByImage.image = image - } + Task { + let etagResource = await database.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await self.database.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + self.sharedWithYouByImage.image = image } } } diff --git a/iOSClient/Share/NCShareUserCell.swift b/iOSClient/Share/NCShareUserCell.swift index e4116a19a9..4e40d40638 100644 --- a/iOSClient/Share/NCShareUserCell.swift +++ b/iOSClient/Share/NCShareUserCell.swift @@ -193,43 +193,37 @@ class NCSearchUserDropDownCell: DropDownCell { } let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: sharee.shareWith) - - if let image = NCImageCache.shared.getImageCache(key: fileName) { + let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) + if let image = UIImage(contentsOfFile: fileNameLocalPath) { self.imageItem.image = image - } else { - let fileNameLocalPath = self.utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - if let image = UIImage(contentsOfFile: fileNameLocalPath) { - self.imageItem.image = image - NCImageCache.shared.addImageCache(image: image, key: fileName) - } + } - let user = sharee.shareWith - let account = session.account - - Task { - let etagResource = await NCManageDatabase.shared.getTableAvatarAsync(fileName: fileName)?.etag - await NCTransferCoordinator.shared.start(identifier: fileName, - priority: .userInitiated) { - let results = await NextcloudKit.shared.downloadAvatarAsync( - user: user, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etagResource, - account: account) - - if results.error == .success, - let image = results.imageAvatar, - let etag = results.etag, - etag != etagResource { - NCImageCache.shared.addImageCache(image: image, key: fileName) - await NCManageDatabase.shared.addAvatarAsync(fileName: fileName, etag: etag) - await MainActor.run { - self.imageItem.image = image - } + let user = sharee.shareWith + let account = session.account + + Task { + let etagResource = await NCManageDatabase.shared.getTableAvatarAsync(fileName: fileName)?.etag + await NCTransferCoordinator.shared.start(identifier: fileName, + priority: .userInitiated) { + let results = await NextcloudKit.shared.downloadAvatarAsync( + user: user, + fileNameLocalPath: fileNameLocalPath, + sizeImage: NCGlobal.shared.avatarSize, + avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, + etagResource: etagResource, + account: account) + + if results.error == .success, + let image = results.imageAvatar, + let etag = results.etag, + etag != etagResource { + await NCManageDatabase.shared.addAvatarAsync(fileName: fileName, etag: etag) + await MainActor.run { + self.imageItem.image = image } } } } + } } diff --git a/iOSClient/Shares/NCShares.swift b/iOSClient/Shares/NCShares.swift index 79674d943f..11d6d69dee 100644 --- a/iOSClient/Shares/NCShares.swift +++ b/iOSClient/Shares/NCShares.swift @@ -66,8 +66,6 @@ class NCShares: NCCollectionViewCommon { account: session.account) await super.reloadDataSource() - - cachingAsync(metadatas: metadatas) } override func getServerData(forced: Bool = false) async {