New media viewer

This commit is contained in:
Thomas Ricouard 2023-10-16 19:08:59 +02:00
parent 017275ec69
commit fd55020533
16 changed files with 361 additions and 155 deletions

View file

@ -85,6 +85,7 @@
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FD34822293D06E800DB0EE9 /* Assets.xcassets */; };
9FD542E72962D2FF0045321A /* Lists in Frameworks */ = {isa = PBXBuildFile; productRef = 9FD542E62962D2FF0045321A /* Lists */; };
9FE0346C2ADD5C2100529EA8 /* MediaUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE0346B2ADD5C2100529EA8 /* MediaUI */; };
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; };
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
@ -222,6 +223,8 @@
9FCBB3D429859615009B77EE /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
9FD34822293D06E800DB0EE9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9FD542E52962D2CE0045321A /* Lists */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lists; path = Packages/Lists; sourceTree = "<group>"; };
9FE034692ADD597100529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
B0BAB49E29B3D7A9008F54D7 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@ -287,6 +290,7 @@
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */,
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */,
9FE0346C2ADD5C2100529EA8 /* MediaUI in Frameworks */,
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
@ -434,6 +438,7 @@
9F55C68E295598F900F94077 /* Explore */,
9F5E581729545B5500A53960 /* Env */,
9F398AA32935F90100A889F2 /* Models */,
9FE0346A2ADD59AC00529EA8 /* MediaUI */,
9F29553D292B67B600E0E81B /* Network */,
9FD542E52962D2CE0045321A /* Lists */,
9F35DB4829506F7F00B3281A /* Notifications */,
@ -470,6 +475,7 @@
9FBFE64C292A72BD00C250E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
9FE034692ADD597100529EA8 /* MediaUI */,
9F2A540D2969A0B0009B2D7C /* StoreKit.framework */,
9F2A5404296995FB009B2D7C /* QuickLookUI.framework */,
9F7335EE29674F7100AFF0BA /* QuickLook.framework */,
@ -612,6 +618,7 @@
9FE3DB56296FEFCA00628CB0 /* AppAccount */,
065FA1FD29866CD600012EA0 /* LRUCache */,
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */,
9FE0346B2ADD5C2100529EA8 /* MediaUI */,
);
productName = IceCubesApp;
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
@ -1481,6 +1488,10 @@
isa = XCSwiftPackageProductDependency;
productName = Lists;
};
9FE0346B2ADD5C2100529EA8 /* MediaUI */ = {
isa = XCSwiftPackageProductDependency;
productName = MediaUI;
};
9FE3DB56296FEFCA00628CB0 /* AppAccount */ = {
isa = XCSwiftPackageProductDependency;
productName = AppAccount;

View file

@ -9,6 +9,7 @@ import RevenueCat
import Status
import SwiftUI
import Timeline
import MediaUI
@main
struct IceCubesApp: App {
@ -54,11 +55,14 @@ struct IceCubesApp: App {
.environment(watcher)
.environment(pushNotificationsService)
.environment(\.isSupporter, isSupporter)
.fullScreenCover(item: $quickLook.url, content: { url in
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
.edgesIgnoringSafeArea(.bottom)
.background(TransparentBackground())
})
.sheet(item: $quickLook.selectedMediaAttachment) { selectedMediaAttachment in
MediaUIView(selectedAttachment: selectedMediaAttachment,
attachments: quickLook.mediaAttachments)
.presentationBackground(.ultraThinMaterial)
.presentationCornerRadius(16)
.environment(userPreferences)
.environment(theme)
}
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if newValue != nil {
pushNotificationsService.handledNotification = nil
@ -265,8 +269,6 @@ struct IceCubesApp: App {
}
class AppDelegate: NSObject, UIApplicationDelegate {
let themeObserver = ThemeObserverViewController(nibName: nil, bundle: nil)
func application(_: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool
{
@ -300,11 +302,3 @@ class AppDelegate: NSObject, UIApplicationDelegate {
return configuration
}
}
class ThemeObserverViewController: UIViewController {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
print(traitCollection.userInterfaceStyle.rawValue)
}
}

View file

@ -79,9 +79,8 @@ struct AccountDetailHeaderView: View {
guard account.haveHeader else {
return
}
Task {
await quickLook.prepareFor(urls: [account.header], selectedURL: account.header)
}
let attachement = MediaAttachment.imageWith(url: account.header)
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits([.isImage, .isButton])
@ -110,9 +109,8 @@ struct AccountDetailHeaderView: View {
guard account.haveAvatar else {
return
}
Task {
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
}
let attachement = MediaAttachment.imageWith(url: account.avatar)
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits([.isImage, .isButton])

View file

@ -48,19 +48,12 @@ public struct AppAccountsSelectorView: View {
labelView
}
.sheet(isPresented: $isPresented, content: {
if #available(iOS 16.4, *) {
accountsView.presentationDetents([.height(preferredHeight), .large])
.presentationBackground(.thinMaterial)
.presentationCornerRadius(16)
.onAppear {
refreshAccounts()
}
} else {
accountsView.presentationDetents([.height(preferredHeight), .large])
.onAppear {
refreshAccounts()
}
}
accountsView.presentationDetents([.height(preferredHeight), .large])
.presentationBackground(.thinMaterial)
.presentationCornerRadius(16)
.onAppear {
refreshAccounts()
}
})
.onChange(of: currentAccount.account?.id) {
refreshAccounts()

View file

@ -200,11 +200,7 @@ struct ConversationMessageView: View {
.frame(height: 200)
.contentShape(Rectangle())
.onTapGesture {
if let url = attachement.url {
Task {
await quickLook.prepareFor(urls: [url], selectedURL: url)
}
}
quickLook.prepareFor(selectedMediaAttachment: attachement, mediaAttachments: [attachement])
}
}

View file

@ -1,87 +1,16 @@
import Combine
@preconcurrency import QuickLook
import SwiftUI
import Models
@MainActor
@Observable public class QuickLook {
public var url: URL? {
didSet {
if url == nil {
cleanup(urls: urls)
}
}
}
public private(set) var urls: [URL] = []
public private(set) var isPreparing: Bool = false
public private(set) var latestError: Error?
public var selectedMediaAttachment: MediaAttachment?
public var mediaAttachments: [MediaAttachment] = []
public init() {}
public func prepareFor(urls: [URL], selectedURL: URL) async {
var transaction = Transaction(animation: .default)
transaction.disablesAnimations = true
withTransaction(transaction) {
isPreparing = true
}
do {
var order = 0
let pathOrderMap = urls.reduce(into: [String: Int]()) { result, url in
result[url.lastPathComponent] = order
order += 1
}
let paths: [URL] = try await withThrowingTaskGroup(of: URL.self, body: { group in
var paths: [URL] = []
for url in urls {
group.addTask {
try await self.localPathFor(url: url)
}
}
for try await path in group {
paths.append(path)
}
return paths.sorted { url1, url2 in
pathOrderMap[url1.lastPathComponent] ?? 0 < pathOrderMap[url2.lastPathComponent] ?? 0
}
})
withTransaction(transaction) {
self.urls = paths
url = paths.first(where: { $0.lastPathComponent == selectedURL.lastPathComponent })
isPreparing = false
}
} catch {
withTransaction(transaction) {
isPreparing = false
self.urls = []
url = nil
latestError = error
}
}
}
private var quickLookDir: URL {
try! FileManager.default.url(for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appending(component: "quicklook")
}
private func localPathFor(url: URL) async throws -> URL {
try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true)
let path = quickLookDir.appendingPathComponent(url.lastPathComponent)
// Warning: Non-sendable type '(any URLSessionTaskDelegate)?' exiting main actor-isolated
// context in call to non-isolated instance method 'data(for:delegate:)' cannot cross actor
// boundary.
// This is on the defaulted-to-nil second parameter of `.data(from:delegate:)`.
// There is a Radar tracking this & others like it.
let data = try await URLSession.shared.data(from: url).0
try data.write(to: path)
return path
}
private func cleanup(urls _: [URL]) {
try? FileManager.default.removeItem(at: quickLookDir)
public func prepareFor(selectedMediaAttachment: MediaAttachment?, mediaAttachments: [MediaAttachment]) {
self.selectedMediaAttachment = selectedMediaAttachment
self.mediaAttachments = mediaAttachments
}
}

8
Packages/MediaUI/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View file

@ -0,0 +1,32 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "MediaUI",
defaultLocalization: "en",
platforms: [
.iOS(.v17),
],
products: [
.library(
name: "MediaUI",
targets: ["MediaUI"]
),
],
dependencies: [
.package(name: "Models", path: "../Models"),
],
targets: [
.target(
name: "MediaUI",
dependencies: [
.product(name: "Models", package: "Models"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]
),
]
)

View file

@ -0,0 +1,27 @@
import SwiftUI
import Models
import NukeUI
struct MediaUIAttachmentImageView: View {
let url: URL
@GestureState private var zoom = 1.0
var body: some View {
MediaUIZoomableContainer {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizable()
.clipShape(RoundedRectangle(cornerRadius: 8))
.scaledToFit()
.padding(.horizontal, 8)
.scaleEffect(zoom)
} else if state.isLoading {
ProgressView()
.progressViewStyle(.circular)
}
}
}
}
}

View file

@ -5,11 +5,11 @@ import Observation
import SwiftUI
@MainActor
@Observable class VideoPlayerViewModel {
@Observable public class MediaUIAttachmentVideoViewModel {
var player: AVPlayer?
private let url: URL
init(url: URL) {
public init(url: URL) {
self.url = url
}
@ -48,15 +48,19 @@ import SwiftUI
}
}
struct VideoPlayerView: View {
public struct MediaUIAttachmentVideoView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.isCompact) private var isCompact
@Environment(UserPreferences.self) private var preferences
@Environment(Theme.self) private var theme
@State var viewModel: VideoPlayerViewModel
@State var viewModel: MediaUIAttachmentVideoViewModel
public init(viewModel: MediaUIAttachmentVideoViewModel) {
_viewModel = .init(wrappedValue: viewModel)
}
var body: some View {
public var body: some View {
ZStack {
VideoPlayer(player: viewModel.player)
.accessibilityAddTraits(.startsMediaSession)

View file

@ -0,0 +1,121 @@
import Foundation
import NukeUI
import Nuke
import SwiftUI
import Models
import QuickLook
public struct MediaUIView: View {
@Environment(\.dismiss) private var dismiss
public let selectedAttachment: MediaAttachment
public let attachments: [MediaAttachment]
@State private var scrollToId: String?
@State private var altTextDisplayed: String?
@State private var isAltAlertDisplayed: Bool = false
@State private var quickLookURL: URL?
public init(selectedAttachment: MediaAttachment, attachments: [MediaAttachment]) {
self.selectedAttachment = selectedAttachment
self.attachments = attachments
}
public var body: some View {
NavigationStack {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(attachments) { attachment in
if let url = attachment.url {
switch attachment.supportedType {
case .image:
MediaUIAttachmentImageView(url: url)
.containerRelativeFrame(.horizontal, count: 1, span: 1, spacing: 0)
.id(attachment.id)
case .video, .gifv, .audio:
MediaUIAttachmentVideoView(viewModel: .init(url: url))
.containerRelativeFrame(.horizontal, count: 1, span: 1, spacing: 0)
.containerRelativeFrame(.vertical, count: 1, span: 1, spacing: 0)
.id(attachment.id)
case .none:
EmptyView()
}
}
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.scrollPosition(id: $scrollToId)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle")
}
}
ToolbarItem(placement: .topBarTrailing) {
if let url = attachments.first(where: { $0.id == scrollToId})?.url {
Button {
Task {
quickLookURL = try? await localPathFor(url: url)
}
} label: {
Image(systemName: "info.circle")
}
}
}
ToolbarItem(placement: .topBarTrailing) {
if let alt = attachments.first(where: { $0.id == scrollToId})?.description {
Button {
altTextDisplayed = alt
isAltAlertDisplayed = true
} label: {
Text("status.image.alt-text.abbreviation")
}
}
}
ToolbarItem(placement: .topBarTrailing) {
if let url = attachments.first(where: { $0.id == scrollToId})?.url {
ShareLink(item: url)
}
}
}
.alert("status.editor.media.image-description",
isPresented: $isAltAlertDisplayed)
{
Button("alert.button.ok", action: {})
} message: {
Text(altTextDisplayed ?? "")
}
.quickLookPreview($quickLookURL)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
scrollToId = selectedAttachment.id
}
}
}
}
private var quickLookDir: URL {
try! FileManager.default.url(for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appending(component: "quicklook")
}
private func localPathFor(url: URL) async throws -> URL {
try? FileManager.default.removeItem(at: quickLookDir)
try? FileManager.default.createDirectory(at: quickLookDir, withIntermediateDirectories: true)
let path = quickLookDir.appendingPathComponent(url.lastPathComponent)
var data = ImagePipeline.shared.cache.cachedData(for: .init(url: url))
if data == nil {
data = try await URLSession.shared.data(from: url).0
}
try data?.write(to: path)
return path
}
}

View file

@ -0,0 +1,107 @@
import SwiftUI
import UIKit
// ref: https://stackoverflow.com/questions/74238414/is-there-an-easy-way-to-pinch-to-zoom-and-drag-any-view-in-swiftui
fileprivate let maxAllowedScale = 4.0
@MainActor
struct MediaUIZoomableContainer<Content: View>: View {
let content: Content
@State private var currentScale: CGFloat = 1.0
@State private var tapLocation: CGPoint = .zero
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
func doubleTapAction(location: CGPoint) {
tapLocation = location
currentScale = currentScale == 1.0 ? maxAllowedScale : 1.0
}
var body: some View {
ZoomableScrollView(scale: $currentScale, tapLocation: $tapLocation) {
content
}
.onTapGesture(count: 2, perform: doubleTapAction)
}
fileprivate struct ZoomableScrollView<ScollContent: View>: UIViewRepresentable {
private var content: ScollContent
@Binding private var currentScale: CGFloat
@Binding private var tapLocation: CGPoint
init(scale: Binding<CGFloat>, tapLocation: Binding<CGPoint>, @ViewBuilder content: () -> ScollContent) {
_currentScale = scale
_tapLocation = tapLocation
self.content = content()
}
func makeUIView(context: Context) -> UIScrollView {
let scrollView = UIScrollView()
scrollView.backgroundColor = .clear
scrollView.delegate = context.coordinator
scrollView.maximumZoomScale = maxAllowedScale
scrollView.minimumZoomScale = 1
scrollView.bouncesZoom = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = false
scrollView.backgroundColor = .clear
let hostedView = context.coordinator.hostingController.view!
hostedView.translatesAutoresizingMaskIntoConstraints = true
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
hostedView.frame = scrollView.bounds
hostedView.backgroundColor = .clear
scrollView.addSubview(hostedView)
return scrollView
}
func makeCoordinator() -> Coordinator {
return Coordinator(hostingController: UIHostingController(rootView: content), scale: $currentScale)
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
context.coordinator.hostingController.rootView = content
if uiView.zoomScale > uiView.minimumZoomScale { // Scale out
uiView.setZoomScale(currentScale, animated: true)
} else if tapLocation != .zero { // Scale in to a specific point
uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true)
DispatchQueue.main.async { tapLocation = .zero }
}
}
@MainActor func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
let scrollViewSize = scrollView.bounds.size
let width = scrollViewSize.width / scale
let height = scrollViewSize.height / scale
let x = center.x - (width / 2.0)
let y = center.y - (height / 2.0)
return CGRect(x: x, y: y, width: width, height: height)
}
class Coordinator: NSObject, UIScrollViewDelegate {
var hostingController: UIHostingController<ScollContent>
@Binding var currentScale: CGFloat
init(hostingController: UIHostingController<ScollContent>, scale: Binding<CGFloat>) {
self.hostingController = hostingController
_currentScale = scale
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return hostingController.view
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
currentScale = scale
}
}
}
}

View file

@ -44,6 +44,15 @@ public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
public let previewUrl: URL?
public let description: String?
public let meta: MetaContainer?
public static func imageWith(url: URL) -> MediaAttachment{
return .init(id: UUID().uuidString,
type: "image",
url: url,
previewUrl: url,
description: nil,
meta: nil)
}
}
extension MediaAttachment: Sendable {}

View file

@ -18,6 +18,7 @@ let package = Package(
dependencies: [
.package(name: "AppAccount", path: "../AppAccount"),
.package(name: "Models", path: "../Models"),
.package(name: "MediaUI", path: "../MediaUI"),
.package(name: "Network", path: "../Network"),
.package(name: "Env", path: "../Env"),
.package(name: "DesignSystem", path: "../DesignSystem"),
@ -28,6 +29,7 @@ let package = Package(
dependencies: [
.product(name: "AppAccount", package: "AppAccount"),
.product(name: "Models", package: "Models"),
.product(name: "MediaUI", package: "MediaUI"),
.product(name: "Network", package: "Network"),
.product(name: "Env", package: "Env"),
.product(name: "DesignSystem", package: "DesignSystem"),

View file

@ -207,10 +207,8 @@ public struct StatusRowView: View {
if viewModel.finalStatus.mediaAttachments.isEmpty == false {
Button("accessibility.status.media-viewer-action.label") {
HapticManager.shared.fireHaptic(of: .notification(.success))
Task {
let attachments = viewModel.finalStatus.mediaAttachments
await quickLook.prepareFor(urls: attachments.compactMap(\.url), selectedURL: attachments[0].url!)
}
let attachments = viewModel.finalStatus.mediaAttachments
quickLook.prepareFor(selectedMediaAttachment: attachments[0], mediaAttachments: attachments)
}
}

View file

@ -4,6 +4,7 @@ import Models
import Nuke
import NukeUI
import SwiftUI
import MediaUI
@MainActor
public struct StatusRowMediaPreviewView: View {
@ -87,9 +88,7 @@ public struct StatusRowMediaPreviewView: View {
if attachments.count == 1, let attachment = attachments.first {
makeFeaturedImagePreview(attachment: attachment)
.onTapGesture {
Task {
await quickLook.prepareFor(urls: attachments.compactMap(\.url), selectedURL: attachment.url!)
}
quickLook.prepareFor(selectedMediaAttachment: attachment, mediaAttachments: attachments)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(Self.accessibilityLabel(for: attachment))
@ -117,11 +116,6 @@ public struct StatusRowMediaPreviewView: View {
}
}
.overlay {
if quickLook.isPreparing {
quickLookLoadingView
.transition(.opacity)
}
if isHidingMedia {
sensitiveMediaOverlay
.transition(.opacity)
@ -182,7 +176,7 @@ public struct StatusRowMediaPreviewView: View {
case .gifv, .video, .audio:
if let url = attachment.url {
VideoPlayerView(viewModel: .init(url: url))
MediaUIAttachmentVideoView(viewModel: .init(url: url))
.frame(width: newSize.width, height: newSize.height)
}
case .none:
@ -265,7 +259,7 @@ public struct StatusRowMediaPreviewView: View {
}
case .gifv, .video, .audio:
if let url = attachment.url {
VideoPlayerView(viewModel: .init(url: url))
MediaUIAttachmentVideoView(viewModel: .init(url: url))
.frame(width: isCompact ? imageMaxHeight : proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
.accessibilityAddTraits(.startsMediaSession)
@ -278,9 +272,7 @@ public struct StatusRowMediaPreviewView: View {
// #965: do not create overlapping tappable areas, when multiple images are shown
.contentShape(Rectangle())
.onTapGesture {
Task {
await quickLook.prepareFor(urls: attachments.compactMap(\.url), selectedURL: attachment.url!)
}
quickLook.prepareFor(selectedMediaAttachment: attachment, mediaAttachments: attachments)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(Self.accessibilityLabel(for: attachment))
@ -288,21 +280,6 @@ public struct StatusRowMediaPreviewView: View {
}
}
private var quickLookLoadingView: some View {
ZStack(alignment: .center) {
VStack {
Spacer()
HStack {
Spacer()
ProgressView()
Spacer()
}
Spacer()
}
}
.background(.ultraThinMaterial)
}
private var sensitiveMediaOverlay: some View {
ZStack {
Rectangle()