WIP Media upload

This commit is contained in:
Thomas Ricouard 2022-12-27 16:16:25 +01:00
parent 39d45fd480
commit 627173989e
6 changed files with 143 additions and 28 deletions

View file

@ -23,7 +23,7 @@ public struct MediaAttachement: Codable, Identifiable, Hashable {
public var supportedType: SupportedType? { public var supportedType: SupportedType? {
SupportedType(rawValue: type) SupportedType(rawValue: type)
} }
public let url: URL public let url: URL?
public let previewUrl: URL? public let previewUrl: URL?
public let description: String? public let description: String?
public let meta: MetaContainer? public let meta: MetaContainer?

View file

@ -134,6 +134,24 @@ public class Client: ObservableObject, Equatable {
return urlSession.webSocketTask(with: request) return urlSession.webSocketTask(with: request)
} }
public func mediaUpload(mimeType: String, data: Data) async throws -> MediaAttachement {
let url = makeURL(endpoint: Media.medias, forceVersion: .v2)
var request = makeURLRequest(url: url, httpMethod: "POST")
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let httpBody = NSMutableData()
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"file\"; filename=\"file.png\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Type: \(mimeType)\r\n".data(using: .utf8)!)
httpBody.append("\r\n".data(using: .utf8)!)
httpBody.append(data)
httpBody.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = httpBody as Data
let (data, httpResponse) = try await urlSession.data(for: request)
logResponseOnError(httpResponse: httpResponse, data: data)
return try decoder.decode(MediaAttachement.self, from: data)
}
private func logResponseOnError(httpResponse: URLResponse, data: Data) { private func logResponseOnError(httpResponse: URLResponse, data: Data) {
if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 { if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 {
print(httpResponse) print(httpResponse)

View file

@ -0,0 +1,19 @@
import Foundation
public enum Media: Endpoint {
case medias
case media(id: String)
public func path() -> String {
switch self {
case .medias:
return "media"
case let .media(id):
return "media/\(id)"
}
}
public func queryItems() -> [URLQueryItem]? {
return nil
}
}

View file

@ -6,8 +6,10 @@ import TextView
import Models import Models
import Network import Network
import PhotosUI import PhotosUI
import NukeUI
public struct StatusEditorView: View { public struct StatusEditorView: View {
@EnvironmentObject private var quicklook: QuickLook
@EnvironmentObject private var client: Client @EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount @EnvironmentObject private var currentAccount: CurrentAccount
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@ -94,18 +96,56 @@ public struct StatusEditorView: View {
private var mediasView: some View { private var mediasView: some View {
ScrollView(.horizontal) { ScrollView(.horizontal) {
HStack { HStack(spacing: 8) {
ForEach(viewModel.mediasImages) { container in ForEach(viewModel.mediasImages) { container in
Image(uiImage: container.image) if let localImage = container.image {
.resizable() makeLocalImage(image: localImage)
.aspectRatio(contentMode: .fill) } else if let url = container.mediaAttachement?.url {
.frame(width: 150, height: 150) ZStack(alignment: .topTrailing) {
.clipped() makeLazyImage(url: url)
Button {
withAnimation {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
}
} label: {
Image(systemName: "xmark.circle")
}
.padding(8)
}
}
} }
} }
} }
} }
private func makeLocalImage(image: UIImage) -> some View {
ZStack(alignment: .center) {
Image(uiImage: image)
.resizable()
.blur(radius: 20 )
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.cornerRadius(8)
ProgressView()
}
}
private func makeLazyImage(url: URL?) -> some View {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizingMode(.aspectFill)
.frame(width: 150, height: 150)
} else {
Rectangle()
.frame(width: 150, height: 150)
}
}
.frame(width: 150, height: 150)
.cornerRadius(8)
}
private var accessoryView: some View { private var accessoryView: some View {
HStack { HStack {
PhotosPicker(selection: $viewModel.selectedMedias, PhotosPicker(selection: $viewModel.selectedMedias,

View file

@ -8,7 +8,9 @@ import PhotosUI
public class StatusEditorViewModel: ObservableObject { public class StatusEditorViewModel: ObservableObject {
struct ImageContainer: Identifiable { struct ImageContainer: Identifiable {
let id = UUID().uuidString let id = UUID().uuidString
let image: UIImage let image: UIImage?
let mediaAttachement: MediaAttachement?
let error: Error?
} }
var mode: Mode var mode: Mode
@ -30,9 +32,10 @@ public class StatusEditorViewModel: ObservableObject {
} }
} }
@Published var mediasImages: [ImageContainer] = [] @Published var mediasImages: [ImageContainer] = []
@Published var embededStatus: Status? @Published var embededStatus: Status?
private var uploadTask: Task<Void, Never>?
init(mode: Mode) { init(mode: Mode) {
self.mode = mode self.mode = mode
} }
@ -46,12 +49,12 @@ public class StatusEditorViewModel: ObservableObject {
case .new, .replyTo, .quote: case .new, .replyTo, .quote:
postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string, postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string,
inReplyTo: mode.replyToStatus?.id, inReplyTo: mode.replyToStatus?.id,
mediaIds: nil, mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
spoilerText: nil)) spoilerText: nil))
case let .edit(status): case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
status: statusText.string, status: statusText.string,
mediaIds: nil, mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
spoilerText: nil)) spoilerText: nil))
} }
generator.notificationOccurred(.success) generator.notificationOccurred(.success)
@ -127,20 +130,51 @@ public class StatusEditorViewModel: ObservableObject {
} }
func inflateSelectedMedias() { func inflateSelectedMedias() {
for media in selectedMedias { self.mediasImages = []
media.loadTransferable(type: Data.self) { [weak self] result in
switch result { Task {
case .success(let data?): var medias: [ImageContainer] = []
if let image = UIImage(data: data) { for media in selectedMedias {
DispatchQueue.main.async { do {
self?.mediasImages.append(.init(image: image)) if let data = try await media.loadTransferable(type: Data.self),
} let image = UIImage(data: data) {
medias.append(.init(image: image, mediaAttachement: nil, error: nil))
} }
default: } catch {
break medias.append(.init(image: nil, mediaAttachement: nil, error: error))
}
}
DispatchQueue.main.async { [weak self] in
self?.mediasImages = medias
self?.processUpload()
}
}
}
private func processUpload() {
uploadTask?.cancel()
let mediasCopy = mediasImages
uploadTask = Task {
for (index, media) in mediasCopy.enumerated() {
do {
if !Task.isCancelled,
let data = media.image?.pngData(),
let uploadedMedia = try await uploadMedia(data: data) {
mediasImages[index] = .init(image: nil, mediaAttachement: uploadedMedia, error: nil)
}
} catch {
mediasImages[index] = .init(image: nil, mediaAttachement: nil, error: error)
} }
} }
} }
} }
private func uploadMedia(data: Data) async throws -> MediaAttachement? {
guard let client else { return nil }
do {
return try await client.mediaUpload(mimeType: "image/png", data: data)
} catch {
return nil
}
}
} }

View file

@ -40,7 +40,7 @@ public struct StatusMediaPreviewView: View {
makeFeaturedImagePreview(attachement: attachement) makeFeaturedImagePreview(attachement: attachement)
.onTapGesture { .onTapGesture {
Task { Task {
await quickLook.prepareFor(urls: attachements.map{ $0.url }, selectedURL: attachement.url) await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!)
} }
} }
} else { } else {
@ -109,8 +109,10 @@ public struct StatusMediaPreviewView: View {
}) })
} }
case .gifv: case .gifv:
VideoPlayerView(viewModel: .init(url: attachement.url)) if let url = attachement.url {
.frame(height: imageMaxHeight) VideoPlayerView(viewModel: .init(url: url))
.frame(height: imageMaxHeight)
}
case .none: case .none:
EmptyView() EmptyView()
} }
@ -139,16 +141,18 @@ public struct StatusMediaPreviewView: View {
.frame(width: proxy.frame(in: .local).width) .frame(width: proxy.frame(in: .local).width)
.frame(height: imageMaxHeight) .frame(height: imageMaxHeight)
case .gifv: case .gifv:
VideoPlayerView(viewModel: .init(url: attachement.url)) if let url = attachement.url {
.frame(width: proxy.frame(in: .local).width) VideoPlayerView(viewModel: .init(url: url))
.frame(height: imageMaxHeight) .frame(width: proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
}
} }
} }
.frame(height: imageMaxHeight) .frame(height: imageMaxHeight)
} }
.onTapGesture { .onTapGesture {
Task { Task {
await quickLook.prepareFor(urls: attachements.map{ $0.url }, selectedURL: attachement.url) await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!)
} }
} }
} }