generated from element-hq/.github
-
Notifications
You must be signed in to change notification settings - Fork 112
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update the timeline media QuickLook modifier. (#3593)
Not hooked up to any flows yet.
- Loading branch information
Showing
31 changed files
with
1,052 additions
and
20 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
// | ||
// Copyright 2024 New Vector Ltd. | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
// Please see LICENSE in the repository root for full details. | ||
// | ||
|
||
import Combine | ||
import Compound | ||
import QuickLook | ||
import SwiftUI | ||
|
||
class TimelineMediaPreviewController: QLPreviewController, QLPreviewControllerDataSource { | ||
private let viewModel: TimelineMediaPreviewViewModel | ||
|
||
private var cancellables: Set<AnyCancellable> = [] | ||
|
||
private let headerHostingController: UIHostingController<HeaderView> | ||
private let captionHostingController: UIHostingController<CaptionView> | ||
private let detailsHostingController: UIHostingController<TimelineMediaPreviewDetailsView> | ||
|
||
private var navigationBar: UINavigationBar? { view.subviews.first?.subviews.first { $0 is UINavigationBar } as? UINavigationBar } | ||
private var toolbar: UIToolbar? { view.subviews.first?.subviews.last { $0 is UIToolbar } as? UIToolbar } | ||
private var captionView: UIView { captionHostingController.view } | ||
|
||
init(viewModel: TimelineMediaPreviewViewModel) { | ||
self.viewModel = viewModel | ||
|
||
headerHostingController = UIHostingController(rootView: HeaderView(context: viewModel.context)) | ||
headerHostingController.view.backgroundColor = .clear | ||
captionHostingController = UIHostingController(rootView: CaptionView(context: viewModel.context)) | ||
captionHostingController.view.backgroundColor = .clear | ||
detailsHostingController = UIHostingController(rootView: TimelineMediaPreviewDetailsView(context: viewModel.context)) | ||
detailsHostingController.view.backgroundColor = .compound.bgCanvasDefault | ||
|
||
// let materialView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial)) | ||
// captionHostingController.view.insertMatchedSubview(materialView, at: 0) | ||
|
||
super.init(nibName: nil, bundle: nil) | ||
|
||
view.addSubview(captionView) | ||
|
||
// Observation of currentPreviewItem doesn't work, so use the index instead. | ||
publisher(for: \.currentPreviewItemIndex) | ||
.sink { [weak self] _ in | ||
guard let self, let currentPreviewItem = currentPreviewItem as? TimelineMediaPreviewItem else { return } | ||
Task { await self.viewModel.updateCurrentItem(currentPreviewItem) } | ||
} | ||
.store(in: &cancellables) | ||
|
||
viewModel.actions | ||
.sink { [weak self] action in | ||
switch action { | ||
case .loadedMediaFile: | ||
self?.refreshCurrentPreviewItem() | ||
case .viewInTimeline: | ||
self?.dismiss(animated: true) // Dismiss the details sheet. | ||
// Errrr, hmmmmm, do something else here. | ||
} | ||
} | ||
.store(in: &cancellables) | ||
|
||
dataSource = self | ||
} | ||
|
||
@available(*, unavailable) required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
override func viewWillLayoutSubviews() { | ||
super.viewWillLayoutSubviews() | ||
|
||
overrideUserInterfaceStyle = .dark | ||
|
||
if let toolbar { | ||
captionView.isHidden = toolbar.alpha == 0 | ||
|
||
if captionView.constraints.isEmpty { | ||
captionView.translatesAutoresizingMaskIntoConstraints = false | ||
NSLayoutConstraint.activate([ | ||
captionView.bottomAnchor.constraint(equalTo: toolbar.topAnchor), | ||
captionView.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor), | ||
captionView.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor) | ||
]) | ||
} | ||
} | ||
|
||
navigationBar?.topItem?.titleView = headerHostingController.view | ||
|
||
if navigationBar?.topItem?.rightBarButtonItems?.count == 1 { | ||
navigationBar?.topItem?.rightBarButtonItems?.append(UIBarButtonItem(image: UIImage(systemSymbol: .infoCircle), style: .plain, target: self, action: #selector(presentMediaDetails))) | ||
} | ||
} | ||
|
||
// MARK: QLPreviewControllerDataSource | ||
|
||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { | ||
viewModel.state.previewItems.count | ||
} | ||
|
||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { | ||
viewModel.state.previewItems[index] | ||
} | ||
|
||
// MARK: Private | ||
|
||
@objc func presentMediaDetails() { | ||
detailsHostingController.overrideUserInterfaceStyle = .dark | ||
detailsHostingController.sheetPresentationController?.detents = [.medium()] | ||
detailsHostingController.sheetPresentationController?.prefersGrabberVisible = true | ||
|
||
present(detailsHostingController, animated: true) | ||
} | ||
} | ||
|
||
// MARK: - Subviews | ||
|
||
private struct HeaderView: View { | ||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context | ||
|
||
var body: some View { | ||
VStack(spacing: 0) { | ||
Text(context.viewState.currentItem?.sender.displayName ?? context.viewState.currentItem?.sender.id ?? L10n.commonLoading) | ||
.font(.compound.bodySMSemibold) | ||
.foregroundStyle(.compound.textPrimary) | ||
Text(context.viewState.currentItem?.timestamp.formatted(date: .abbreviated, time: .omitted) ?? "") | ||
.font(.compound.bodyXS) | ||
.foregroundStyle(.compound.textPrimary) | ||
.textCase(.uppercase) | ||
} | ||
} | ||
} | ||
|
||
private struct CaptionView: View { | ||
@ObservedObject var context: TimelineMediaPreviewViewModel.Context | ||
|
||
var body: some View { | ||
if let caption = context.viewState.currentItem?.caption { | ||
Text(caption) | ||
.font(.compound.bodyLG) | ||
.foregroundStyle(.compound.textPrimary) | ||
.lineLimit(5) | ||
.frame(maxWidth: .infinity, alignment: .leading) | ||
.fixedSize(horizontal: false, vertical: true) | ||
.padding(16) | ||
.background(.ultraThinMaterial) | ||
} | ||
} | ||
} |
162 changes: 162 additions & 0 deletions
162
ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// | ||
// Copyright 2024 New Vector Ltd. | ||
// | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
// Please see LICENSE in the repository root for full details. | ||
// | ||
|
||
import QuickLook | ||
|
||
enum TimelineMediaPreviewViewModelAction { | ||
case loadedMediaFile | ||
case viewInTimeline | ||
} | ||
|
||
struct TimelineMediaPreviewViewState: BindableState { | ||
var previewItems: [TimelineMediaPreviewItem] | ||
var currentItem: TimelineMediaPreviewItem? | ||
} | ||
|
||
/// Wraps a media file and title to be previewed with QuickLook. | ||
class TimelineMediaPreviewItem: NSObject, QLPreviewItem { | ||
private let timelineItem: EventBasedMessageTimelineItemProtocol | ||
var fileHandle: MediaFileHandleProxy? | ||
|
||
init(timelineItem: EventBasedMessageTimelineItemProtocol) { | ||
self.timelineItem = timelineItem | ||
} | ||
|
||
var id: TimelineItemIdentifier { timelineItem.id } | ||
|
||
// MARK: QLPreviewItem | ||
|
||
var previewItemURL: URL? { | ||
fileHandle?.url | ||
} | ||
|
||
var previewItemTitle: String? { | ||
switch timelineItem { | ||
case let audioItem as AudioRoomTimelineItem: | ||
audioItem.content.filename | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.filename | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.filename | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.filename | ||
default: | ||
nil | ||
} | ||
} | ||
|
||
// MARK: Event details | ||
|
||
var sender: TimelineItemSender { | ||
timelineItem.sender | ||
} | ||
|
||
var timestamp: Date { | ||
timelineItem.timestamp | ||
} | ||
|
||
// MARK: Media details | ||
|
||
var mediaSource: MediaSourceProxy? { | ||
switch timelineItem { | ||
case let audioItem as AudioRoomTimelineItem: | ||
audioItem.content.source | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.source | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.imageInfo.source | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.videoInfo.source | ||
default: | ||
nil | ||
} | ||
} | ||
|
||
var thumbnailMediaSource: MediaSourceProxy? { | ||
switch timelineItem { | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.thumbnailSource | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.thumbnailInfo?.source | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.thumbnailInfo?.source | ||
default: | ||
nil | ||
} | ||
} | ||
|
||
var filename: String? { | ||
switch timelineItem { | ||
case let audioItem as AudioRoomTimelineItem: | ||
audioItem.content.filename | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.filename | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.filename | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.filename | ||
default: | ||
nil | ||
} | ||
} | ||
|
||
var fileSize: Double? { | ||
previewItemURL.flatMap { try? FileManager.default.sizeForItem(at: $0) } ?? expectedFileSize | ||
} | ||
|
||
private var expectedFileSize: Double? { | ||
let fileSize: UInt? = switch timelineItem { | ||
case let audioItem as AudioRoomTimelineItem: | ||
audioItem.content.fileSize | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.fileSize | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.imageInfo.fileSize | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.videoInfo.fileSize | ||
default: | ||
nil | ||
} | ||
|
||
return fileSize.map(Double.init) | ||
} | ||
|
||
var caption: String? { | ||
timelineItem.mediaCaption | ||
} | ||
|
||
var contentType: String? { | ||
switch timelineItem { | ||
case let audioItem as AudioRoomTimelineItem: | ||
audioItem.content.contentType?.localizedDescription | ||
case let fileItem as FileRoomTimelineItem: | ||
fileItem.content.contentType?.localizedDescription | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.contentType?.localizedDescription | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.contentType?.localizedDescription | ||
default: | ||
nil | ||
} | ||
} | ||
|
||
var blurhash: String? { | ||
switch timelineItem { | ||
case let imageItem as ImageRoomTimelineItem: | ||
imageItem.content.blurhash | ||
case let videoItem as VideoRoomTimelineItem: | ||
videoItem.content.blurhash | ||
default: | ||
nil | ||
} | ||
} | ||
} | ||
|
||
enum TimelineMediaPreviewViewAction { | ||
case viewInTimeline | ||
case redact | ||
} |
Oops, something went wrong.