QuickLook media. (#447)

* Use QL previews for video and present full screen.
* Use URL(staticString:) in more places.
* Fix DesignKit issues.
This commit is contained in:
Doug
2023-01-12 17:37:33 +00:00
committed by GitHub
parent 3153c0f0d9
commit c383029203
20 changed files with 135 additions and 349 deletions
@@ -64,10 +64,7 @@ extension LoginHomeserver {
/// A mock homeserver that supports only supports authentication via a single SSO provider.
static var mockOIDC: LoginHomeserver {
guard let issuerURL = URL(string: "https://auth.company.com") else {
fatalError("This shoud never fail parsing")
}
let issuerURL = URL(staticString: "https://auth.company.com")
return LoginHomeserver(address: "company.com", loginMode: .oidc(issuerURL))
}
@@ -22,19 +22,27 @@ struct FilePreviewScreen: View {
@ObservedObject var context: FilePreviewViewModel.Context
var body: some View {
PreviewController(fileURL: context.viewState.fileURL, title: context.viewState.title)
.ignoresSafeArea(.all, edges: [.horizontal, .bottom])
.navigationTitle(ElementL10n.attachmentTypeFile)
PreviewView(context: context,
fileURL: context.viewState.fileURL,
title: context.viewState.title)
.ignoresSafeArea()
}
}
private struct PreviewController: UIViewControllerRepresentable {
private struct PreviewView: UIViewControllerRepresentable {
let context: FilePreviewViewModel.Context
let fileURL: URL
let title: String?
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: context.coordinator,
action: #selector(Coordinator.done))
controller.navigationItem.rightBarButtonItem = doneButton
return UINavigationController(rootViewController: controller)
}
@@ -42,22 +50,28 @@ private struct PreviewController: UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
Coordinator(view: self)
}
class Coordinator: NSObject, QLPreviewControllerDataSource {
let view: PreviewView
class Coordinator: QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
init(view: PreviewView) {
self.view = view
}
@objc func done() {
Task { await view.context.send(viewAction: .cancel) }
}
// MARK: - QLPreviewControllerDataSource
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
PreviewItem(previewItemURL: parent.fileURL, previewItemTitle: parent.title)
PreviewItem(previewItemURL: view.fileURL, previewItemTitle: view.title)
}
}
}
@@ -55,9 +55,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
switch result {
case .displayRoomDetails:
self.displayRoomDetails()
case .displayVideo(let videoURL):
self.displayVideo(for: videoURL)
case .displayFile(let fileURL, let title):
case .displayVideo(let fileURL, let title), .displayFile(let fileURL, let title):
self.displayFile(for: fileURL, with: title)
case .displayEmojiPicker(let itemId):
self.displayEmojiPickerScreen(for: itemId)
@@ -81,25 +79,14 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
// MARK: - Private
private func displayVideo(for videoURL: URL) {
let params = VideoPlayerCoordinatorParameters(videoURL: videoURL)
let coordinator = VideoPlayerCoordinator(parameters: params)
coordinator.callback = { [weak self] _ in
self?.navigationStackCoordinator.pop()
}
navigationStackCoordinator.push(coordinator)
}
private func displayFile(for fileURL: URL, with title: String?) {
let params = FilePreviewCoordinatorParameters(fileURL: fileURL, title: title)
let coordinator = FilePreviewCoordinator(parameters: params)
coordinator.callback = { [weak self] _ in
self?.navigationStackCoordinator.pop()
self?.navigationStackCoordinator.setFullScreenCoverCoordinator(nil)
}
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator.setFullScreenCoverCoordinator(coordinator)
}
private func displayEmojiPickerScreen(for itemId: String) {
@@ -19,7 +19,7 @@ import UIKit
enum RoomScreenViewModelAction {
case displayRoomDetails
case displayVideo(videoURL: URL)
case displayVideo(videoURL: URL, title: String?)
case displayFile(fileURL: URL, title: String?)
case displayEmojiPicker(itemId: String)
}
@@ -140,8 +140,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
let action = await timelineController.processItemTap(itemId)
switch action {
case .displayVideo(let videoURL):
callback?(.displayVideo(videoURL: videoURL))
case .displayVideo(let videoURL, let title):
callback?(.displayVideo(videoURL: videoURL, title: title))
case .displayFile(let fileURL, let title):
callback?(.displayFile(fileURL: fileURL, title: title))
case .none:
@@ -1,83 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVKit
import SwiftUI
struct VideoPlayerCoordinatorParameters {
let videoURL: URL
}
enum VideoPlayerCoordinatorAction {
case cancel
}
final class VideoPlayerCoordinator: CoordinatorProtocol {
private let parameters: VideoPlayerCoordinatorParameters
private var viewModel: VideoPlayerViewModelProtocol
var callback: ((VideoPlayerCoordinatorAction) -> Void)?
init(parameters: VideoPlayerCoordinatorParameters) {
self.parameters = parameters
viewModel = VideoPlayerViewModel(videoURL: parameters.videoURL,
autoplay: UIApplication.shared.applicationState == .active)
}
// MARK: - Public
func start() {
configureAudioSession(.sharedInstance())
viewModel.callback = { [weak self] action in
guard let self else { return }
MXLog.debug("VideoPlayerViewModel did complete with result: \(action).")
switch action {
case .cancel:
self.callback?(.cancel)
}
}
}
func stop() {
deconfigureAudioSession(.sharedInstance())
}
func toPresentable() -> AnyView {
AnyView(VideoPlayerScreen(context: viewModel.context))
}
// MARK: - Private
private func configureAudioSession(_ session: AVAudioSession) {
do {
try session.setCategory(.playback,
mode: .default)
try session.setActive(true)
} catch {
MXLog.debug("Configure audio session failed: \(error)")
}
}
private func deconfigureAudioSession(_ session: AVAudioSession) {
do {
try session.setActive(false, options: .notifyOthersOnDeactivation)
} catch {
MXLog.debug("Deconfigure audio session failed: \(error)")
}
}
}
@@ -1,30 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum VideoPlayerViewModelAction {
case cancel
}
struct VideoPlayerViewState: BindableState {
let videoURL: URL
let autoplay: Bool
}
enum VideoPlayerViewAction {
case cancel
}
@@ -1,35 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
typealias VideoPlayerViewModelType = StateStoreViewModel<VideoPlayerViewState, VideoPlayerViewAction>
class VideoPlayerViewModel: VideoPlayerViewModelType, VideoPlayerViewModelProtocol {
var callback: ((VideoPlayerViewModelAction) -> Void)?
init(videoURL: URL, autoplay: Bool = true) {
super.init(initialViewState: VideoPlayerViewState(videoURL: videoURL,
autoplay: autoplay))
}
override func process(viewAction: VideoPlayerViewAction) async {
switch viewAction {
case .cancel:
callback?(.cancel)
}
}
}
@@ -1,23 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@MainActor
protocol VideoPlayerViewModelProtocol {
var callback: ((VideoPlayerViewModelAction) -> Void)? { get set }
var context: VideoPlayerViewModelType.Context { get }
}
@@ -1,49 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVKit
import SwiftUI
struct VideoPlayerScreen: View {
@ObservedObject var context: VideoPlayerViewModel.Context
var body: some View {
VideoPlayer(player: player())
.ignoresSafeArea(.all, edges: [.horizontal, .bottom])
.navigationTitle(ElementL10n.a11yVideo)
}
private func player() -> AVPlayer {
let player = AVPlayer(url: context.viewState.videoURL)
if context.viewState.autoplay {
player.play()
}
return player
}
}
// MARK: - Previews
struct VideoPlayer_Previews: PreviewProvider {
static var previews: some View {
Group {
let viewModel = VideoPlayerViewModel(videoURL: URL(staticString: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
autoplay: false)
VideoPlayerScreen(context: viewModel.context)
}
.tint(.element.accent)
}
}