Add a fullscreen button to TimelineMediaPreviewScreen and hook up swiping through the timeline. (#3638)

* Add a fullscreen button to media previews - Not ideal but the gestures conflict with the preview controller.

* Don't un-flip the preview thumbnail until the preview has disappeared, and only do it on iOS 18.

* Add all of the loaded items for previewing in the preview controller.
This commit is contained in:
Doug
2024-12-18 19:10:19 +00:00
committed by GitHub
parent 435dfb8e46
commit e7cc807084
10 changed files with 105 additions and 33 deletions
@@ -180,8 +180,11 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
contentType: contentType))
let timelineKind = TimelineKind.media(isPresentedOnRoomScreen ? .roomScreen : .mediaFilesScreen)
let timelineController = MockRoomTimelineController(timelineKind: timelineKind)
timelineController.timelineItems = [item]
return TimelineMediaPreviewViewModel(context: .init(item: item,
viewModel: TimelineViewModel.mock(timelineKind: timelineKind),
viewModel: TimelineViewModel.mock(timelineKind: timelineKind,
timelineController: timelineController),
namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()),
photoLibraryManager: PhotoLibraryManagerMock(.init()),
@@ -138,8 +138,11 @@ struct TimelineMediaPreviewRedactConfirmationView_Previews: PreviewProvider, Tes
thumbnailInfo: .mockThumbnail,
contentType: contentType))
let timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = [item]
return TimelineMediaPreviewViewModel(context: .init(item: item,
viewModel: TimelineViewModel.mock,
viewModel: TimelineViewModel.mock(timelineKind: timelineController.timelineKind,
timelineController: timelineController),
namespace: previewNamespace),
mediaProvider: MediaProviderMock(configuration: .init()),
photoLibraryManager: PhotoLibraryManagerMock(.init()),
@@ -12,18 +12,16 @@ import SwiftUI
struct TimelineMediaPreviewScreen: View {
@ObservedObject var context: TimelineMediaPreviewViewModel.Context
var itemIDHandler: ((TimelineItemIdentifier?) -> Void)?
@State private var isFullScreen = false
private var toolbarVisibility: Visibility { isFullScreen ? .hidden : .visible }
private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem }
var body: some View {
NavigationStack {
Color.clear
.overlay { QuickLookView(viewModelContext: context).ignoresSafeArea() } // Overlay to stop QL hijacking the toolbar.
.toolbar { toolbar }
.toolbarBackground(.visible, for: .navigationBar) // The toolbar's scrollEdgeAppearance isn't aware of the quicklook view 🤷‍♂️
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .bottom, spacing: 0) { caption }
quickLookPreview
}
.introspect(.navigationStack, on: .supportedVersions) {
// Fixes a bug where the QuickLook view overrides the .toolbarBackground(.visible) after it loads the real item.
@@ -39,12 +37,41 @@ struct TimelineMediaPreviewScreen: View {
}
.alert(item: $context.alertInfo)
.preferredColorScheme(.dark)
.onDisappear {
itemIDHandler?(nil)
}
.zoomTransition(sourceID: currentItem.id, in: context.viewState.transitionNamespace)
}
var quickLookPreview: some View {
Color.clear // A completely clear view breaks any SwiftUI gestures (such as drag to dismiss).
.background { QuickLookView(viewModelContext: context).ignoresSafeArea() } // Not the root view to stop QL hijacking the toolbar.
.overlay(alignment: .topTrailing) { fullScreenButton }
.toolbar { toolbar }
.toolbar(toolbarVisibility, for: .navigationBar)
.toolbar(toolbarVisibility, for: .bottomBar)
.toolbarBackground(.visible, for: .navigationBar) // The toolbar's scrollEdgeAppearance isn't aware of the quicklook view 🤷‍♂️
.toolbarBackground(.visible, for: .bottomBar)
.navigationBarTitleDisplayMode(.inline)
.safeAreaInset(edge: .bottom, spacing: 0) { caption }
}
private var fullScreenButton: some View {
Button {
withAnimation { isFullScreen.toggle() }
} label: {
CompoundIcon(isFullScreen ? \.collapse : \.expand, size: .xSmall, relativeTo: .compound.bodyLG)
.padding(6)
.background(.thinMaterial, in: Circle())
}
.tint(.compound.textActionPrimary)
.padding(.top, 12)
.padding(.trailing, 14)
}
@ViewBuilder
private var caption: some View {
if let caption = currentItem.caption {
if let caption = currentItem.caption, !isFullScreen {
Text(caption)
.font(.compound.bodyLG)
.foregroundStyle(.compound.textPrimary)
@@ -55,6 +82,7 @@ struct TimelineMediaPreviewScreen: View {
.background {
BlurEffectView(style: .systemChromeMaterial) // Darkest material available, matches the bottom bar when content is beneath.
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
@@ -114,12 +142,16 @@ struct TimelineMediaPreviewScreen: View {
}
}
// MARK: - QuickLook
private struct QuickLookView: UIViewControllerRepresentable {
let viewModelContext: TimelineMediaPreviewViewModel.Context
func makeUIViewController(context: Context) -> PreviewController {
PreviewController(coordinator: context.coordinator,
fileLoadedPublisher: viewModelContext.viewState.fileLoadedPublisher.eraseToAnyPublisher())
let fileLoadedPublisher = viewModelContext.viewState.fileLoadedPublisher.eraseToAnyPublisher()
let controller = PreviewController(coordinator: context.coordinator, fileLoadedPublisher: fileLoadedPublisher)
controller.currentPreviewItemIndex = viewModelContext.viewState.initialItemIndex
return controller
}
func updateUIViewController(_ uiViewController: PreviewController, context: Context) { }
@@ -128,6 +160,8 @@ private struct QuickLookView: UIViewControllerRepresentable {
Coordinator(viewModelContext: viewModelContext)
}
// MARK: Coordinator
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
private let viewModelContext: TimelineMediaPreviewViewModel.Context
@@ -148,14 +182,12 @@ private struct QuickLookView: UIViewControllerRepresentable {
}
}
// MARK: UIKit
class PreviewController: QLPreviewController {
let coordinator: Coordinator
private var cancellables: Set<AnyCancellable> = []
init(coordinator: Coordinator, fileLoadedPublisher: AnyPublisher<TimelineItemIdentifier, Never>) {
self.coordinator = coordinator
super.init(nibName: nil, bundle: nil)
dataSource = coordinator
@@ -208,8 +240,12 @@ struct TimelineMediaPreviewScreen_Previews: PreviewProvider {
thumbnailSource: nil,
contentType: .pdf))
let timelineController = MockRoomTimelineController(timelineKind: .media(.mediaFilesScreen))
timelineController.timelineItems = [item]
return TimelineMediaPreviewViewModel(context: .init(item: item,
viewModel: TimelineViewModel.mock(timelineKind: .media(.mediaFilesScreen)),
viewModel: TimelineViewModel.mock(timelineKind: timelineController.timelineKind,
timelineController: timelineController),
namespace: namespace),
mediaProvider: MediaProviderMock(configuration: .init()),
photoLibraryManager: PhotoLibraryManagerMock(.init()),