зеркало из https://github.com/nextcloud/talk-ios.git
Add new media view for pictures
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
This commit is contained in:
Родитель
4c2f12911c
Коммит
d8e5bb6b39
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче