another.im-ios/Monal/Classes/MediaGallery.swift

217 lines
7.4 KiB
Swift
Raw Normal View History

2024-11-18 14:53:52 +00:00
//
// MediaGallery.swift
// Monal
//
// Created by Vaidik on 03.08.24.
// Copyright © 2021 Monal.im. All rights reserved.
import SwiftUI
import AVKit
import AVFoundation
struct MediaGalleryView: View {
@State private var mediaItems: [[String: Any]] = []
let contact: String
let accountID: NSNumber
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 10) {
ForEach(mediaItems.indices, id: \.self) { index in
NavigationLink(destination: LazyClosureView {
MediaItemSwipeView(currentItem: mediaItems[index], allItems: mediaItems)
}) {
MediaItemView(fileInfo: mediaItems[index])
}
}
}
.padding()
}
.navigationTitle("Shared Media")
.onAppear {
fetchDownloadedMediaItems()
}
}
private func fetchDownloadedMediaItems() {
if let attachments = DataLayer.sharedInstance().allAttachments(fromContact: contact, forAccount: accountID) as? [[String: Any]] {
mediaItems = attachments.filter { fileInfo in
if let mimeType = fileInfo["mimeType"] as? String,
!((fileInfo["needsDownloading"] as? NSNumber)?.boolValue ?? true) &&
(mimeType.starts(with: "image/") || mimeType.starts(with: "video/")) {
return true
}
return false
}
}
}
}
class MediaItem: Identifiable, ObservableObject {
let id = UUID()
let fileInfo: [String: Any]
@Published var thumbnail: UIImage?
init(fileInfo: [String: Any]) {
self.fileInfo = fileInfo
self.thumbnail = nil
Task {
await generateThumbnail()
}
}
@MainActor
func generateThumbnail() async {
guard let cacheFile = fileInfo["cacheFile"] as? String, let mimeType = fileInfo["mimeType"] as? String else {
DDLogError("Failed to get cacheFile or mimeType for: \(fileInfo)")
self.thumbnail = UIImage(systemName: "exclamationmark.triangle")
return
}
if mimeType.starts(with: "image/") {
if let image = UIImage(contentsOfFile: cacheFile) {
self.thumbnail = image
} else {
DDLogError("Failed to generate image thumbnail for: \(fileInfo)")
self.thumbnail = UIImage(systemName: "photo")
}
return
} else if mimeType.starts(with: "video/") {
if let thumbnail = await videoPreview(for:fileInfo) {
self.thumbnail = thumbnail
} else {
DDLogError("Failed to generate video thumbnail for: \(fileInfo)")
self.thumbnail = UIImage(systemName: "video")
}
return
}
DDLogError("Unsupported mime type: \(mimeType)")
self.thumbnail = UIImage(systemName: "doc")
}
@MainActor
func videoPreview(for fileInfo: [String: Any]) async -> UIImage? {
let moviePath = URL(fileURLWithPath: fileInfo["cacheFile"] as! String)
DDLogInfo("Trying to generate video thumbnail for: \(String(describing:fileInfo))")
var payload: NSMutableDictionary = [:]
HelperTools.addUploadItemPreview(forItem:moviePath, provider:nil, andPayload:payload) { newPayload in
payload = newPayload ?? [:]
}
guard let image = payload["preview"] as? UIImage else {
return try? await HelperTools.generateVideoThumbnail(
fromFile:fileInfo["cacheFile"] as! String,
havingMimeType:fileInfo["mimeType"] as! String,
andFileExtension:fileInfo["fileExtension"] as? String
).toPromise().asyncOnMainActor()
}
return image
}
}
struct MediaItemView: View {
@StateObject private var item: MediaItem
init(fileInfo: [String: Any]) {
_item = StateObject(wrappedValue: MediaItem(fileInfo: fileInfo))
}
var body: some View {
ZStack {
Group {
if let thumbnail = item.thumbnail {
Image(uiImage: thumbnail)
.resizable()
//.scaledToFit() //leaves empty room around image if not having a square format
.scaledToFill() //this is what the ios gallery app uses (will crop the edges of that preview)
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
}
.frame(width: 100, height: 100, alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 10))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
// Add play icon overlay for video files
if let mimeType = item.fileInfo["mimeType"] as? String, mimeType.starts(with: "video/") {
Image(systemName: "play.circle.fill")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(.white)
.background(Color.black.opacity(0.5))
.clipShape(Circle())
}
}
}
}
struct MediaItemDetailView: View {
@StateObject private var item: MediaItem
@StateObject private var dismisser = SheetDismisserProtocol()
init(fileInfo: [String: Any]) {
_item = StateObject(wrappedValue: MediaItem(fileInfo: fileInfo))
}
var body: some View {
ImageViewerWrapper(info: item.fileInfo as [String: AnyObject], dismisser: dismisser)
.onAppear {
if let hostingController = UIApplication.shared.windows.first?.rootViewController?.presentedViewController as? UIHostingController<AnyView> {
dismisser.host = hostingController
}
}
}
}
struct MediaItemSwipeView: View {
@State private var currentIndex: Int
let allItems: [[String: Any]]
init(currentItem: [String: Any], allItems: [[String: Any]]) {
let index = allItems.firstIndex { item in
// Compare using 'cacheFile'
if let currentPath = currentItem["cacheFile"] as? String,
let itemPath = item["cacheFile"] as? String {
return currentPath == itemPath
}
return false
} ?? 0
self._currentIndex = State(initialValue: index)
self.allItems = allItems
}
var body: some View {
TabView(selection: $currentIndex) {
ForEach(allItems.indices, id: \.self) { index in
MediaItemDetailView(fileInfo: allItems[index])
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.animation(.easeInOut, value: currentIndex)
.ignoresSafeArea()
.navigationBarHidden(true)
.statusBar(hidden: true)
}
}
struct ImageViewerWrapper: View {
let info: [String: AnyObject]
let dismisser: SheetDismisserProtocol
var body: some View {
Group {
if let _ = info["mimeType"] as? String {
try? ImageViewer(delegate: dismisser, info: info)
} else {
Text("Invalid file data")
}
}
}
}