mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 02:01:02 +00:00
Videos: Compress them before upload + error handling close #430
This commit is contained in:
parent
92c1f40535
commit
7f7a967d87
13 changed files with 77 additions and 9 deletions
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Sichern";
|
"action.save" = "Sichern";
|
||||||
"action.done" = "Fertig";
|
"action.done" = "Fertig";
|
||||||
"action.retry" = "Wiederholen";
|
"action.retry" = "Wiederholen";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "Ok";
|
"alert.button.ok" = "Ok";
|
||||||
"alert.error" = "Fehler!";
|
"alert.error" = "Fehler!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Save";
|
"action.save" = "Save";
|
||||||
"action.done" = "Done";
|
"action.done" = "Done";
|
||||||
"action.retry" = "Retry";
|
"action.retry" = "Retry";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "OK";
|
"alert.button.ok" = "OK";
|
||||||
"alert.error" = "Error!";
|
"alert.error" = "Error!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Guardar";
|
"action.save" = "Guardar";
|
||||||
"action.done" = "Hecho";
|
"action.done" = "Hecho";
|
||||||
"action.retry" = "Reintentar";
|
"action.retry" = "Reintentar";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "Ok";
|
"alert.button.ok" = "Ok";
|
||||||
"alert.error" = "¡Error!";
|
"alert.error" = "¡Error!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Salva";
|
"action.save" = "Salva";
|
||||||
"action.done" = "Fatto";
|
"action.done" = "Fatto";
|
||||||
"action.retry" = "Riprova";
|
"action.retry" = "Riprova";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "Ok";
|
"alert.button.ok" = "Ok";
|
||||||
"alert.error" = "Errore!";
|
"alert.error" = "Errore!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "保存";
|
"action.save" = "保存";
|
||||||
"action.done" = "完了";
|
"action.done" = "完了";
|
||||||
"action.retry" = "リトライ";
|
"action.retry" = "リトライ";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "OK";
|
"alert.button.ok" = "OK";
|
||||||
"alert.error" = "エラー!";
|
"alert.error" = "エラー!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Opslaan";
|
"action.save" = "Opslaan";
|
||||||
"action.done" = "Gereed";
|
"action.done" = "Gereed";
|
||||||
"action.retry" = "Opnieuw";
|
"action.retry" = "Opnieuw";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "OK";
|
"alert.button.ok" = "OK";
|
||||||
"alert.error" = "Fout!";
|
"alert.error" = "Fout!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "Kaydet";
|
"action.save" = "Kaydet";
|
||||||
"action.done" = "Tamamlandı";
|
"action.done" = "Tamamlandı";
|
||||||
"action.retry" = "Yeniden Dene";
|
"action.retry" = "Yeniden Dene";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "Tamam";
|
"alert.button.ok" = "Tamam";
|
||||||
"alert.error" = "Hata!";
|
"alert.error" = "Hata!";
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
"action.save" = "保存";
|
"action.save" = "保存";
|
||||||
"action.done" = "完成";
|
"action.done" = "完成";
|
||||||
"action.retry" = "重试";
|
"action.retry" = "重试";
|
||||||
|
"action.view.error" = "View error";
|
||||||
|
|
||||||
"alert.button.ok" = "OK";
|
"alert.button.ok" = "OK";
|
||||||
"alert.error" = "错误!";
|
"alert.error" = "错误!";
|
||||||
|
|
|
@ -203,7 +203,14 @@ public class Client: ObservableObject, Equatable {
|
||||||
request.httpBody = httpBody as Data
|
request.httpBody = httpBody as Data
|
||||||
let (data, httpResponse) = try await urlSession.data(for: request)
|
let (data, httpResponse) = try await urlSession.data(for: request)
|
||||||
logResponseOnError(httpResponse: httpResponse, data: data)
|
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) {
|
private func logResponseOnError(httpResponse: URLResponse, data: Data) {
|
||||||
|
|
|
@ -24,7 +24,11 @@ struct StatusEditorAccessoryView: View {
|
||||||
HStack(alignment: .center, spacing: 16) {
|
HStack(alignment: .center, spacing: 16) {
|
||||||
PhotosPicker(selection: $viewModel.selectedMedias,
|
PhotosPicker(selection: $viewModel.selectedMedias,
|
||||||
matching: .any(of: [.images, .videos])) {
|
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)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ struct StatusEditorMediaView: View {
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@ObservedObject var viewModel: StatusEditorViewModel
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
@State private var editingContainer: StatusEditorMediaContainer?
|
@State private var editingContainer: StatusEditorMediaContainer?
|
||||||
|
|
||||||
|
@State private var isErrorDisplayed: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
@ -24,6 +26,8 @@ struct StatusEditorMediaView: View {
|
||||||
makeLocalImage(container: container)
|
makeLocalImage(container: container)
|
||||||
} else if container.movieTransferable != nil {
|
} else if container.movieTransferable != nil {
|
||||||
makeVideoAttachement(container: container)
|
makeVideoAttachement(container: container)
|
||||||
|
} else if let error = container.error as? ServerError {
|
||||||
|
makeErrorView(error: error)
|
||||||
}
|
}
|
||||||
if container.mediaAttachment?.description?.isEmpty == false {
|
if container.mediaAttachment?.description?.isEmpty == false {
|
||||||
altMarker
|
altMarker
|
||||||
|
@ -121,15 +125,24 @@ struct StatusEditorMediaView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeImageMenu(container: StatusEditorMediaContainer) -> some View {
|
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 {
|
Button {
|
||||||
editingContainer = container
|
isErrorDisplayed = true
|
||||||
} label: {
|
} label: {
|
||||||
Label(container.mediaAttachment?.description?.isEmpty == false ?
|
Label("action.view.error", systemImage: "exclamationmark.triangle")
|
||||||
"status.editor.description.edit" : "status.editor.description.add",
|
|
||||||
systemImage: "pencil.line")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
|
||||||
|
@ -138,6 +151,20 @@ struct StatusEditorMediaView: View {
|
||||||
Label("action.delete", systemImage: "trash")
|
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 {
|
private var altMarker: some View {
|
||||||
Button {} label: {
|
Button {} label: {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import PhotosUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
|
@ -70,7 +71,25 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MovieFileTranseferable: Transferable {
|
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 {
|
static var transferRepresentation: some TransferRepresentation {
|
||||||
FileRepresentation(contentType: .movie) { movie in
|
FileRepresentation(contentType: .movie) { movie in
|
||||||
|
|
|
@ -52,9 +52,11 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
if selectedMedias.count > 4 {
|
if selectedMedias.count > 4 {
|
||||||
selectedMedias = selectedMedias.prefix(4).map { $0 }
|
selectedMedias = selectedMedias.prefix(4).map { $0 }
|
||||||
}
|
}
|
||||||
|
isMediasLoading = true
|
||||||
inflateSelectedMedias()
|
inflateSelectedMedias()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Published var isMediasLoading: Bool = false
|
||||||
|
|
||||||
@Published var mediasImages: [StatusEditorMediaContainer] = []
|
@Published var mediasImages: [StatusEditorMediaContainer] = []
|
||||||
@Published var replyToStatus: Status?
|
@Published var replyToStatus: Status?
|
||||||
|
@ -490,6 +492,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processMediasToUpload() {
|
private func processMediasToUpload() {
|
||||||
|
isMediasLoading = false
|
||||||
uploadTask?.cancel()
|
uploadTask?.cancel()
|
||||||
let mediasCopy = mediasImages
|
let mediasCopy = mediasImages
|
||||||
uploadTask = Task {
|
uploadTask = Task {
|
||||||
|
@ -522,7 +525,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
if let uploadedMedia, uploadedMedia.url == nil {
|
if let uploadedMedia, uploadedMedia.url == nil {
|
||||||
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
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 data = try? Data(contentsOf: videoURL)
|
||||||
{
|
{
|
||||||
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())
|
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())
|
||||||
|
|
Loading…
Reference in a new issue