WIP Media upload
This commit is contained in:
parent
39d45fd480
commit
627173989e
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue