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 {
|
enum ClientStoreError: Error {
|
||||||
case clientNotFound
|
case clientNotFound
|
||||||
case rosterNotFound
|
case rosterNotFound
|
||||||
|
case imageNotFound
|
||||||
|
case videoNotFound
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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.layer.addSublayer(previewLayer)
|
||||||
view.previewLayer = previewLayer
|
view.previewLayer = previewLayer
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
captureSession.startRunning()
|
captureSession.startRunning()
|
||||||
|
}
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ struct MediaPickerView: View {
|
||||||
CameraCellPreview()
|
CameraCellPreview()
|
||||||
|
|
||||||
// For gallery
|
// For gallery
|
||||||
GalleryView()
|
GalleryView(selectedItems: $selectedItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue