NCMedia.swift 15 KB

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