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 { enum ClientStoreError: Error {
case clientNotFound case clientNotFound
case rosterNotFound case rosterNotFound
case imageNotFound
case videoNotFound
} }

View file

@ -1,11 +1,13 @@
import Combine import Combine
import Foundation import Foundation
import Photos import Photos
import SwiftUI
@MainActor @MainActor
final class FileStore: ObservableObject { final class FileStore: ObservableObject {
@Published var cameraAccessGranted = false @Published var cameraAccessGranted = false
@Published var galleryAccessGranted = false @Published var galleryAccessGranted = false
@Published var galleryItems: [GalleryItem] = []
private let client: Client private let client: Client
private let roster: Roster private let roster: Roster
@ -35,4 +37,9 @@ extension FileStore {
} }
galleryAccessGranted = isAuthorized 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.layer.addSublayer(previewLayer)
view.previewLayer = previewLayer view.previewLayer = previewLayer
DispatchQueue.global(qos: .background).async {
captureSession.startRunning() captureSession.startRunning()
}
return view return view
} }

View file

@ -1,67 +1,75 @@
import SwiftUI import SwiftUI
struct GalleryView: View { struct GalleryView: View {
// @State private var selectedItems: [String] = [] @EnvironmentObject var store: FileStore
@Binding var selectedItems: [String]
var body: some View { var body: some View {
Text("test") Group {
// Group { if store.galleryAccessGranted {
// if store.state.sharingState.isGalleryAccessGranted { ForEach(store.galleryItems) { item in
// ForEach(store.state.sharingState.galleryItems) { item in GridViewItem(item: item, selected: $selectedItems)
// GridViewItem(item: item, selected: $selectedItems) }
// } } else {
// } else { Button {
// Button { openAppSettings()
// openAppSettings() } label: {
// } label: { ZStack {
// ZStack { Rectangle()
// Rectangle() .fill(Color.Material.Background.light)
// .fill(Color.Material.Background.light) .overlay {
// .overlay { VStack {
// VStack { Image(systemName: "photo")
// Image(systemName: "photo") .foregroundColor(.Material.Elements.active)
// .foregroundColor(.Material.Elements.active) .font(.system(size: 30))
// .font(.system(size: 30)) Text("Allow gallery access")
// Text("Allow gallery access") .foregroundColor(.Material.Text.main)
// .foregroundColor(.Material.Text.main) .font(.body3)
// .font(.body3) }
// } }
// } .frame(height: 100)
// .frame(height: 100) }
// } }
// } }
// } }
// } .task {
await store.checkGalleryAuthorization()
}
.onChange(of: store.galleryAccessGranted) { flag in
if flag {
Task {
await store.fetchGalleryItems()
}
}
}
} }
} }
private struct GridViewItem: View { private struct GridViewItem: View {
// let item: SharingGalleryItem @State var item: GalleryItem
@Binding var selected: [String] @Binding var selected: [String]
@State var isSelected = false
var body: some View { var body: some View {
Text("Test") if let img = item.thumbnail {
// if let data = item.thumbnail { ZStack {
// ZStack { img
// Image(uiImage: UIImage(data: data) ?? UIImage()) .resizable()
// .resizable() .aspectRatio(contentMode: .fill)
// .aspectRatio(contentMode: .fill) .frame(width: Const.galleryGridSize, height: Const.galleryGridSize)
// .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) .clipped()
// .clipped() if let duration = item.duration {
// if let duration = item.duration { VStack {
// VStack { Spacer()
// Spacer() HStack {
// HStack { Spacer()
// Spacer() Text(duration)
// Text(duration) .foregroundColor(.Material.Text.white)
// .foregroundColor(.Material.Text.white) .font(.sub1)
// .font(.sub1) .shadow(color: .black, radius: 2)
// .shadow(color: .black, radius: 2) .padding(4)
// .padding(4) }
// } }
// } }
// }
// if isSelected { // if isSelected {
// VStack { // VStack {
// HStack { // HStack {
@ -80,25 +88,33 @@ private struct GridViewItem: View {
// Spacer() // Spacer()
// } // }
// } // }
// } }
// .onTapGesture { // .onTapGesture {
// isSelected.toggle()
// if isSelected { // if isSelected {
// selected.append(item.id) // selected.append(item.id)
// } else { // } else {
// selected.removeAll { $0 == item.id } // selected.removeAll { $0 == item.id }
// } // }
// } // }
// } else { } else {
// ZStack { ZStack {
// Rectangle() Rectangle()
// .fill(Color.Material.Background.light) .fill(Color.Material.Background.light)
// .overlay { .overlay {
// ProgressView() ProgressView()
// .foregroundColor(.Material.Elements.active) .foregroundColor(.Material.Elements.active)
// } }
// .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) .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() CameraCellPreview()
// For gallery // For gallery
GalleryView() GalleryView(selectedItems: $selectedItems)
} }
} }