NCViewerQuickLook.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. //
  2. // NCViewerQuickLook.swift
  3. // Nextcloud
  4. //
  5. // Created by Marino Faggiana on 03/05/2020.
  6. // Copyright © 2020 Marino Faggiana. All rights reserved.
  7. // Copyright © 2022 Henrik Storch. All rights reserved.
  8. // Copyright © 2023 Marino Faggiana. All rights reserved.
  9. //
  10. // Author Marino Faggiana <marino.faggiana@nextcloud.com>
  11. // Author Henrik Storch <henrik.storch@nextcloud.com>
  12. //
  13. // This program is free software: you can redistribute it and/or modify
  14. // it under the terms of the GNU General Public License as published by
  15. // the Free Software Foundation, either version 3 of the License, or
  16. // (at your option) any later version.
  17. //
  18. // This program is distributed in the hope that it will be useful,
  19. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. // GNU General Public License for more details.
  22. //
  23. // You should have received a copy of the GNU General Public License
  24. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. //
  26. import UIKit
  27. import QuickLook
  28. import NextcloudKit
  29. import Mantis
  30. import SwiftUI
  31. public protocol NCViewerQuickLookDelegate: AnyObject {
  32. func dismissQuickLook(fileNameSource: String, hasChangesQuickLook: Bool)
  33. }
  34. // optional func
  35. public extension NCViewerQuickLookDelegate {
  36. func dismissQuickLook(fileNameSource: String, hasChangesQuickLook: Bool) {}
  37. }
  38. // if the document has any changes
  39. private var hasChangesQuickLook: Bool = false
  40. @objc class NCViewerQuickLook: QLPreviewController {
  41. private let url: URL
  42. private let fileNameSource: String
  43. private var previewItems: [PreviewItem] = []
  44. private var isEditingEnabled: Bool
  45. private var metadata: tableMetadata?
  46. private var timer: Timer?
  47. // used to display the save alert
  48. private var parentVC: UIViewController?
  49. private let utilityFileSystem = NCUtilityFileSystem()
  50. public var saveAsCopyAlert: Bool = true
  51. public var uploadMetadata: Bool = true
  52. public weak var delegateQuickLook: NCViewerQuickLookDelegate?
  53. required init?(coder: NSCoder) {
  54. fatalError("init(coder:) has not been implemented")
  55. }
  56. @objc init(with url: URL, fileNameSource: String = "", isEditingEnabled: Bool, metadata: tableMetadata?) {
  57. self.url = url
  58. self.fileNameSource = fileNameSource
  59. self.isEditingEnabled = isEditingEnabled
  60. if let metadata = metadata {
  61. self.metadata = tableMetadata.init(value: metadata)
  62. }
  63. let previewItem = PreviewItem()
  64. previewItem.previewItemURL = url
  65. self.previewItems.append(previewItem)
  66. super.init(nibName: nil, bundle: nil)
  67. self.dataSource = self
  68. self.delegate = self
  69. self.currentPreviewItemIndex = 0
  70. hasChangesQuickLook = false
  71. }
  72. override func viewDidLoad() {
  73. super.viewDidLoad()
  74. guard isEditingEnabled else { return }
  75. if metadata?.isLivePhoto == true {
  76. let error = NKError(errorCode: NCGlobal.shared.errorCharactersForbidden, errorDescription: "_message_disable_overwrite_livephoto_")
  77. NCContentPresenter().showInfo(error: error)
  78. }
  79. if let metadata = metadata, metadata.isImage {
  80. let buttonDone = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissView))
  81. let buttonCrop = UIBarButtonItem(image: NCUtility().loadImage(named: "crop"), style: .plain, target: self, action: #selector(crop))
  82. navigationItem.leftBarButtonItems = [buttonDone, buttonCrop]
  83. startTimer(navigationItem: navigationItem)
  84. }
  85. }
  86. override func viewDidAppear(_ animated: Bool) {
  87. super.viewDidAppear(animated)
  88. // needs to be saved bc in didDisappear presentingVC is already nil
  89. parentVC = presentingViewController
  90. }
  91. override func viewDidDisappear(_ animated: Bool) {
  92. super.viewDidDisappear(animated)
  93. if let metadata = metadata, metadata.classFile != NKCommon.TypeClassFile.image.rawValue {
  94. dismissView()
  95. }
  96. }
  97. func startTimer(navigationItem: UINavigationItem) {
  98. self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
  99. guard let buttonDone = navigationItem.leftBarButtonItems?.first, let buttonCrop = navigationItem.leftBarButtonItems?.last else { return }
  100. buttonCrop.isEnabled = true
  101. buttonDone.isEnabled = true
  102. if let markup = navigationItem.rightBarButtonItems?.first(where: { $0.accessibilityIdentifier == "QLOverlayMarkupButtonAccessibilityIdentifier" }) {
  103. if let originalButton = markup.value(forKey: "originalButton") as AnyObject? {
  104. if let symbolImageName = originalButton.value(forKey: "symbolImageName") as? String {
  105. if symbolImageName == "pencil.tip.crop.circle.on" {
  106. buttonCrop.isEnabled = false
  107. buttonDone.isEnabled = false
  108. }
  109. }
  110. }
  111. }
  112. })
  113. }
  114. @objc private func dismissView() {
  115. guard isEditingEnabled, hasChangesQuickLook, let metadata = metadata else {
  116. dismiss(animated: true)
  117. return
  118. }
  119. let alertController = UIAlertController(title: NSLocalizedString("_save_", comment: ""), message: nil, preferredStyle: .alert)
  120. var message: String?
  121. if metadata.isLivePhoto {
  122. message = NSLocalizedString("_message_disable_overwrite_livephoto_", comment: "")
  123. } else if metadata.lock {
  124. message = NSLocalizedString("_file_locked_no_override_", comment: "")
  125. } else {
  126. alertController.addAction(UIAlertAction(title: NSLocalizedString("_overwrite_original_", comment: ""), style: .default) { _ in
  127. self.saveModifiedFile(override: true)
  128. self.delegateQuickLook?.dismissQuickLook(fileNameSource: self.fileNameSource, hasChangesQuickLook: hasChangesQuickLook)
  129. })
  130. }
  131. alertController.message = message
  132. if saveAsCopyAlert {
  133. alertController.addAction(UIAlertAction(title: NSLocalizedString("_save_as_copy_", comment: ""), style: .default) { _ in
  134. self.saveModifiedFile(override: false)
  135. self.delegateQuickLook?.dismissQuickLook(fileNameSource: self.fileNameSource, hasChangesQuickLook: hasChangesQuickLook)
  136. })
  137. }
  138. alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { _ in
  139. })
  140. alertController.addAction(UIAlertAction(title: NSLocalizedString("_discard_changes_", comment: ""), style: .destructive) { _ in
  141. self.dismiss(animated: true)
  142. })
  143. if metadata.isImage {
  144. present(alertController, animated: true)
  145. } else {
  146. parentVC?.present(alertController, animated: true)
  147. }
  148. }
  149. @objc private func crop() {
  150. guard let image = UIImage(contentsOfFile: url.path) else { return }
  151. var toolbarConfig = CropToolbarConfig()
  152. toolbarConfig.heightForVerticalOrientation = 80
  153. toolbarConfig.widthForHorizontalOrientation = 100
  154. toolbarConfig.optionButtonFontSize = 16
  155. toolbarConfig.optionButtonFontSizeForPad = 21
  156. toolbarConfig.backgroundColor = .systemGray6
  157. toolbarConfig.foregroundColor = .systemBlue
  158. var viewConfig = CropViewConfig()
  159. viewConfig.cropMaskVisualEffectType = .none
  160. viewConfig.cropBorderColor = .red
  161. var config = Mantis.Config()
  162. if let bundleIdentifier = Bundle.main.bundleIdentifier {
  163. config.localizationConfig.bundle = Bundle(identifier: bundleIdentifier)
  164. config.localizationConfig.tableName = "Localizable"
  165. }
  166. config.cropToolbarConfig = toolbarConfig
  167. config.cropViewConfig = viewConfig
  168. let toolbar = CropToolbar()
  169. toolbar.iconProvider = CropToolbarIcon()
  170. let cropViewController = Mantis.cropViewController(image: image, config: config, cropToolbar: toolbar)
  171. cropViewController.delegate = self
  172. cropViewController.backgroundColor = .systemBackground
  173. cropViewController.modalPresentationStyle = .fullScreen
  174. self.present(cropViewController, animated: true)
  175. }
  176. }
  177. extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerDelegate {
  178. func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  179. previewItems.count
  180. }
  181. func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  182. previewItems[index]
  183. }
  184. func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
  185. return isEditingEnabled ? .createCopy : .disabled
  186. }
  187. fileprivate func saveModifiedFile(override: Bool) {
  188. guard let metadata = self.metadata else { return }
  189. let session = NCSession.shared.getSession(account: metadata.account)
  190. if !uploadMetadata {
  191. return self.dismiss(animated: true)
  192. }
  193. let ocId = NSUUID().uuidString
  194. let size = utilityFileSystem.getFileSize(filePath: url.path)
  195. if !override {
  196. let fileName = utilityFileSystem.createFileName(metadata.fileNameView, serverUrl: metadata.serverUrl, account: metadata.account)
  197. metadata.fileName = fileName
  198. metadata.fileNameView = fileName
  199. }
  200. let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(ocId, fileNameView: metadata.fileNameView)
  201. guard utilityFileSystem.copyFile(atPath: url.path, toPath: fileNamePath) else { return }
  202. let metadataForUpload = NCManageDatabase.shared.createMetadata(fileName: metadata.fileName,
  203. fileNameView: metadata.fileNameView,
  204. ocId: ocId,
  205. serverUrl: metadata.serverUrl,
  206. url: url.path,
  207. contentType: "",
  208. session: session,
  209. sceneIdentifier: nil)
  210. metadataForUpload.session = NCNetworking.shared.sessionUploadBackground
  211. if override {
  212. metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFileNODelete
  213. } else {
  214. metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFile
  215. }
  216. metadataForUpload.size = size
  217. metadataForUpload.status = NCGlobal.shared.metadataStatusWaitUpload
  218. metadataForUpload.sessionDate = Date()
  219. NCNetworkingProcess.shared.createProcessUploads(metadatas: [metadataForUpload]) { _ in
  220. self.dismiss(animated: true)
  221. }
  222. }
  223. func previewController(_ controller: QLPreviewController, didSaveEditedCopyOf previewItem: QLPreviewItem, at modifiedContentsURL: URL) {
  224. // easier to handle that way than to use `.updateContents`
  225. // needs to be moved otherwise it will only be called once!
  226. guard utilityFileSystem.moveFile(atPath: modifiedContentsURL.path, toPath: url.path) else { return }
  227. hasChangesQuickLook = true
  228. }
  229. }
  230. extension NCViewerQuickLook: CropViewControllerDelegate {
  231. func cropViewControllerDidCrop(_ cropViewController: Mantis.CropViewController, cropped: UIImage, transformation: Mantis.Transformation, cropInfo: Mantis.CropInfo) {
  232. cropViewController.dismiss(animated: true)
  233. guard let data = cropped.jpegData(compressionQuality: 0.9) else { return }
  234. do {
  235. try data.write(to: self.url)
  236. hasChangesQuickLook = true
  237. reloadData()
  238. } catch {
  239. print(error)
  240. }
  241. }
  242. func cropViewControllerDidCancel(_ cropViewController: Mantis.CropViewController, original: UIImage) {
  243. cropViewController.dismiss(animated: true)
  244. }
  245. }
  246. class PreviewItem: NSObject, QLPreviewItem {
  247. var previewItemURL: URL?
  248. }
  249. class CropToolbarIcon: CropToolbarIconProvider {
  250. func getCropIcon() -> UIImage? {
  251. return NCUtility().loadImage(named: "checkmark.circle")
  252. }
  253. func getCancelIcon() -> UIImage? {
  254. return NCUtility().loadImage(named: "xmark.circle")
  255. }
  256. }