Add new media view for pictures

Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
This commit is contained in:
Marcel Müller 2024-05-01 21:25:34 +02:00
Родитель 4c2f12911c
Коммит d8e5bb6b39
4 изменённых файлов: 448 добавлений и 1 удалений

Просмотреть файл

@ -3168,7 +3168,16 @@ import QuickLook
// MARK: - FileMessageTableViewCellDelegate
public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter) {
public func cellWants(toDownloadFile fileParameter: NCMessageFileParameter, for message: NCChatMessage) {
if NCUtils.isImage(fileType: fileParameter.mimetype) {
let mediaViewController = NCMediaViewerViewController(initialMessage: message)
let navController = CustomPresentableNavigationController(rootViewController: mediaViewController)
self.present(navController, interactiveDismissalType: .standard)
return
}
if fileParameter.fileStatus != nil && fileParameter.fileStatus?.isDownloading ?? false {
print("File already downloading -> skipping new download")
return

Просмотреть файл

@ -0,0 +1,201 @@
//
// Copyright (c) 2024 Marcel Müller <marcel.mueller@nextcloud.com>
//
// Author Marcel Müller <marcel.mueller@nextcloud.com>
//
// GNU GPL version 3 or any later version
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import UIKit
@objc protocol NCMediaViewerPageViewControllerDelegate {
@objc func mediaViewerPageZoomDidChange(_ controller: NCMediaViewerPageViewController, _ scale: Double)
@objc func mediaViewerPageImageDidLoad(_ controller: NCMediaViewerPageViewController)
}
@objcMembers class NCMediaViewerPageViewController: UIViewController, NCChatFileControllerDelegate, NCZoomableViewDelegate {
public weak var delegate: NCMediaViewerPageViewControllerDelegate?
public let message: NCChatMessage
private let fileDownloader = NCChatFileController()
private lazy var zoomableView = {
let zoomableView = NCZoomableView()
zoomableView.translatesAutoresizingMaskIntoConstraints = false
zoomableView.disablePanningOnInitialZoom = true
zoomableView.delegate = self
return zoomableView
}()
private lazy var imageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = true
return imageView
}()
private lazy var errorView = {
let errorView = UIView()
errorView.translatesAutoresizingMaskIntoConstraints = false
let iconConfiguration = UIImage.SymbolConfiguration(pointSize: 36)
let errorImage = UIImageView()
errorImage.image = UIImage(systemName: "photo")?.withConfiguration(iconConfiguration)
errorImage.contentMode = .scaleAspectFit
errorImage.translatesAutoresizingMaskIntoConstraints = false
errorImage.tintColor = .label
let errorText = UILabel()
errorText.translatesAutoresizingMaskIntoConstraints = false
errorText.text = NSLocalizedString("An error occurred downloading the picture", comment: "")
errorView.addSubview(errorImage)
errorView.addSubview(errorText)
NSLayoutConstraint.activate([
errorImage.topAnchor.constraint(equalTo: errorView.topAnchor),
errorImage.widthAnchor.constraint(equalToConstant: 150),
errorImage.heightAnchor.constraint(greaterThanOrEqualToConstant: 0),
errorImage.centerXAnchor.constraint(equalTo: errorView.centerXAnchor),
errorText.topAnchor.constraint(equalTo: errorImage.bottomAnchor, constant: 10),
errorText.bottomAnchor.constraint(equalTo: errorView.bottomAnchor),
errorText.centerXAnchor.constraint(equalTo: errorView.centerXAnchor)
])
return errorView
}()
public var currentImage: UIImage? {
return self.imageView.image
}
private lazy var activityIndicator = {
let indicator = NCActivityIndicator(frame: .init(x: 0, y: 0, width: 100, height: 100))
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.cycleColors = [.lightGray]
return indicator
}()
init(message: NCChatMessage) {
self.message = message
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
self.view.addSubview(self.zoomableView)
self.view.addSubview(self.activityIndicator)
NSLayoutConstraint.activate([
self.zoomableView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
self.zoomableView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
self.zoomableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
self.zoomableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
self.activityIndicator.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
self.activityIndicator.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor)
])
self.zoomableView.replaceContentView(self.imageView)
self.activityIndicator.startAnimating()
fileDownloader.delegate = self
fileDownloader.downloadFile(fromMessage: self.message.file())
self.navigationItem.title = self.message.file().name
NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
}
override func viewDidLayoutSubviews() {
// Make sure we have the correct bounds and center the view correctly
self.zoomableView.resizeContentView()
}
func showErrorView() {
self.view.addSubview(self.errorView)
NSLayoutConstraint.activate([
self.errorView.leadingAnchor.constraint(greaterThanOrEqualTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 10),
self.errorView.trailingAnchor.constraint(greaterThanOrEqualTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
self.errorView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor),
self.errorView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor)
])
}
// MARK: - NCChatFileController delegate
func fileControllerDidLoadFile(_ fileController: NCChatFileController, with fileStatus: NCChatFileStatus) {
self.activityIndicator.stopAnimating()
self.activityIndicator.isHidden = true
if let image = UIImage(contentsOfFile: fileStatus.fileLocalPath) {
self.imageView.image = image
// Adjust the view to the new image
self.zoomableView.contentViewSize = image.size
self.zoomableView.resizeContentView()
self.delegate?.mediaViewerPageImageDidLoad(self)
} else {
self.imageView.image = nil
self.showErrorView()
print("Error in fileControllerDidLoadFile getting UIImage")
}
}
func fileControllerDidFailLoadingFile(_ fileController: NCChatFileController, withErrorDescription errorDescription: String) {
self.activityIndicator.stopAnimating()
self.activityIndicator.isHidden = true
self.showErrorView()
print("Error downloading picture: " + errorDescription)
}
func didChangeDownloadProgress(notification: Notification) {
DispatchQueue.main.async {
// Make sure this notification is really for this view controller
guard let userInfo = notification.userInfo,
let receivedStatus = userInfo["fileStatus"] as? NCChatFileStatus,
let fileParameter = self.message.file(),
receivedStatus.fileId == fileParameter.parameterId,
receivedStatus.filePath == fileParameter.path,
let progress = userInfo["progress"] as? CGFloat
else { return }
// Switch to determinate mode and set the progress
self.activityIndicator.indicatorMode = .determinate
self.activityIndicator.setProgress(Float(progress), animated: true)
}
}
// MARK: - NCZoomableView delegate
func contentViewZoomDidChange(_ view: NCZoomableView, _ scale: Double) {
self.delegate?.mediaViewerPageZoomDidChange(self, scale)
}
}

Просмотреть файл

@ -0,0 +1,195 @@
//
// Copyright (c) 2024 Marcel Müller <marcel.mueller@nextcloud.com>
//
// Author Marcel Müller <marcel.mueller@nextcloud.com>
//
// GNU GPL version 3 or any later version
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import Foundation
import UIKit
@objcMembers class NCMediaViewerViewController: UIViewController,
UIPageViewControllerDelegate,
UIPageViewControllerDataSource,
NCMediaViewerPageViewControllerDelegate {
private let pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
private var initialMessage: NCChatMessage
private lazy var shareButton = {
let shareButton = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
shareButton.isEnabled = false
shareButton.primaryAction = UIAction(title: "", image: .init(systemName: "square.and.arrow.up"), handler: { [unowned self, unowned shareButton] _ in
guard let mediaPageViewController = self.getCurrentPageViewController(),
let image = mediaPageViewController.currentImage
else { return }
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
activityViewController.popoverPresentationController?.barButtonItem = shareButton
self.present(activityViewController, animated: true)
})
return shareButton
}()
init(initialMessage: NCChatMessage) {
self.initialMessage = initialMessage
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
NCAppBranding.styleViewController(self)
self.view.backgroundColor = .systemBackground
self.setupNavigationBar()
self.pageController.delegate = self
self.pageController.dataSource = self
self.pageController.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.pageController.view)
NSLayoutConstraint.activate([
self.pageController.view.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor),
self.pageController.view.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor),
self.pageController.view.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
self.pageController.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
])
self.pageController.didMove(toParent: self)
let initialViewController = NCMediaViewerPageViewController(message: self.initialMessage)
initialViewController.delegate = self
self.pageController.setViewControllers([initialViewController], direction: .forward, animated: false)
self.navigationItem.title = initialViewController.navigationItem.title
}
func setupNavigationBar() {
let closeButton = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
closeButton.primaryAction = UIAction(title: NSLocalizedString("Close", comment: ""), handler: { [unowned self] _ in
self.dismiss(animated: true)
})
self.navigationItem.rightBarButtonItems = [closeButton]
self.navigationController?.setToolbarHidden(false, animated: false)
self.toolbarItems = [shareButton]
}
func getCurrentPageViewController() -> NCMediaViewerPageViewController? {
return self.pageController.viewControllers?.first as? NCMediaViewerPageViewController
}
// MARK: - PageViewController delegate
func getAllFileMessages() -> RLMResults<AnyObject> {
let query = NSPredicate(format: "accountId = %@ AND token = %@ AND messageParametersJSONString contains[cd] %@", self.initialMessage.accountId, self.initialMessage.token, "\"file\":")
let messages = NCChatMessage.objects(with: query).sortedResults(usingKeyPath: "messageId", ascending: true)
return messages
}
func getPreviousFileMessage(from message: NCChatMessage) -> NCChatMessage? {
let prevQuery = NSPredicate(format: "messageId < %ld", message.messageId)
let messageObject = self.getAllFileMessages().objects(with: prevQuery).lastObject()
if let message = messageObject as? NCChatMessage {
if NCUtils.isImage(fileType: message.file().mimetype) {
return message
}
// The current message contains a file, but not an image -> try to find another message
return self.getPreviousFileMessage(from: message)
}
return nil
}
func getNextFileMessage(from message: NCChatMessage) -> NCChatMessage? {
let prevQuery = NSPredicate(format: "messageId > %ld", message.messageId)
let messageObject = self.getAllFileMessages().objects(with: prevQuery).firstObject()
if let message = messageObject as? NCChatMessage {
if NCUtils.isImage(fileType: message.file().mimetype) {
return message
}
// The current message contains a file, but not an image -> try to find another message
return self.getNextFileMessage(from: message)
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let prevMediaPageVC = viewController as? NCMediaViewerPageViewController,
let prevMessage = self.getPreviousFileMessage(from: prevMediaPageVC.message)
else { return nil }
let mediaPageViewController = NCMediaViewerPageViewController(message: prevMessage)
mediaPageViewController.delegate = self
return mediaPageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let prevMediaPageVC = viewController as? NCMediaViewerPageViewController,
let nextMessage = self.getNextFileMessage(from: prevMediaPageVC.message)
else { return nil }
let mediaPageViewController = NCMediaViewerPageViewController(message: nextMessage)
mediaPageViewController.delegate = self
return mediaPageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
// Update the titel of the currently shown viewController
guard let mediaPageViewController = self.getCurrentPageViewController() else { return }
self.navigationItem.title = mediaPageViewController.navigationItem.title
self.shareButton.isEnabled = (mediaPageViewController.currentImage != nil)
}
// MARK: - NCMediaViewerPageViewController delegate
func mediaViewerPageZoomDidChange(_ controller: NCMediaViewerPageViewController, _ scale: Double) {
// Prevent the scrollView interfering with our pan gesture recognizer when the view is zoomed
// Also disable dismissal gesture when the view is zoomed
guard let navController = self.navigationController as? CustomPresentableNavigationController else { return }
if scale == 1 {
pageController.enableSwipeGesture()
navController.dismissalGestureEnabled = true
} else {
pageController.disableSwipeGesture()
navController.dismissalGestureEnabled = false
}
}
func mediaViewerPageImageDidLoad(_ controller: NCMediaViewerPageViewController) {
if let mediaPageViewController = self.getCurrentPageViewController(), mediaPageViewController.isEqual(controller) {
self.shareButton.isEnabled = true
}
}
}

Просмотреть файл

@ -0,0 +1,42 @@
//
// Copyright (c) 2024 Marcel Müller <marcel-mueller@gmx.de>
//
// Author Marcel Müller <marcel-mueller@gmx.de>
//
// GNU GPL version 3 or any later version
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import UIKit
extension UIPageViewController {
// https://stackoverflow.com/a/47075283
func enableSwipeGesture() {
for view in self.view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = true
}
}
}
func disableSwipeGesture() {
for view in self.view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = false
}
}
}
}