Videos: Compress them before upload + error handling close #430

This commit is contained in:
Thomas Ricouard 2023-01-27 08:54:41 +01:00
parent 92c1f40535
commit 7f7a967d87
13 changed files with 77 additions and 9 deletions

View file

@ -4,6 +4,7 @@
"action.save" = "Sichern";
"action.done" = "Fertig";
"action.retry" = "Wiederholen";
"action.view.error" = "View error";
"alert.button.ok" = "Ok";
"alert.error" = "Fehler!";

View file

@ -4,6 +4,7 @@
"action.save" = "Save";
"action.done" = "Done";
"action.retry" = "Retry";
"action.view.error" = "View error";
"alert.button.ok" = "OK";
"alert.error" = "Error!";

View file

@ -4,6 +4,7 @@
"action.save" = "Guardar";
"action.done" = "Hecho";
"action.retry" = "Reintentar";
"action.view.error" = "View error";
"alert.button.ok" = "Ok";
"alert.error" = "¡Error!";

View file

@ -4,6 +4,7 @@
"action.save" = "Salva";
"action.done" = "Fatto";
"action.retry" = "Riprova";
"action.view.error" = "View error";
"alert.button.ok" = "Ok";
"alert.error" = "Errore!";

View file

@ -4,6 +4,7 @@
"action.save" = "保存";
"action.done" = "完了";
"action.retry" = "リトライ";
"action.view.error" = "View error";
"alert.button.ok" = "OK";
"alert.error" = "エラー!";

View file

@ -4,6 +4,7 @@
"action.save" = "Opslaan";
"action.done" = "Gereed";
"action.retry" = "Opnieuw";
"action.view.error" = "View error";
"alert.button.ok" = "OK";
"alert.error" = "Fout!";

View file

@ -4,6 +4,7 @@
"action.save" = "Kaydet";
"action.done" = "Tamamlandı";
"action.retry" = "Yeniden Dene";
"action.view.error" = "View error";
"alert.button.ok" = "Tamam";
"alert.error" = "Hata!";

View file

@ -4,6 +4,7 @@
"action.save" = "保存";
"action.done" = "完成";
"action.retry" = "重试";
"action.view.error" = "View error";
"alert.button.ok" = "OK";
"alert.error" = "错误!";

View file

@ -203,7 +203,14 @@ public class Client: ObservableObject, Equatable {
request.httpBody = httpBody as Data
let (data, httpResponse) = try await urlSession.data(for: request)
logResponseOnError(httpResponse: httpResponse, data: data)
return try decoder.decode(Entity.self, from: data)
do {
return try decoder.decode(Entity.self, from: data)
} catch {
if let serverError = try? decoder.decode(ServerError.self, from: data) {
throw serverError
}
throw error
}
}
private func logResponseOnError(httpResponse: URLResponse, data: Data) {

View file

@ -24,7 +24,11 @@ struct StatusEditorAccessoryView: View {
HStack(alignment: .center, spacing: 16) {
PhotosPicker(selection: $viewModel.selectedMedias,
matching: .any(of: [.images, .videos])) {
Image(systemName: "photo.fill.on.rectangle.fill")
if viewModel.isMediasLoading {
ProgressView()
} else {
Image(systemName: "photo.fill.on.rectangle.fill")
}
}
.disabled(viewModel.showPoll)

View file

@ -9,6 +9,8 @@ struct StatusEditorMediaView: View {
@EnvironmentObject private var theme: Theme
@ObservedObject var viewModel: StatusEditorViewModel
@State private var editingContainer: StatusEditorMediaContainer?
@State private var isErrorDisplayed: Bool = false
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
@ -24,6 +26,8 @@ struct StatusEditorMediaView: View {
makeLocalImage(container: container)
} else if container.movieTransferable != nil {
makeVideoAttachement(container: container)
} else if let error = container.error as? ServerError {
makeErrorView(error: error)
}
if container.mediaAttachment?.description?.isEmpty == false {
altMarker
@ -121,15 +125,24 @@ struct StatusEditorMediaView: View {
@ViewBuilder
private func makeImageMenu(container: StatusEditorMediaContainer) -> some View {
if !viewModel.mode.isEditing {
if container.mediaAttachment != nil {
if !viewModel.mode.isEditing {
Button {
editingContainer = container
} label: {
Label(container.mediaAttachment?.description?.isEmpty == false ?
"status.editor.description.edit" : "status.editor.description.add",
systemImage: "pencil.line")
}
}
} else if container.error != nil {
Button {
editingContainer = container
isErrorDisplayed = true
} label: {
Label(container.mediaAttachment?.description?.isEmpty == false ?
"status.editor.description.edit" : "status.editor.description.add",
systemImage: "pencil.line")
Label("action.view.error", systemImage: "exclamationmark.triangle")
}
}
Button(role: .destructive) {
withAnimation {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
@ -138,6 +151,20 @@ struct StatusEditorMediaView: View {
Label("action.delete", systemImage: "trash")
}
}
private func makeErrorView(error: ServerError) -> some View {
ZStack {
placeholderView
Text("alert.error")
.foregroundColor(.red)
}
.alert("alert.error", isPresented: $isErrorDisplayed) {
Button("Ok", action: { })
} message: {
Text(error.error ?? "")
}
}
private var altMarker: some View {
Button {} label: {

View file

@ -3,6 +3,7 @@ import PhotosUI
import SwiftUI
import UIKit
import UniformTypeIdentifiers
import AVFoundation
@MainActor
enum StatusEditorUTTypeSupported: String, CaseIterable {
@ -70,7 +71,25 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
}
struct MovieFileTranseferable: Transferable {
let url: URL
private let url: URL
var compressedVideoURL: URL? {
get async {
return await withCheckedContinuation { continuation in
let urlAsset = AVURLAsset(url: url, options: nil)
guard let exportSession = AVAssetExportSession(asset: urlAsset, presetName: AVAssetExportPresetMediumQuality) else {
continuation.resume(returning: nil)
return
}
let outputURL = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(url.pathExtension)")
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.exportAsynchronously { () -> Void in
continuation.resume(returning: outputURL)
}
}
}
}
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { movie in

View file

@ -52,9 +52,11 @@ public class StatusEditorViewModel: ObservableObject {
if selectedMedias.count > 4 {
selectedMedias = selectedMedias.prefix(4).map { $0 }
}
isMediasLoading = true
inflateSelectedMedias()
}
}
@Published var isMediasLoading: Bool = false
@Published var mediasImages: [StatusEditorMediaContainer] = []
@Published var replyToStatus: Status?
@ -490,6 +492,7 @@ public class StatusEditorViewModel: ObservableObject {
}
private func processMediasToUpload() {
isMediasLoading = false
uploadTask?.cancel()
let mediasCopy = mediasImages
uploadTask = Task {
@ -522,7 +525,7 @@ public class StatusEditorViewModel: ObservableObject {
if let uploadedMedia, uploadedMedia.url == nil {
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
}
} else if let videoURL = originalContainer.movieTransferable?.url,
} else if let videoURL = await originalContainer.movieTransferable?.compressedVideoURL,
let data = try? Data(contentsOf: videoURL)
{
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())