Media viewer: various fixes

This commit is contained in:
Thomas Ricouard 2023-10-18 12:19:39 +02:00
parent e9b322e289
commit 1b228d504f
5 changed files with 198 additions and 79 deletions

View file

@ -63,6 +63,11 @@ struct IceCubesApp: App {
.environment(userPreferences)
.environment(theme)
}
.fullScreenCover(item: $quickLook.url, content: { url in
QuickLookPreview(selectedURL: url, urls: quickLook.urls)
.edgesIgnoringSafeArea(.bottom)
.background(TransparentBackground())
})
.onChange(of: pushNotificationsService.handledNotification) { _, newValue in
if newValue != nil {
pushNotificationsService.handledNotification = nil

View file

@ -1,16 +1,95 @@
import Combine
import SwiftUI
@preconcurrency import SwiftUI
import Models
import QuickLook
@MainActor
@Observable public class QuickLook {
public var selectedMediaAttachment: MediaAttachment?
public var mediaAttachments: [MediaAttachment] = []
public var url: URL? {
didSet {
if url == nil {
cleanup(urls: urls)
}
}
}
public private(set) var urls: [URL] = []
public init() {}
public func prepareFor(selectedMediaAttachment: MediaAttachment?, mediaAttachments: [MediaAttachment]) {
self.selectedMediaAttachment = selectedMediaAttachment
self.mediaAttachments = mediaAttachments
public func prepareFor(selectedMediaAttachment: MediaAttachment, mediaAttachments: [MediaAttachment]) {
if ProcessInfo.processInfo.isiOSAppOnMac, let selectedURL = selectedMediaAttachment.url {
let urls = mediaAttachments.compactMap{ $0.url }
Task {
await prepareFor(urls: urls, selectedURL: selectedURL)
}
} else {
self.selectedMediaAttachment = selectedMediaAttachment
self.mediaAttachments = mediaAttachments
}
}
private func prepareFor(urls: [URL], selectedURL: URL) async {
var transaction = Transaction(animation: .default)
transaction.disablesAnimations = 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 })
}
} catch {
withTransaction(transaction) {
self.urls = []
url = nil
}
}
}
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)
}
}

View file

@ -17,7 +17,7 @@ import SwiftUI
func preparePlayer(autoPlay: Bool) {
player = .init(url: url)
player?.isMuted = true
player?.isMuted = !forceAutoPlay
player?.audiovisualBackgroundPlaybackPolicy = .pauses
if autoPlay || forceAutoPlay {
player?.play()

View file

@ -0,0 +1,21 @@
import SwiftUI
import UIKit
import CoreTransferable
struct MediaUIImageTransferable: Codable, Transferable {
let url: URL
func fetchAsImage() async -> Image {
let data = try? await URLSession.shared.data(from: url).0
guard let data, let uiimage = UIImage(data: data) else {
return Image(systemName: "photo")
}
return Image(uiImage: uiimage)
}
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { media in
await media.fetchAsImage()
}
}
}

View file

@ -5,7 +5,7 @@ import SwiftUI
import Models
import QuickLook
public struct MediaUIView: View {
public struct MediaUIView: View, @unchecked Sendable {
@Environment(\.dismiss) private var dismiss
public let selectedAttachment: MediaAttachment
@ -51,69 +51,7 @@ public struct MediaUIView: View {
.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 attachment = attachments.first(where: { $0.id == scrollToId}),
let url = attachment.url,
attachment.supportedType == .image {
Button {
Task {
isSavingPhoto = true
if await saveImage(url: url) {
withAnimation {
isSavingPhoto = false
didSavePhoto = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
didSavePhoto = false
}
}
} else {
isSavingPhoto = false
}
}
} label: {
if isSavingPhoto {
ProgressView()
} else if didSavePhoto {
Image(systemName: "checkmark.circle.fill")
} else {
Image(systemName: "arrow.down.circle")
}
}
}
}
ToolbarItem(placement: .topBarTrailing) {
if let url = attachments.first(where: { $0.id == scrollToId})?.url {
ShareLink(item: url)
}
}
toolbarView
}
.alert("status.editor.media.image-description",
isPresented: $isAltAlertDisplayed)
@ -131,6 +69,80 @@ public struct MediaUIView: View {
}
}
@ToolbarContentBuilder
private var toolbarView: some ToolbarContent {
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 = 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 attachment = attachments.first(where: { $0.id == scrollToId}),
let url = attachment.url,
attachment.supportedType == .image {
Button {
Task {
isSavingPhoto = true
if await saveImage(url: url) {
withAnimation {
isSavingPhoto = false
didSavePhoto = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
didSavePhoto = false
}
}
} else {
isSavingPhoto = false
}
}
} label: {
if isSavingPhoto {
ProgressView()
} else if didSavePhoto {
Image(systemName: "checkmark.circle.fill")
} else {
Image(systemName: "arrow.down.circle")
}
}
}
}
ToolbarItem(placement: .topBarTrailing) {
if let attachment = attachments.first(where: { $0.id == scrollToId}),
let url = attachment.url {
switch attachment.supportedType {
case .image:
let transferable = MediaUIImageTransferable(url: url)
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
image: transferable))
default:
ShareLink(item: url)
}
}
}
}
private var quickLookDir: URL {
try! FileManager.default.url(for: .cachesDirectory,
@ -140,23 +152,25 @@ public struct MediaUIView: View {
.appending(component: "quicklook")
}
private func localPathFor(url: URL) async throws -> URL {
private func imageData(_ url: URL) async -> Data? {
var data = ImagePipeline.shared.cache.cachedData(for: .init(url: url))
if data == nil {
data = try? await URLSession.shared.data(from: url).0
}
return data
}
private func localPathFor(url: URL) async -> 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)
let data = await imageData(url)
try? data?.write(to: path)
return path
}
private func uiimageFor(url: URL) async throws -> UIImage? {
var data = ImagePipeline.shared.cache.cachedData(for: .init(url: url))
if data == nil {
data = try await URLSession.shared.data(from: url).0
}
let data = await imageData(url)
if let data {
return UIImage(data: data)
}