NCMedia.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. //
  2. // NCMedia.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 12/02/2019.
  6. // Copyright © 2019 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 Foundation
  24. import UIKit
  25. import NextcloudKit
  26. import RealmSwift
  27. class NCMedia: UIViewController {
  28. @IBOutlet weak var collectionView: UICollectionView!
  29. @IBOutlet weak var titleDate: UILabel!
  30. @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
  31. @IBOutlet weak var activityIndicatorTrailing: NSLayoutConstraint!
  32. @IBOutlet weak var selectOrCancelButton: UIButton!
  33. @IBOutlet weak var selectOrCancelButtonTrailing: NSLayoutConstraint!
  34. @IBOutlet weak var menuButton: UIButton!
  35. @IBOutlet weak var gradientView: UIView!
  36. let lockQueue = DispatchQueue(label: "com.nextcloud.mediasearch.lockqueue")
  37. var hasRunSearchMedia: Bool = false
  38. let layout = NCMediaLayout()
  39. var layoutType = NCGlobal.shared.mediaLayoutRatio
  40. var documentPickerViewController: NCDocumentPickerViewController?
  41. var tabBarSelect: NCMediaSelectTabBar!
  42. let utilityFileSystem = NCUtilityFileSystem()
  43. let global = NCGlobal.shared
  44. let utility = NCUtility()
  45. let database = NCManageDatabase.shared
  46. let imageCache = NCImageCache.shared
  47. var dataSource = NCMediaDataSource()
  48. let refreshControl = UIRefreshControl()
  49. var isTop: Bool = true
  50. var isEditMode = false
  51. var fileSelect: [String] = []
  52. var filesExists: [String] = []
  53. var fileDeleted: [String] = []
  54. var attributesZoomIn: UIMenuElement.Attributes = []
  55. var attributesZoomOut: UIMenuElement.Attributes = []
  56. let gradient: CAGradientLayer = CAGradientLayer()
  57. var showOnlyImages = false
  58. var showOnlyVideos = false
  59. var timeIntervalSearchNewMedia: TimeInterval = 2.0
  60. var timerSearchNewMedia: Timer?
  61. let insetsTop: CGFloat = 75
  62. let livePhotoImage = NCUtility().loadImage(named: "livephoto", colors: [.white])
  63. let playImage = NCUtility().loadImage(named: "play.fill", colors: [.white])
  64. var photoImage = UIImage()
  65. var videoImage = UIImage()
  66. var pinchGesture: UIPinchGestureRecognizer = UIPinchGestureRecognizer()
  67. var lastScale: CGFloat = 1.0
  68. var currentScale: CGFloat = 1.0
  69. var maxColumns: Int {
  70. let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
  71. let column = Int(screenWidth / 44)
  72. return column
  73. }
  74. var transitionColumns = false
  75. var numberOfColumns: Int = 0
  76. var lastNumberOfColumns: Int = 0
  77. var hiddenCellMetadats: ThreadSafeArray<String> = ThreadSafeArray()
  78. var session: NCSession.Session {
  79. NCSession.shared.getSession(controller: tabBarController)
  80. }
  81. var controller: NCMainTabBarController? {
  82. self.tabBarController as? NCMainTabBarController
  83. }
  84. var isViewActived: Bool {
  85. return self.isViewLoaded && self.view.window != nil
  86. }
  87. var isPinchGestureActive: Bool {
  88. return pinchGesture.state == .began || pinchGesture.state == .changed
  89. }
  90. // MARK: - View Life Cycle
  91. override func viewDidLoad() {
  92. super.viewDidLoad()
  93. view.backgroundColor = .systemBackground
  94. collectionView.register(UINib(nibName: "NCSectionFirstHeaderEmptyData", bundle: nil), forSupplementaryViewOfKind: mediaSectionHeader, withReuseIdentifier: "sectionFirstHeaderEmptyData")
  95. collectionView.register(UINib(nibName: "NCSectionFooter", bundle: nil), forSupplementaryViewOfKind: mediaSectionFooter, withReuseIdentifier: "sectionFooter")
  96. collectionView.register(UINib(nibName: "NCMediaCell", bundle: nil), forCellWithReuseIdentifier: "mediaCell")
  97. collectionView.alwaysBounceVertical = true
  98. collectionView.contentInset = UIEdgeInsets(top: insetsTop, left: 0, bottom: 50, right: 0)
  99. collectionView.backgroundColor = .systemBackground
  100. collectionView.prefetchDataSource = self
  101. collectionView.dragInteractionEnabled = true
  102. collectionView.dragDelegate = self
  103. collectionView.dropDelegate = self
  104. layout.sectionInset = UIEdgeInsets(top: 0, left: 2, bottom: 0, right: 2)
  105. collectionView.collectionViewLayout = layout
  106. layoutType = database.getLayoutForView(account: session.account, key: global.layoutViewMedia, serverUrl: "")?.layout ?? global.mediaLayoutRatio
  107. tabBarSelect = NCMediaSelectTabBar(tabBarController: self.tabBarController, delegate: self)
  108. titleDate.text = ""
  109. selectOrCancelButton.backgroundColor = .clear
  110. selectOrCancelButton.layer.cornerRadius = 15
  111. selectOrCancelButton.layer.masksToBounds = true
  112. selectOrCancelButton.setTitle( NSLocalizedString("_select_", comment: ""), for: .normal)
  113. selectOrCancelButton.addBlur(style: .systemUltraThinMaterial)
  114. menuButton.backgroundColor = .clear
  115. menuButton.layer.cornerRadius = 15
  116. menuButton.layer.masksToBounds = true
  117. menuButton.showsMenuAsPrimaryAction = true
  118. menuButton.configuration = UIButton.Configuration.plain()
  119. menuButton.setImage(NCUtility().loadImage(named: "ellipsis"), for: .normal)
  120. menuButton.changesSelectionAsPrimaryAction = false
  121. menuButton.addBlur(style: .systemUltraThinMaterial)
  122. gradient.startPoint = CGPoint(x: 0, y: 0.1)
  123. gradient.endPoint = CGPoint(x: 0, y: 1)
  124. gradient.colors = [UIColor.black.withAlphaComponent(UIAccessibility.isReduceTransparencyEnabled ? 0.8 : 0.4).cgColor, UIColor.clear.cgColor]
  125. gradientView.layer.insertSublayer(gradient, at: 0)
  126. collectionView.refreshControl = refreshControl
  127. refreshControl.action(for: .valueChanged) { _ in
  128. self.loadDataSource()
  129. self.searchMediaUI(true)
  130. }
  131. pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchGesture(_:)))
  132. collectionView.addGestureRecognizer(pinchGesture)
  133. NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: global.notificationCenterChangeUser), object: nil, queue: nil) { _ in
  134. self.layoutType = self.database.getLayoutForView(account: self.session.account, key: self.global.layoutViewMedia, serverUrl: "")?.layout ?? self.global.mediaLayoutRatio
  135. self.imageCache.removeAll()
  136. self.loadDataSource()
  137. self.searchMediaUI(true)
  138. }
  139. NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: global.notificationCenterClearCache), object: nil, queue: nil) { _ in
  140. self.dataSource.metadatas.removeAll()
  141. self.imageCache.removeAll()
  142. self.searchMediaUI(true)
  143. }
  144. NotificationCenter.default.addObserver(self, selector: #selector(fileExists(_:)), name: NSNotification.Name(rawValue: global.notificationCenterFileExists), object: nil)
  145. NotificationCenter.default.addObserver(self, selector: #selector(deleteFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterDeleteFile), object: nil)
  146. NotificationCenter.default.addObserver(self, selector: #selector(reloadDataSource(_:)), name: NSNotification.Name(rawValue: global.notificationCenterReloadDataSource), object: nil)
  147. NotificationCenter.default.addObserver(self, selector: #selector(networkRemoveAll), name: UIApplication.didEnterBackgroundNotification, object: nil)
  148. }
  149. override func viewWillAppear(_ animated: Bool) {
  150. super.viewWillAppear(animated)
  151. navigationController?.setMediaAppreance()
  152. if dataSource.metadatas.isEmpty {
  153. loadDataSource()
  154. }
  155. }
  156. override func viewDidAppear(_ animated: Bool) {
  157. super.viewDidAppear(animated)
  158. NotificationCenter.default.addObserver(self, selector: #selector(copyMoveFile(_:)), name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil)
  159. NotificationCenter.default.addObserver(self, selector: #selector(enterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
  160. searchNewMedia()
  161. createMenu()
  162. }
  163. override func viewDidDisappear(_ animated: Bool) {
  164. super.viewDidDisappear(animated)
  165. NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: global.notificationCenterCopyMoveFile), object: nil)
  166. NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
  167. networkRemoveAll()
  168. }
  169. override var preferredStatusBarStyle: UIStatusBarStyle {
  170. if self.traitCollection.userInterfaceStyle == .dark {
  171. return .lightContent
  172. } else if isTop {
  173. return .darkContent
  174. } else {
  175. return .lightContent
  176. }
  177. }
  178. override func viewWillLayoutSubviews() {
  179. super.viewWillLayoutSubviews()
  180. if let frame = tabBarController?.tabBar.frame {
  181. tabBarSelect.hostingController.view.frame = frame
  182. }
  183. gradient.frame = gradientView.bounds
  184. }
  185. func searchNewMedia() {
  186. timerSearchNewMedia?.invalidate()
  187. timerSearchNewMedia = Timer.scheduledTimer(timeInterval: timeIntervalSearchNewMedia, target: self, selector: #selector(searchMediaUI(_:)), userInfo: nil, repeats: false)
  188. }
  189. // MARK: - NotificationCenter
  190. @objc func networkRemoveAll() {
  191. timerSearchNewMedia?.invalidate()
  192. timerSearchNewMedia = nil
  193. filesExists.removeAll()
  194. fileDeleted.removeAll()
  195. NCNetworking.shared.fileExistsQueue.cancelAll()
  196. NCNetworking.shared.downloadThumbnailQueue.cancelAll()
  197. Task {
  198. let tasks = await NCNetworking.shared.getAllDataTask()
  199. for task in tasks.filter({ $0.taskDescription == global.taskDescriptionRetrievesProperties }) {
  200. task.cancel()
  201. }
  202. }
  203. }
  204. @objc func reloadDataSource(_ notification: NSNotification) {
  205. self.loadDataSource()
  206. }
  207. @objc func deleteFile(_ notification: NSNotification) {
  208. guard let userInfo = notification.userInfo as NSDictionary?,
  209. let error = userInfo["error"] as? NKError else { return }
  210. if error != .success {
  211. NCContentPresenter().showError(error: error)
  212. self.loadDataSource()
  213. }
  214. }
  215. @objc func enterForeground(_ notification: NSNotification) {
  216. searchNewMedia()
  217. }
  218. @objc func fileExists(_ notification: NSNotification) {
  219. guard let userInfo = notification.userInfo as NSDictionary?,
  220. let ocId = userInfo["ocId"] as? String,
  221. let fileExists = userInfo["fileExists"] as? Bool else { return }
  222. var indexPaths: [IndexPath] = []
  223. filesExists.append(ocId)
  224. if !fileExists {
  225. if let index = dataSource.metadatas.firstIndex(where: {$0.ocId == ocId}),
  226. let cell = collectionView.cellForItem(at: IndexPath(row: index, section: 0)) as? NCMediaCell,
  227. dataSource.metadatas[index].ocId == cell.ocId {
  228. indexPaths.append(IndexPath(row: index, section: 0))
  229. }
  230. dataSource.removeMetadata([ocId])
  231. database.deleteMetadataOcId(ocId)
  232. if !indexPaths.isEmpty {
  233. collectionView.deleteItems(at: indexPaths)
  234. } else {
  235. collectionViewReloadData()
  236. }
  237. }
  238. }
  239. @objc func copyMoveFile(_ notification: NSNotification) {
  240. guard let userInfo = notification.userInfo as NSDictionary?,
  241. let dragDrop = userInfo["dragdrop"] as? Bool,
  242. dragDrop else { return }
  243. setEditMode(false)
  244. DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
  245. self.loadDataSource()
  246. self.searchMediaUI()
  247. }
  248. }
  249. func buildMediaPhotoVideo(columnCount: Int) {
  250. var pointSize: CGFloat = 0
  251. switch columnCount {
  252. case 0...1: pointSize = 60
  253. case 2...3: pointSize = 30
  254. case 4...5: pointSize = 25
  255. case 6...Int(maxColumns): pointSize = 20
  256. default: pointSize = 20
  257. }
  258. if let image = UIImage(systemName: "photo.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))?.withTintColor(.systemGray4, renderingMode: .alwaysOriginal) {
  259. photoImage = image
  260. }
  261. if let image = UIImage(systemName: "video.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: pointSize))?.withTintColor(.systemGray4, renderingMode: .alwaysOriginal) {
  262. videoImage = image
  263. }
  264. }
  265. }
  266. // MARK: -
  267. extension NCMedia: UIScrollViewDelegate {
  268. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  269. if !dataSource.metadatas.isEmpty {
  270. isTop = scrollView.contentOffset.y <= -(insetsTop + view.safeAreaInsets.top - 25)
  271. setColor()
  272. setTitleDate()
  273. setNeedsStatusBarAppearanceUpdate()
  274. } else {
  275. setColor()
  276. }
  277. }
  278. func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  279. }
  280. func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  281. if !decelerate {
  282. if !decelerate {
  283. searchNewMedia()
  284. }
  285. }
  286. }
  287. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  288. searchNewMedia()
  289. }
  290. func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
  291. let y = view.safeAreaInsets.top
  292. scrollView.contentOffset.y = -(insetsTop + y)
  293. }
  294. }
  295. // MARK: -
  296. extension NCMedia: NCSelectDelegate {
  297. func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool, session: NCSession.Session) {
  298. guard let serverUrl else { return }
  299. let home = utilityFileSystem.getHomeServer(session: session)
  300. let mediaPath = serverUrl.replacingOccurrences(of: home, with: "")
  301. database.setAccountMediaPath(mediaPath, account: session.account)
  302. imageCache.removeAll()
  303. loadDataSource()
  304. searchNewMedia()
  305. }
  306. }