GalleryViewController.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import UIKit
  2. import DcCore
  3. import SDWebImage
  4. class GalleryViewController: UIViewController {
  5. // MARK: - data
  6. private let mediaMessageIds: [Int]
  7. private var items: [Int: GalleryItem] = [:]
  8. // MARK: - subview specs
  9. private let gridDefaultSpacing: CGFloat = 5
  10. private lazy var gridLayout: GridCollectionViewFlowLayout = {
  11. let layout = GridCollectionViewFlowLayout()
  12. layout.minimumLineSpacing = gridDefaultSpacing
  13. layout.minimumInteritemSpacing = gridDefaultSpacing
  14. layout.format = .square
  15. return layout
  16. }()
  17. private lazy var grid: UICollectionView = {
  18. let collection = UICollectionView(frame: .zero, collectionViewLayout: gridLayout)
  19. collection.dataSource = self
  20. collection.delegate = self
  21. collection.register(GalleryCell.self, forCellWithReuseIdentifier: GalleryCell.reuseIdentifier)
  22. collection.contentInset = UIEdgeInsets(top: gridDefaultSpacing, left: gridDefaultSpacing, bottom: gridDefaultSpacing, right: gridDefaultSpacing)
  23. collection.backgroundColor = DcColors.defaultBackgroundColor
  24. collection.delaysContentTouches = false
  25. collection.alwaysBounceVertical = true
  26. collection.isPrefetchingEnabled = true
  27. collection.prefetchDataSource = self
  28. return collection
  29. }()
  30. private lazy var timeLabel: GalleryTimeLabel = {
  31. let view = GalleryTimeLabel()
  32. view.hide(animated: false)
  33. return view
  34. }()
  35. private lazy var emptyStateView: EmptyStateLabel = {
  36. let label = EmptyStateLabel()
  37. label.text = String.localized("tab_gallery_empty_hint")
  38. label.isHidden = true
  39. return label
  40. }()
  41. init(mediaMessageIds: [Int]) {
  42. self.mediaMessageIds = mediaMessageIds
  43. super.init(nibName: nil, bundle: nil)
  44. }
  45. required init?(coder: NSCoder) {
  46. fatalError("init(coder:) has not been implemented")
  47. }
  48. // MARK: - lifecycle
  49. override func viewDidLoad() {
  50. super.viewDidLoad()
  51. setupSubviews()
  52. title = String.localized("gallery")
  53. if mediaMessageIds.isEmpty {
  54. emptyStateView.isHidden = false
  55. }
  56. }
  57. override func viewWillAppear(_ animated: Bool) {
  58. grid.reloadData()
  59. }
  60. override func viewWillLayoutSubviews() {
  61. super.viewWillLayoutSubviews()
  62. self.reloadCollectionViewLayout()
  63. }
  64. // MARK: - setup
  65. private func setupSubviews() {
  66. view.addSubview(grid)
  67. grid.translatesAutoresizingMaskIntoConstraints = false
  68. grid.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
  69. grid.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
  70. grid.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
  71. grid.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
  72. view.addSubview(timeLabel)
  73. timeLabel.translatesAutoresizingMaskIntoConstraints = false
  74. timeLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10).isActive = true
  75. timeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
  76. view.addSubview(emptyStateView)
  77. emptyStateView.translatesAutoresizingMaskIntoConstraints = false
  78. emptyStateView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor).isActive = true
  79. emptyStateView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor).isActive = true
  80. emptyStateView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true
  81. emptyStateView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
  82. }
  83. // MARK: - updates
  84. private func updateFloatingTimeLabel() {
  85. if let indexPath = grid.indexPathsForVisibleItems.min() {
  86. let msgId = mediaMessageIds[indexPath.row]
  87. let msg = DcMsg(id: msgId)
  88. timeLabel.update(date: msg.sentDate)
  89. }
  90. }
  91. }
  92. extension GalleryViewController: UICollectionViewDataSourcePrefetching {
  93. func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
  94. indexPaths.forEach { if items[$0.row] == nil {
  95. let item = GalleryItem(msgId: mediaMessageIds[$0.row])
  96. items[$0.row] = item
  97. }}
  98. }
  99. }
  100. // MARK: - UICollectionViewDataSource, UICollectionViewDelegate
  101. extension GalleryViewController: UICollectionViewDataSource, UICollectionViewDelegate {
  102. func numberOfSections(in collectionView: UICollectionView) -> Int {
  103. return 1
  104. }
  105. func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  106. return mediaMessageIds.count
  107. }
  108. func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  109. guard let galleryCell = collectionView.dequeueReusableCell(
  110. withReuseIdentifier: GalleryCell.reuseIdentifier,
  111. for: indexPath) as? GalleryCell else {
  112. return UICollectionViewCell()
  113. }
  114. let msgId = mediaMessageIds[indexPath.row]
  115. var item: GalleryItem
  116. if let galleryItem = items[indexPath.row] {
  117. item = galleryItem
  118. } else {
  119. let galleryItem = GalleryItem(msgId: msgId)
  120. items[indexPath.row] = galleryItem
  121. item = galleryItem
  122. }
  123. galleryCell.update(item: item)
  124. return galleryCell
  125. }
  126. func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  127. let msgId = mediaMessageIds[indexPath.row]
  128. showPreview(msgId: msgId)
  129. collectionView.deselectItem(at: indexPath, animated: true)
  130. }
  131. func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  132. updateFloatingTimeLabel()
  133. timeLabel.show(animated: true)
  134. }
  135. func scrollViewDidScroll(_ scrollView: UIScrollView) {
  136. updateFloatingTimeLabel()
  137. }
  138. func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  139. timeLabel.hide(animated: true)
  140. }
  141. }
  142. // MARK: - grid layout + updates
  143. private extension GalleryViewController {
  144. func reloadCollectionViewLayout() {
  145. // columns specification
  146. let phonePortrait = 3
  147. let phoneLandscape = 4
  148. let padPortrait = 5
  149. let padLandscape = 8
  150. let orientation = UIApplication.shared.statusBarOrientation
  151. let deviceType = UIDevice.current.userInterfaceIdiom
  152. var gridDisplay: GridDisplay?
  153. if deviceType == .phone {
  154. if orientation.isPortrait {
  155. gridDisplay = .grid(columns: phonePortrait)
  156. } else {
  157. gridDisplay = .grid(columns: phoneLandscape)
  158. }
  159. } else if deviceType == .pad {
  160. if orientation.isPortrait {
  161. gridDisplay = .grid(columns: padPortrait)
  162. } else {
  163. gridDisplay = .grid(columns: padLandscape)
  164. }
  165. }
  166. if let gridDisplay = gridDisplay {
  167. gridLayout.display = gridDisplay
  168. } else {
  169. safe_fatalError("undefined format")
  170. }
  171. let containerWidth = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - 2 * gridDefaultSpacing
  172. gridLayout.containerWidth = containerWidth
  173. }
  174. }
  175. // MARK: - coordinator
  176. extension GalleryViewController {
  177. func showPreview(msgId: Int) {
  178. guard let index = mediaMessageIds.index(of: msgId) else {
  179. return
  180. }
  181. let mediaUrls = mediaMessageIds.compactMap {
  182. return DcMsg(id: $0).fileURL
  183. }
  184. let previewController = PreviewController(currentIndex: index, urls: mediaUrls)
  185. present(previewController, animated: true, completion: nil)
  186. }
  187. }
  188. class GalleryItem {
  189. var onImageLoaded: ((UIImage?) -> Void)?
  190. var msg: DcMsg
  191. var fileUrl: URL? {
  192. return msg.fileURL
  193. }
  194. var thumbnailImage: UIImage? {
  195. willSet {
  196. onImageLoaded?(newValue)
  197. }
  198. }
  199. var showPlayButton: Bool {
  200. switch msg.viewtype {
  201. case .video:
  202. return true
  203. default:
  204. return false
  205. }
  206. }
  207. init(msgId: Int) {
  208. self.msg = DcMsg(id: msgId)
  209. if let key = msg.fileURL?.absoluteString, let image = ThumbnailCache.shared.restoreImage(key: key) {
  210. self.thumbnailImage = image
  211. } else {
  212. loadThumbnail()
  213. }
  214. }
  215. private func loadThumbnail() {
  216. guard let viewtype = msg.viewtype, let url = msg.fileURL else {
  217. return
  218. }
  219. switch viewtype {
  220. case .image:
  221. thumbnailImage = msg.image
  222. case .video:
  223. loadVideoThumbnail(from: url)
  224. case .gif:
  225. loadGifThumbnail(from: url)
  226. default:
  227. safe_fatalError("unsupported viewtype - viewtype \(viewtype) not supported.")
  228. }
  229. }
  230. private func loadGifThumbnail(from url: URL) {
  231. guard let imageData = try? Data(contentsOf: url) else {
  232. return
  233. }
  234. self.thumbnailImage = SDAnimatedImage(data: imageData)
  235. }
  236. private func loadVideoThumbnail(from url: URL) {
  237. DispatchQueue.global(qos: .background).async {
  238. let thumbnailImage = DcUtils.generateThumbnailFromVideo(url: url)
  239. DispatchQueue.main.async { [weak self] in
  240. self?.thumbnailImage = thumbnailImage
  241. if let image = thumbnailImage {
  242. ThumbnailCache.shared.storeImage(image: image, key: url.absoluteString)
  243. }
  244. }
  245. }
  246. }
  247. }