mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-10 00:05:27 +00:00
WIP Media upload
This commit is contained in:
parent
39d45fd480
commit
627173989e
6 changed files with 143 additions and 28 deletions
|
@ -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?
|
||||||
|
|
|
@ -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)
|
||||||
|
|
19
Packages/Network/Sources/Network/Endpoint/Media.swift
Normal file
19
Packages/Network/Sources/Network/Endpoint/Media.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue