NCViewerQuickLook.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. // if the document has any changes
  32. private var hasChangesQuickLook: Bool = false
  33. @objc class NCViewerQuickLook: QLPreviewController {
  34. let url: URL
  35. var previewItems: [PreviewItem] = []
  36. var isEditingEnabled: Bool
  37. var metadata: tableMetadata?
  38. var timer: Timer?
  39. // used to display the save alert
  40. var parentVC: UIViewController?
  41. required init?(coder: NSCoder) {
  42. fatalError("init(coder:) has not been implemented")
  43. }
  44. @objc init(with url: URL, isEditingEnabled: Bool, metadata: tableMetadata?) {
  45. self.url = url
  46. self.isEditingEnabled = isEditingEnabled
  47. if let metadata = metadata {
  48. self.metadata = tableMetadata.init(value: metadata)
  49. }
  50. let previewItem = PreviewItem()
  51. previewItem.previewItemURL = url
  52. self.previewItems.append(previewItem)
  53. super.init(nibName: nil, bundle: nil)
  54. self.dataSource = self
  55. self.delegate = self
  56. self.currentPreviewItemIndex = 0
  57. hasChangesQuickLook = false
  58. }
  59. override func viewDidLoad() {
  60. super.viewDidLoad()
  61. guard isEditingEnabled else { return }
  62. if metadata?.livePhoto == true {
  63. let error = NKError(errorCode: NCGlobal.shared.errorCharactersForbidden, errorDescription: "_message_disable_overwrite_livephoto_")
  64. NCContentPresenter.shared.showInfo(error: error)
  65. }
  66. if let metadata = metadata, metadata.isImage {
  67. let buttonDone = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissView))
  68. let buttonCrop = UIBarButtonItem(image: UIImage(systemName: "crop"), style: .plain, target: self, action: #selector(crop))
  69. navigationItem.leftBarButtonItems = [buttonDone, buttonCrop]
  70. startTimer(navigationItem: navigationItem)
  71. }
  72. }
  73. override func viewDidAppear(_ animated: Bool) {
  74. super.viewDidAppear(animated)
  75. // needs to be saved bc in didDisappear presentingVC is already nil
  76. parentVC = presentingViewController
  77. }
  78. override func viewDidDisappear(_ animated: Bool) {
  79. super.viewDidDisappear(animated)
  80. if let metadata = metadata, metadata.classFile != NKCommon.TypeClassFile.image.rawValue {
  81. dismissView()
  82. }
  83. }
  84. func startTimer(navigationItem: UINavigationItem) {
  85. self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in
  86. guard let buttonDone = navigationItem.leftBarButtonItems?.first, let buttonCrop = navigationItem.leftBarButtonItems?.last else { return }
  87. buttonCrop.isEnabled = true
  88. buttonDone.isEnabled = true
  89. if let markup = navigationItem.rightBarButtonItems?.first(where: { $0.accessibilityIdentifier == "QLOverlayMarkupButtonAccessibilityIdentifier" }) {
  90. if let originalButton = markup.value(forKey: "originalButton") as AnyObject? {
  91. if let symbolImageName = originalButton.value(forKey: "symbolImageName") as? String {
  92. if symbolImageName == "pencil.tip.crop.circle.on" {
  93. buttonCrop.isEnabled = false
  94. buttonDone.isEnabled = false
  95. }
  96. }
  97. }
  98. }
  99. })
  100. }
  101. @objc private func dismissView() {
  102. guard isEditingEnabled, hasChangesQuickLook, let metadata = metadata else {
  103. dismiss(animated: true)
  104. return
  105. }
  106. let alertController = UIAlertController(title: NSLocalizedString("_save_", comment: ""), message: nil, preferredStyle: .alert)
  107. var message: String?
  108. if metadata.livePhoto {
  109. message = NSLocalizedString("_message_disable_overwrite_livephoto_", comment: "")
  110. } else if metadata.lock {
  111. message = NSLocalizedString("_file_locked_no_override_", comment: "")
  112. } else {
  113. alertController.addAction(UIAlertAction(title: NSLocalizedString("_overwrite_original_", comment: ""), style: .default) { _ in
  114. self.saveModifiedFile(override: true)
  115. })
  116. }
  117. alertController.message = message
  118. alertController.addAction(UIAlertAction(title: NSLocalizedString("_save_as_copy_", comment: ""), style: .default) { _ in
  119. self.saveModifiedFile(override: false)
  120. })
  121. alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) { _ in
  122. })
  123. alertController.addAction(UIAlertAction(title: NSLocalizedString("_discard_changes_", comment: ""), style: .destructive) { _ in
  124. self.dismiss(animated: true)
  125. })
  126. if metadata.isImage {
  127. present(alertController, animated: true)
  128. } else {
  129. parentVC?.present(alertController, animated: true)
  130. }
  131. }
  132. @objc private func crop() {
  133. guard let image = UIImage(contentsOfFile: url.path) else { return }
  134. var toolbarConfig = CropToolbarConfig()
  135. toolbarConfig.heightForVerticalOrientation = 80
  136. toolbarConfig.widthForHorizontalOrientation = 100
  137. toolbarConfig.optionButtonFontSize = 16
  138. toolbarConfig.optionButtonFontSizeForPad = 21
  139. toolbarConfig.backgroundColor = .systemGray6
  140. toolbarConfig.foregroundColor = .systemBlue
  141. var viewConfig = CropViewConfig()
  142. viewConfig.cropMaskVisualEffectType = .none
  143. viewConfig.cropBorderColor = .red
  144. var config = Mantis.Config()
  145. if let bundleIdentifier = Bundle.main.bundleIdentifier {
  146. config.localizationConfig.bundle = Bundle(identifier: bundleIdentifier)
  147. config.localizationConfig.tableName = "Localizable"
  148. }
  149. config.cropToolbarConfig = toolbarConfig
  150. config.cropViewConfig = viewConfig
  151. let toolbar = CropToolbar()
  152. toolbar.iconProvider = CropToolbarIcon()
  153. let cropViewController = Mantis.cropViewController(image: image, config: config, cropToolbar: toolbar)
  154. cropViewController.delegate = self
  155. cropViewController.backgroundColor = .systemBackground
  156. cropViewController.modalPresentationStyle = .fullScreen
  157. self.present(cropViewController, animated: true)
  158. }
  159. }
  160. extension NCViewerQuickLook: QLPreviewControllerDataSource, QLPreviewControllerDelegate {
  161. func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
  162. previewItems.count
  163. }
  164. func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
  165. previewItems[index]
  166. }
  167. func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
  168. return isEditingEnabled ? .createCopy : .disabled
  169. }
  170. fileprivate func saveModifiedFile(override: Bool) {
  171. guard let metadata = self.metadata else { return }
  172. let ocId = NSUUID().uuidString
  173. let size = NCUtilityFileSystem.shared.getFileSize(filePath: url.path)
  174. if !override {
  175. let fileName = NCUtilityFileSystem.shared.createFileName(metadata.fileNameView, serverUrl: metadata.serverUrl, account: metadata.account)
  176. metadata.fileName = fileName
  177. metadata.fileNameView = fileName
  178. }
  179. guard let fileNamePath = CCUtility.getDirectoryProviderStorageOcId(ocId, fileNameView: metadata.fileNameView),
  180. NCUtilityFileSystem.shared.copyFile(atPath: url.path, toPath: fileNamePath) else { return }
  181. let metadataForUpload = NCManageDatabase.shared.createMetadata(
  182. account: metadata.account,
  183. user: metadata.user,
  184. userId: metadata.userId,
  185. fileName: metadata.fileName,
  186. fileNameView: metadata.fileNameView,
  187. ocId: ocId,
  188. serverUrl: metadata.serverUrl,
  189. urlBase: metadata.urlBase,
  190. url: url.path,
  191. contentType: "")
  192. metadataForUpload.session = NCNetworking.shared.sessionIdentifierBackground
  193. if override {
  194. metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFileNODelete
  195. } else {
  196. metadataForUpload.sessionSelector = NCGlobal.shared.selectorUploadFile
  197. }
  198. metadataForUpload.size = size
  199. metadataForUpload.status = NCGlobal.shared.metadataStatusWaitUpload
  200. NCNetworkingProcessUpload.shared.createProcessUploads(metadatas: [metadataForUpload]) { _ in
  201. self.dismiss(animated: true)
  202. }
  203. }
  204. func previewController(_ controller: QLPreviewController, didSaveEditedCopyOf previewItem: QLPreviewItem, at modifiedContentsURL: URL) {
  205. // easier to handle that way than to use `.updateContents`
  206. // needs to be moved otherwise it will only be called once!
  207. guard NCUtilityFileSystem.shared.moveFile(atPath: modifiedContentsURL.path, toPath: url.path) else { return }
  208. hasChangesQuickLook = true
  209. }
  210. }
  211. extension NCViewerQuickLook: CropViewControllerDelegate {
  212. func cropViewControllerDidCrop(_ cropViewController: Mantis.CropViewController, cropped: UIImage, transformation: Mantis.Transformation, cropInfo: Mantis.CropInfo) {
  213. cropViewController.dismiss(animated: true)
  214. guard let data = cropped.jpegData(compressionQuality: 0.9) else { return }
  215. do {
  216. try data.write(to: self.url)
  217. hasChangesQuickLook = true
  218. reloadData()
  219. } catch {
  220. print(error)
  221. }
  222. }
  223. func cropViewControllerDidCancel(_ cropViewController: Mantis.CropViewController, original: UIImage) {
  224. cropViewController.dismiss(animated: true)
  225. }
  226. }
  227. class PreviewItem: NSObject, QLPreviewItem {
  228. var previewItemURL: URL?
  229. }
  230. class CropToolbarIcon: CropToolbarIconProvider {
  231. func getCropIcon() -> UIImage? {
  232. return UIImage(systemName: "checkmark.circle")
  233. }
  234. func getCancelIcon() -> UIImage? {
  235. return UIImage(systemName: "xmark.circle")
  236. }
  237. }