123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- //
- // NCMedia.swift
- // Nextcloud
- //
- // Created by Marino Faggiana on 12/02/2019.
- // Copyright © 2019 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
- 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 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
- var lastContentOffsetY: CGFloat = 0
- var timeIntervalSearchNewMedia: TimeInterval = 2.0
- var timerSearchNewMedia: Timer?
- let insetsTop: CGFloat = 75
- let maxImageGrid: CGFloat = 7
- var livePhotoImage = UIImage()
- var playImage = UIImage()
- var photoImage = UIImage()
- var videoImage = UIImage()
- // MARK: - View Life Cycle
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .systemBackground
- 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)
- 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)
- 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)
- 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()
- }
- }
- }
- 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)
- startTimer()
- }
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- 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)
- coordinator.animate(alongsideTransition: nil) { _ in
- self.setTitleDate()
- }
- }
- override var preferredStatusBarStyle: UIStatusBarStyle {
- if self.traitCollection.userInterfaceStyle == .dark {
- return .lightContent
- } else if isTop {
- return .darkContent
- } else {
- return .lightContent
- }
- }
- 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 error = userInfo["error"] as? NKError else { return }
- 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 || imageCache.createMediaCacheInProgress {
- view.emptyTitle.text = NSLocalizedString("_search_in_progress_", comment: "")
- } else {
- view.emptyTitle.text = NSLocalizedString("_tutorial_photo_view_", comment: "")
- }
- view.emptyDescription.text = ""
- }
- // MARK: - Image
- func getImage(metadata: tableMetadata) -> UIImage? {
- 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)),
- 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)
- }
- tabBarSelect?.selectCount = selectOcId.count
- } else {
- // ACTIVE SERVERURL
- appDelegate.activeServerUrl = metadata.serverUrl
- if let metadatas = self.metadatas?.getArray() {
- NCViewer().view(viewController: self, metadata: metadata, metadatas: metadatas, imageIcon: getImage(metadata: metadata))
- }
- }
- }
- }
- 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
- let image = cell.imageItem.image
- return UIContextMenuConfiguration(identifier: identifier, previewProvider: {
- return NCViewerProviderContextMenu(metadata: metadata, image: image)
- }, actionProvider: { _ in
- return NCContextMenu().viewMenu(ocId: metadata.ocId, indexPath: indexPath, viewController: self, image: image)
- })
- }
- func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
- animator.addCompletion {
- if let indexPath = configuration.identifier as? IndexPath {
- self.collectionView(collectionView, didSelectItemAt: indexPath)
- }
- }
- }
- }
- 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 numberOfItemsInSection == 0 {
- selectOrCancelButton.isHidden = true
- menuButton.isHidden = false
- gradientView.isHidden = true
- activityIndicatorTrailing.constant = 50
- } else if isEditMode {
- selectOrCancelButton.isHidden = false
- menuButton.isHidden = true
- activityIndicatorTrailing.constant = 150
- } else {
- 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 {
- guard let metadata = metadatas[indexPath.row] else { return }
- for case let operation as NCMediaDownloadThumbnaill in NCNetworking.shared.downloadThumbnailQueue.operations where operation.metadata.ocId == metadata.ocId {
- operation.cancel()
- }
- for case let operation as NCOperationConvertLivePhoto in NCNetworking.shared.convertLivePhotoQueue.operations where operation.ocId == metadata.ocId {
- operation.cancel()
- }
- }
- }
- 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() }
- 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.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
- if NCGlobal.shared.isLivePhotoServerAvailable, metadata.isLivePhoto, metadata.isNotFlaggedAsLivePhotoByServer {
- NCNetworking.shared.convertLivePhoto(metadata: metadata)
- }
- if metadata.isAudioOrVideo {
- cell.imageStatus.image = playImage
- } else if metadata.isLivePhoto {
- cell.imageStatus.image = livePhotoImage
- } else {
- cell.imageStatus.image = nil
- }
- if isEditMode, selectOcId.contains(metadata.ocId) {
- cell.selected(true)
- } else {
- cell.selected(false)
- }
- return cell
- }
- }
- // MARK: -
- 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 {
- isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25)
- setColor()
- setNeedsStatusBarAppearanceUpdate()
- if lastContentOffsetY == 0 || lastContentOffsetY / 2 <= scrollView.contentOffset.y || lastContentOffsetY / 2 >= scrollView.contentOffset.y {
- setTitleDate()
- lastContentOffsetY = scrollView.contentOffset.y
- }
- } else {
- setColor()
- }
- }
- func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
- }
- func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
- if !decelerate {
- if !decelerate {
- startTimer()
- }
- }
- }
- func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
- startTimer()
- }
- func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
- let y = view.safeAreaInsets.top
- scrollView.contentOffset.y = -(insetsTop + y)
- }
- }
- // 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)
- let mediaPath = serverUrl.replacingOccurrences(of: home, with: "")
- NCManageDatabase.shared.setAccountMediaPath(mediaPath, account: activeAccount.account)
- activeAccount = NCManageDatabase.shared.getActiveAccount() ?? tableAccount()
- reloadDataSource()
- startTimer()
- }
- }
|