ios: improve media picker for multiple images/videos (#3538)
* ios: improve media picker to work with multiple images reliably * MainActor
This commit is contained in:
parent
aca3a71b38
commit
a5048db6fa
@ -195,18 +195,18 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String {
|
||||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func uniqueCombine(_ fileName: String) -> String {
|
private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String {
|
||||||
func tryCombine(_ fileName: String, _ n: Int) -> String {
|
func tryCombine(_ fileName: String, _ n: Int) -> String {
|
||||||
let ns = fileName as NSString
|
let ns = fileName as NSString
|
||||||
let name = ns.deletingPathExtension
|
let name = ns.deletingPathExtension
|
||||||
let ext = ns.pathExtension
|
let ext = ns.pathExtension
|
||||||
let suffix = (n == 0) ? "" : "_\(n)"
|
let suffix = (n == 0) ? "" : "_\(n)"
|
||||||
let f = "\(name)\(suffix).\(ext)"
|
let f = "\(name)\(suffix).\(ext)"
|
||||||
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
||||||
}
|
}
|
||||||
return tryCombine(fileName, 0)
|
return tryCombine(fileName, 0)
|
||||||
}
|
}
|
||||||
|
@ -384,10 +384,10 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showMediaPicker) {
|
.sheet(isPresented: $showMediaPicker) {
|
||||||
LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in
|
LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10) { itemsSelected in
|
||||||
showMediaPicker = false
|
await MainActor.run {
|
||||||
if itemsSelected {
|
showMediaPicker = false
|
||||||
DispatchQueue.main.async {
|
if itemsSelected {
|
||||||
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
|
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -488,6 +488,21 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addMediaContent(_ content: UploadContent) async {
|
||||||
|
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
|
||||||
|
var newMedia: [(String, UploadContent?)] = []
|
||||||
|
if case var .mediaPreviews(media) = composeState.preview {
|
||||||
|
media.append((img, content))
|
||||||
|
newMedia = media
|
||||||
|
} else {
|
||||||
|
newMedia = [(img, content)]
|
||||||
|
}
|
||||||
|
await MainActor.run {
|
||||||
|
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var maxFileSize: Int64 {
|
private var maxFileSize: Int64 {
|
||||||
getMaxFileSize(.xftp)
|
getMaxFileSize(.xftp)
|
||||||
}
|
}
|
||||||
|
@ -103,8 +103,10 @@ struct GroupProfileView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showImagePicker) {
|
.sheet(isPresented: $showImagePicker) {
|
||||||
LibraryImagePicker(image: $chosenImage) {
|
LibraryImagePicker(image: $chosenImage) { _ in
|
||||||
didSelectItem in showImagePicker = false
|
await MainActor.run {
|
||||||
|
showImagePicker = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: chosenImage) { image in
|
.onChange(of: chosenImage) { image in
|
||||||
|
@ -13,112 +13,122 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct LibraryImagePicker: View {
|
struct LibraryImagePicker: View {
|
||||||
@Binding var image: UIImage?
|
@Binding var image: UIImage?
|
||||||
var didFinishPicking: (_ didSelectItems: Bool) -> Void
|
var didFinishPicking: (_ didSelectImage: Bool) async -> Void
|
||||||
@State var images: [UploadContent] = []
|
@State var mediaAdded = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
|
LibraryMediaListPicker(addMedia: addMedia, selectionLimit: 1, didFinishPicking: didFinishPicking)
|
||||||
.onChange(of: images) { _ in
|
}
|
||||||
if let img = images.first {
|
|
||||||
image = img.uiImage
|
private func addMedia(_ content: UploadContent) async {
|
||||||
}
|
if mediaAdded { return }
|
||||||
}
|
await MainActor.run {
|
||||||
|
mediaAdded = true
|
||||||
|
image = content.uiImage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LibraryMediaListPicker: UIViewControllerRepresentable {
|
struct LibraryMediaListPicker: UIViewControllerRepresentable {
|
||||||
typealias UIViewControllerType = PHPickerViewController
|
typealias UIViewControllerType = PHPickerViewController
|
||||||
@Binding var media: [UploadContent]
|
var addMedia: (_ content: UploadContent) async -> Void
|
||||||
var selectionLimit: Int
|
var selectionLimit: Int
|
||||||
var didFinishPicking: (_ didSelectItems: Bool) -> Void
|
var didFinishPicking: (_ didSelectItems: Bool) async -> Void
|
||||||
|
|
||||||
class Coordinator: PHPickerViewControllerDelegate {
|
class Coordinator: PHPickerViewControllerDelegate {
|
||||||
let parent: LibraryMediaListPicker
|
let parent: LibraryMediaListPicker
|
||||||
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker")
|
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker")
|
||||||
var media: [UploadContent] = []
|
|
||||||
var mediaCount: Int = 0
|
|
||||||
|
|
||||||
init(_ parent: LibraryMediaListPicker) {
|
init(_ parent: LibraryMediaListPicker) {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
}
|
}
|
||||||
|
|
||||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
parent.didFinishPicking(!results.isEmpty)
|
Task {
|
||||||
guard !results.isEmpty else {
|
await parent.didFinishPicking(!results.isEmpty)
|
||||||
return
|
if results.isEmpty { return }
|
||||||
|
for r in results {
|
||||||
|
await loadItem(r.itemProvider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parent.media = []
|
private func loadItem(_ p: NSItemProvider) async {
|
||||||
media = []
|
logger.debug("LibraryMediaListPicker result")
|
||||||
mediaCount = results.count
|
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||||
for result in results {
|
if let video = await loadVideo(p) {
|
||||||
logger.log("LibraryMediaListPicker result")
|
await self.parent.addMedia(video)
|
||||||
let p = result.itemProvider
|
logger.debug("LibraryMediaListPicker: added video")
|
||||||
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
}
|
||||||
p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
|
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||||
if let url = url {
|
if let img = await loadImageData(p) {
|
||||||
let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension))
|
await self.parent.addMedia(img)
|
||||||
if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) {
|
logger.debug("LibraryMediaListPicker: added image")
|
||||||
ChatModel.shared.filesToDelete.insert(tempUrl)
|
}
|
||||||
self.loadVideo(url: tempUrl, error: error)
|
} else if p.canLoadObject(ofClass: UIImage.self) {
|
||||||
|
if let img = await loadImage(p) {
|
||||||
|
await self.parent.addMedia(.simpleImage(image: img))
|
||||||
|
logger.debug("LibraryMediaListPicker: added image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadImageData(_ p: NSItemProvider) async -> UploadContent? {
|
||||||
|
await withCheckedContinuation { cont in
|
||||||
|
loadFileURL(p, type: UTType.data) { url in
|
||||||
|
if let url = url {
|
||||||
|
let img = UploadContent.loadFromURL(url: url)
|
||||||
|
cont.resume(returning: img)
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadImage(_ p: NSItemProvider) async -> UIImage? {
|
||||||
|
await withCheckedContinuation { cont in
|
||||||
|
p.loadObject(ofClass: UIImage.self) { obj, err in
|
||||||
|
if let err = err {
|
||||||
|
logger.error("LibraryMediaListPicker result image error: \(err.localizedDescription)")
|
||||||
|
cont.resume(returning: nil)
|
||||||
|
} else {
|
||||||
|
cont.resume(returning: obj as? UIImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadVideo(_ p: NSItemProvider) async -> UploadContent? {
|
||||||
|
await withCheckedContinuation { cont in
|
||||||
|
loadFileURL(p, type: UTType.movie) { url in
|
||||||
|
if let url = url {
|
||||||
|
let tempUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "video", url.pathExtension, fullPath: true))
|
||||||
|
do {
|
||||||
|
// logger.debug("LibraryMediaListPicker copyItem \(url) to \(tempUrl)")
|
||||||
|
try FileManager.default.copyItem(at: url, to: tempUrl)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
_ = ChatModel.shared.filesToDelete.insert(tempUrl)
|
||||||
}
|
}
|
||||||
|
let video = UploadContent.loadVideoFromURL(url: tempUrl)
|
||||||
|
cont.resume(returning: video)
|
||||||
|
return
|
||||||
|
} catch let err {
|
||||||
|
logger.error("LibraryMediaListPicker copyItem error: \(err.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
cont.resume(returning: nil)
|
||||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
}
|
||||||
self.loadImage(object: url, error: error)
|
}
|
||||||
}
|
}
|
||||||
} else if p.canLoadObject(ofClass: UIImage.self) {
|
|
||||||
p.loadObject(ofClass: UIImage.self) { image, error in
|
private func loadFileURL(_ p: NSItemProvider, type: UTType, completion: @escaping (URL?) -> Void) {
|
||||||
DispatchQueue.main.async {
|
p.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, err in
|
||||||
self.loadImage(object: image, error: error)
|
if let err = err {
|
||||||
}
|
logger.error("LibraryMediaListPicker loadFileURL error: \(err.localizedDescription)")
|
||||||
}
|
completion(nil)
|
||||||
} else {
|
} else {
|
||||||
dispatchQueue.sync { self.mediaCount -= 1}
|
completion(url)
|
||||||
}
|
|
||||||
}
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
|
||||||
self.dispatchQueue.sync {
|
|
||||||
if self.parent.media.count == 0 {
|
|
||||||
logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)")
|
|
||||||
self.parent.media = self.media
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadImage(object: Any?, error: Error? = nil) {
|
|
||||||
if let error = error {
|
|
||||||
logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)")
|
|
||||||
} else if let image = object as? UIImage {
|
|
||||||
media.append(.simpleImage(image: image))
|
|
||||||
logger.log("LibraryMediaListPicker: added image")
|
|
||||||
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
|
|
||||||
media.append(image)
|
|
||||||
}
|
|
||||||
dispatchQueue.sync {
|
|
||||||
self.mediaCount -= 1
|
|
||||||
if self.mediaCount == 0 && self.parent.media.count == 0 {
|
|
||||||
logger.log("LibraryMediaListPicker: added all media")
|
|
||||||
self.parent.media = self.media
|
|
||||||
self.media = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadVideo(url: URL?, error: Error? = nil) {
|
|
||||||
if let error = error {
|
|
||||||
logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)")
|
|
||||||
} else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) {
|
|
||||||
media.append(video)
|
|
||||||
}
|
|
||||||
dispatchQueue.sync {
|
|
||||||
self.mediaCount -= 1
|
|
||||||
if self.mediaCount == 0 && self.parent.media.count == 0 {
|
|
||||||
logger.log("LibraryMediaListPicker: added all media")
|
|
||||||
self.parent.media = self.media
|
|
||||||
self.media = []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,8 +130,10 @@ struct AddGroupView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showImagePicker) {
|
.sheet(isPresented: $showImagePicker) {
|
||||||
LibraryImagePicker(image: $chosenImage) {
|
LibraryImagePicker(image: $chosenImage) { _ in
|
||||||
didSelectItem in showImagePicker = false
|
await MainActor.run {
|
||||||
|
showImagePicker = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(isPresented: $showInvalidNameAlert) {
|
.alert(isPresented: $showInvalidNameAlert) {
|
||||||
|
@ -120,8 +120,10 @@ struct UserProfile: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showImagePicker) {
|
.sheet(isPresented: $showImagePicker) {
|
||||||
LibraryImagePicker(image: $chosenImage) {
|
LibraryImagePicker(image: $chosenImage) { _ in
|
||||||
didSelectItem in showImagePicker = false
|
await MainActor.run {
|
||||||
|
showImagePicker = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: chosenImage) { image in
|
.onChange(of: chosenImage) { image in
|
||||||
|
Loading…
Reference in New Issue
Block a user