mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 09:41:02 +00:00
Media viewer: various fixes
This commit is contained in:
parent
e9b322e289
commit
1b228d504f
5 changed files with 198 additions and 79 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue