wip
This commit is contained in:
parent
1676ab63e9
commit
b8dca34f84
53
ConversationsClassic/AppData/Model/GalleryItem.swift
Normal file
53
ConversationsClassic/AppData/Model/GalleryItem.swift
Normal file
|
@ -0,0 +1,53 @@
|
|||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
enum GalleryMediaType {
|
||||
case video
|
||||
case photo
|
||||
}
|
||||
|
||||
struct GalleryItem: Identifiable {
|
||||
let id: String
|
||||
let type: GalleryMediaType
|
||||
var thumbnail: Image?
|
||||
var duration: String?
|
||||
}
|
||||
|
||||
extension GalleryItem {
|
||||
static func fetchAll() async -> [GalleryItem] {
|
||||
await Task {
|
||||
let fetchOptions = PHFetchOptions()
|
||||
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||||
let assets = PHAsset.fetchAssets(with: fetchOptions)
|
||||
var tmpGalleryItems: [GalleryItem] = []
|
||||
assets.enumerateObjects { asset, _, _ in
|
||||
if asset.mediaType == .image {
|
||||
let item = GalleryItem(id: asset.localIdentifier, type: .photo, thumbnail: nil, duration: nil)
|
||||
tmpGalleryItems.append(item)
|
||||
}
|
||||
if asset.mediaType == .video {
|
||||
let item = GalleryItem(id: asset.localIdentifier, type: .video, thumbnail: nil, duration: asset.duration.minAndSec)
|
||||
tmpGalleryItems.append(item)
|
||||
}
|
||||
}
|
||||
return tmpGalleryItems
|
||||
}.value
|
||||
}
|
||||
|
||||
mutating func fetchThumbnail() async throws {
|
||||
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else { return }
|
||||
let size = CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||
|
||||
switch type {
|
||||
case .photo:
|
||||
let originalImage = try await PHImageManager.default().getPhoto(for: asset)
|
||||
let cropped = try await originalImage.scaleAndCropImage(size)
|
||||
thumbnail = Image(uiImage: cropped)
|
||||
|
||||
case .video:
|
||||
let avAsset = try await PHImageManager.default().getVideo(for: asset)
|
||||
let cropped = try await avAsset.generateVideoThumbnail(size)
|
||||
thumbnail = Image(uiImage: cropped)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
enum ClientStoreError: Error {
|
||||
case clientNotFound
|
||||
case rosterNotFound
|
||||
case imageNotFound
|
||||
case videoNotFound
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Photos
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class FileStore: ObservableObject {
|
||||
@Published var cameraAccessGranted = false
|
||||
@Published var galleryAccessGranted = false
|
||||
@Published var galleryItems: [GalleryItem] = []
|
||||
|
||||
private let client: Client
|
||||
private let roster: Roster
|
||||
|
@ -35,4 +37,9 @@ extension FileStore {
|
|||
}
|
||||
galleryAccessGranted = isAuthorized
|
||||
}
|
||||
|
||||
func fetchGalleryItems() async {
|
||||
guard galleryAccessGranted else { return }
|
||||
galleryItems = await GalleryItem.fetchAll()
|
||||
}
|
||||
}
|
||||
|
|
16
ConversationsClassic/Helpers/AVAsset+Thumbnail.swift
Normal file
16
ConversationsClassic/Helpers/AVAsset+Thumbnail.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
extension AVAsset {
|
||||
func generateVideoThumbnail(_ size: CGSize) async throws -> UIImage {
|
||||
try await Task {
|
||||
let assetImgGenerate = AVAssetImageGenerator(asset: self)
|
||||
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||
let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 600)
|
||||
let cgImage = try assetImgGenerate.copyCGImage(at: time, actualTime: nil)
|
||||
let image = UIImage(cgImage: cgImage)
|
||||
let result = try await image.scaleAndCropImage(size)
|
||||
return result
|
||||
}.value
|
||||
}
|
||||
}
|
43
ConversationsClassic/Helpers/PHImageManager+Fetch.swift
Normal file
43
ConversationsClassic/Helpers/PHImageManager+Fetch.swift
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Photos
|
||||
import UIKit
|
||||
|
||||
extension PHImageManager {
|
||||
func getPhoto(for asset: PHAsset) async throws -> UIImage {
|
||||
let options = PHImageRequestOptions()
|
||||
options.version = .original
|
||||
options.isSynchronous = true
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
requestImage(
|
||||
for: asset,
|
||||
targetSize: PHImageManagerMaximumSize,
|
||||
contentMode: .aspectFill,
|
||||
options: options
|
||||
) { image, _ in
|
||||
if let image {
|
||||
continuation.resume(returning: image)
|
||||
} else {
|
||||
continuation.resume(throwing: ClientStoreError.imageNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getVideo(for asset: PHAsset) async throws -> AVAsset {
|
||||
let options = PHVideoRequestOptions()
|
||||
options.version = .original
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
requestAVAsset(
|
||||
forVideo: asset,
|
||||
options: options
|
||||
) { avAsset, _, _ in
|
||||
if let avAsset {
|
||||
continuation.resume(returning: avAsset)
|
||||
} else {
|
||||
continuation.resume(throwing: ClientStoreError.videoNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
ConversationsClassic/Helpers/UIImage+Crop.swift
Normal file
30
ConversationsClassic/Helpers/UIImage+Crop.swift
Normal file
|
@ -0,0 +1,30 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
|
||||
extension UIImage {
|
||||
func scaleAndCropImage(_ size: CGSize) async throws -> UIImage {
|
||||
try await Task {
|
||||
let aspect = self.size.width / self.size.height
|
||||
let targetAspect = size.width / size.height
|
||||
var newWidth: CGFloat
|
||||
var newHeight: CGFloat
|
||||
if aspect < targetAspect {
|
||||
newWidth = size.width
|
||||
newHeight = size.width / aspect
|
||||
} else {
|
||||
newHeight = size.height
|
||||
newWidth = size.height * aspect
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
self.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight))
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
if let newImage = newImage {
|
||||
return newImage
|
||||
} else {
|
||||
throw NSError(domain: "UIImage", code: -900, userInfo: nil)
|
||||
}
|
||||
}.value
|
||||
}
|
||||
}
|
|
@ -25,7 +25,9 @@ struct CameraView: UIViewRepresentable {
|
|||
view.layer.addSublayer(previewLayer)
|
||||
view.previewLayer = previewLayer
|
||||
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
captureSession.startRunning()
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -1,67 +1,75 @@
|
|||
import SwiftUI
|
||||
|
||||
struct GalleryView: View {
|
||||
// @State private var selectedItems: [String] = []
|
||||
@EnvironmentObject var store: FileStore
|
||||
@Binding var selectedItems: [String]
|
||||
|
||||
var body: some View {
|
||||
Text("test")
|
||||
// Group {
|
||||
// if store.state.sharingState.isGalleryAccessGranted {
|
||||
// ForEach(store.state.sharingState.galleryItems) { item in
|
||||
// GridViewItem(item: item, selected: $selectedItems)
|
||||
// }
|
||||
// } else {
|
||||
// Button {
|
||||
// openAppSettings()
|
||||
// } label: {
|
||||
// ZStack {
|
||||
// Rectangle()
|
||||
// .fill(Color.Material.Background.light)
|
||||
// .overlay {
|
||||
// VStack {
|
||||
// Image(systemName: "photo")
|
||||
// .foregroundColor(.Material.Elements.active)
|
||||
// .font(.system(size: 30))
|
||||
// Text("Allow gallery access")
|
||||
// .foregroundColor(.Material.Text.main)
|
||||
// .font(.body3)
|
||||
// }
|
||||
// }
|
||||
// .frame(height: 100)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Group {
|
||||
if store.galleryAccessGranted {
|
||||
ForEach(store.galleryItems) { item in
|
||||
GridViewItem(item: item, selected: $selectedItems)
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
openAppSettings()
|
||||
} label: {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.Material.Background.light)
|
||||
.overlay {
|
||||
VStack {
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.Material.Elements.active)
|
||||
.font(.system(size: 30))
|
||||
Text("Allow gallery access")
|
||||
.foregroundColor(.Material.Text.main)
|
||||
.font(.body3)
|
||||
}
|
||||
}
|
||||
.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await store.checkGalleryAuthorization()
|
||||
}
|
||||
.onChange(of: store.galleryAccessGranted) { flag in
|
||||
if flag {
|
||||
Task {
|
||||
await store.fetchGalleryItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GridViewItem: View {
|
||||
// let item: SharingGalleryItem
|
||||
@State var item: GalleryItem
|
||||
@Binding var selected: [String]
|
||||
@State var isSelected = false
|
||||
|
||||
var body: some View {
|
||||
Text("Test")
|
||||
// if let data = item.thumbnail {
|
||||
// ZStack {
|
||||
// Image(uiImage: UIImage(data: data) ?? UIImage())
|
||||
// .resizable()
|
||||
// .aspectRatio(contentMode: .fill)
|
||||
// .frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||
// .clipped()
|
||||
// if let duration = item.duration {
|
||||
// VStack {
|
||||
// Spacer()
|
||||
// HStack {
|
||||
// Spacer()
|
||||
// Text(duration)
|
||||
// .foregroundColor(.Material.Text.white)
|
||||
// .font(.sub1)
|
||||
// .shadow(color: .black, radius: 2)
|
||||
// .padding(4)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if let img = item.thumbnail {
|
||||
ZStack {
|
||||
img
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||
.clipped()
|
||||
if let duration = item.duration {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(duration)
|
||||
.foregroundColor(.Material.Text.white)
|
||||
.font(.sub1)
|
||||
.shadow(color: .black, radius: 2)
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
// if isSelected {
|
||||
// VStack {
|
||||
// HStack {
|
||||
|
@ -80,25 +88,33 @@ private struct GridViewItem: View {
|
|||
// Spacer()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// .onTapGesture {
|
||||
// isSelected.toggle()
|
||||
// if isSelected {
|
||||
// selected.append(item.id)
|
||||
// } else {
|
||||
// selected.removeAll { $0 == item.id }
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// ZStack {
|
||||
// Rectangle()
|
||||
// .fill(Color.Material.Background.light)
|
||||
// .overlay {
|
||||
// ProgressView()
|
||||
// .foregroundColor(.Material.Elements.active)
|
||||
// }
|
||||
// .frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color.Material.Background.light)
|
||||
.overlay {
|
||||
ProgressView()
|
||||
.foregroundColor(.Material.Elements.active)
|
||||
}
|
||||
.frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||
}
|
||||
.task {
|
||||
if item.thumbnail == nil {
|
||||
try? await item.fetchThumbnail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isSelected: Bool {
|
||||
selected.contains(item.id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ struct MediaPickerView: View {
|
|||
CameraCellPreview()
|
||||
|
||||
// For gallery
|
||||
GalleryView()
|
||||
GalleryView(selectedItems: $selectedItems)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue