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? {
SupportedType(rawValue: type)
}
public let url: URL
public let url: URL?
public let previewUrl: URL?
public let description: String?
public let meta: MetaContainer?

View file

@ -134,6 +134,24 @@ public class Client: ObservableObject, Equatable {
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) {
if let httpResponse = httpResponse as? HTTPURLResponse, httpResponse.statusCode > 299 {
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 Network
import PhotosUI
import NukeUI
public struct StatusEditorView: View {
@EnvironmentObject private var quicklook: QuickLook
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(\.dismiss) private var dismiss
@ -94,18 +96,56 @@ public struct StatusEditorView: View {
private var mediasView: some View {
ScrollView(.horizontal) {
HStack {
HStack(spacing: 8) {
ForEach(viewModel.mediasImages) { container in
Image(uiImage: container.image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.clipped()
if let localImage = container.image {
makeLocalImage(image: localImage)
} else if let url = container.mediaAttachement?.url {
ZStack(alignment: .topTrailing) {
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 {
HStack {
PhotosPicker(selection: $viewModel.selectedMedias,

View file

@ -8,7 +8,9 @@ import PhotosUI
public class StatusEditorViewModel: ObservableObject {
struct ImageContainer: Identifiable {
let id = UUID().uuidString
let image: UIImage
let image: UIImage?
let mediaAttachement: MediaAttachement?
let error: Error?
}
var mode: Mode
@ -30,8 +32,9 @@ public class StatusEditorViewModel: ObservableObject {
}
}
@Published var mediasImages: [ImageContainer] = []
@Published var embededStatus: Status?
private var uploadTask: Task<Void, Never>?
init(mode: Mode) {
self.mode = mode
@ -46,12 +49,12 @@ public class StatusEditorViewModel: ObservableObject {
case .new, .replyTo, .quote:
postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string,
inReplyTo: mode.replyToStatus?.id,
mediaIds: nil,
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
spoilerText: nil))
case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
status: statusText.string,
mediaIds: nil,
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
spoilerText: nil))
}
generator.notificationOccurred(.success)
@ -127,20 +130,51 @@ public class StatusEditorViewModel: ObservableObject {
}
func inflateSelectedMedias() {
for media in selectedMedias {
media.loadTransferable(type: Data.self) { [weak self] result in
switch result {
case .success(let data?):
if let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.mediasImages.append(.init(image: image))
}
self.mediasImages = []
Task {
var medias: [ImageContainer] = []
for media in selectedMedias {
do {
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:
break
} catch {
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)
.onTapGesture {
Task {
await quickLook.prepareFor(urls: attachements.map{ $0.url }, selectedURL: attachement.url)
await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!)
}
}
} else {
@ -109,8 +109,10 @@ public struct StatusMediaPreviewView: View {
})
}
case .gifv:
VideoPlayerView(viewModel: .init(url: attachement.url))
.frame(height: imageMaxHeight)
if let url = attachement.url {
VideoPlayerView(viewModel: .init(url: url))
.frame(height: imageMaxHeight)
}
case .none:
EmptyView()
}
@ -139,16 +141,18 @@ public struct StatusMediaPreviewView: View {
.frame(width: proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
case .gifv:
VideoPlayerView(viewModel: .init(url: attachement.url))
.frame(width: proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
if let url = attachement.url {
VideoPlayerView(viewModel: .init(url: url))
.frame(width: proxy.frame(in: .local).width)
.frame(height: imageMaxHeight)
}
}
}
.frame(height: imageMaxHeight)
}
.onTapGesture {
Task {
await quickLook.prepareFor(urls: attachements.map{ $0.url }, selectedURL: attachement.url)
await quickLook.prepareFor(urls: attachements.compactMap{ $0.url }, selectedURL: attachement.url!)
}
}
}