mv-experiment #1

Merged
fmodf merged 88 commits from mv-experiment into develop 2024-09-03 15:13:59 +00:00
9 changed files with 261 additions and 92 deletions
Showing only changes of commit b8dca34f84 - Show all commits

View 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)
}
}
}

View file

@ -1,4 +1,6 @@
enum ClientStoreError: Error {
case clientNotFound
case rosterNotFound
case imageNotFound
case videoNotFound
}

View file

@ -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()
}
}

View 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
}
}

View 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)
}
}
}
}
}

View 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
}
}

View file

@ -25,7 +25,9 @@ struct CameraView: UIViewRepresentable {
view.layer.addSublayer(previewLayer)
view.previewLayer = previewLayer
DispatchQueue.global(qos: .background).async {
captureSession.startRunning()
}
return view
}

View file

@ -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)
}
}

View file

@ -17,7 +17,7 @@ struct MediaPickerView: View {
CameraCellPreview()
// For gallery
GalleryView()
GalleryView(selectedItems: $selectedItems)
}
}