mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-02 18:36:44 +01:00
feat: bind the thumbnail and trigger media upload task
This commit is contained in:
parent
fc3750c377
commit
bdedd54318
@ -374,7 +374,11 @@
|
|||||||
"video": "video",
|
"video": "video",
|
||||||
"attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
|
"attachment_broken": "This %s is broken and can’t be\nuploaded to Mastodon.",
|
||||||
"description_photo": "Describe the photo for the visually-impaired...",
|
"description_photo": "Describe the photo for the visually-impaired...",
|
||||||
"description_video": "Describe the video for the visually-impaired..."
|
"description_video": "Describe the video for the visually-impaired...",
|
||||||
|
"load_failed": "Load Failed",
|
||||||
|
"upload_failed": "Upload Failed",
|
||||||
|
"can_not_recognize_this_media_attachment": "Can not regonize this media attachment",
|
||||||
|
"attachment_too_large": "Attachment too large"
|
||||||
},
|
},
|
||||||
"poll": {
|
"poll": {
|
||||||
"duration_time": "Duration: %s",
|
"duration_time": "Duration: %s",
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.400",
|
||||||
|
"green" : "0.275",
|
||||||
|
"red" : "0.275"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0.400",
|
||||||
|
"green" : "0.275",
|
||||||
|
"red" : "0.275"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 2.750000 2.750000 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
9.250000 16.500000 m
|
||||||
|
5.245935 16.500000 2.000000 13.254065 2.000000 9.250000 c
|
||||||
|
2.000000 5.245935 5.245935 2.000000 9.250000 2.000000 c
|
||||||
|
13.254065 2.000000 16.500000 5.245935 16.500000 9.250000 c
|
||||||
|
16.500000 9.535608 16.483484 9.817360 16.451357 10.094351 c
|
||||||
|
16.383255 10.681498 16.809317 11.250000 17.400400 11.250000 c
|
||||||
|
17.916018 11.250000 18.369314 10.891933 18.431660 10.380100 c
|
||||||
|
18.476776 10.009713 18.500000 9.632568 18.500000 9.250000 c
|
||||||
|
18.500000 4.141366 14.358634 0.000000 9.250000 0.000000 c
|
||||||
|
4.141366 0.000000 0.000000 4.141366 0.000000 9.250000 c
|
||||||
|
0.000000 14.358634 4.141366 18.500000 9.250000 18.500000 c
|
||||||
|
11.423139 18.500000 13.421247 17.750608 15.000000 16.496151 c
|
||||||
|
15.000000 17.000000 l
|
||||||
|
15.000000 17.552284 15.447716 18.000000 16.000000 18.000000 c
|
||||||
|
16.552284 18.000000 17.000000 17.552284 17.000000 17.000000 c
|
||||||
|
17.000000 14.301708 l
|
||||||
|
17.011232 14.284512 17.022409 14.267276 17.033529 14.250000 c
|
||||||
|
17.000000 14.250000 l
|
||||||
|
17.000000 14.000000 l
|
||||||
|
17.000000 13.447716 16.552284 13.000000 16.000000 13.000000 c
|
||||||
|
13.000000 13.000000 l
|
||||||
|
12.447715 13.000000 12.000000 13.447716 12.000000 14.000000 c
|
||||||
|
12.000000 14.552284 12.447715 15.000000 13.000000 15.000000 c
|
||||||
|
13.666476 15.000000 l
|
||||||
|
12.443584 15.940684 10.912110 16.500000 9.250000 16.500000 c
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
1365
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001455 00000 n
|
||||||
|
0000001478 00000 n
|
||||||
|
0000001651 00000 n
|
||||||
|
0000001725 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1784
|
||||||
|
%%EOF
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Arrow Clockwise.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Dismiss.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"preserves-vector-representation" : true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
%PDF-1.7
|
||||||
|
|
||||||
|
1 0 obj
|
||||||
|
<< >>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
2 0 obj
|
||||||
|
<< /Length 3 0 R >>
|
||||||
|
stream
|
||||||
|
/DeviceRGB CS
|
||||||
|
/DeviceRGB cs
|
||||||
|
q
|
||||||
|
1.000000 0.000000 -0.000000 1.000000 4.000000 3.804749 cm
|
||||||
|
0.000000 0.000000 0.000000 scn
|
||||||
|
0.209704 15.808150 m
|
||||||
|
0.292893 15.902358 l
|
||||||
|
0.653377 16.262842 1.220608 16.290571 1.612899 15.985547 c
|
||||||
|
1.707107 15.902358 l
|
||||||
|
8.000000 9.610251 l
|
||||||
|
14.292892 15.902358 l
|
||||||
|
14.683416 16.292883 15.316584 16.292883 15.707108 15.902358 c
|
||||||
|
16.097631 15.511834 16.097631 14.878669 15.707108 14.488145 c
|
||||||
|
9.415000 8.195251 l
|
||||||
|
15.707108 1.902359 l
|
||||||
|
16.067591 1.541875 16.095320 0.974643 15.790295 0.582352 c
|
||||||
|
15.707108 0.488144 l
|
||||||
|
15.346623 0.127661 14.779391 0.099932 14.387100 0.404957 c
|
||||||
|
14.292892 0.488144 l
|
||||||
|
8.000000 6.780252 l
|
||||||
|
1.707107 0.488144 l
|
||||||
|
1.316582 0.097620 0.683418 0.097620 0.292893 0.488144 c
|
||||||
|
-0.097631 0.878668 -0.097631 1.511835 0.292893 1.902359 c
|
||||||
|
6.585000 8.195251 l
|
||||||
|
0.292893 14.488145 l
|
||||||
|
-0.067591 14.848629 -0.095320 15.415859 0.209704 15.808150 c
|
||||||
|
0.292893 15.902358 l
|
||||||
|
0.209704 15.808150 l
|
||||||
|
h
|
||||||
|
f
|
||||||
|
n
|
||||||
|
Q
|
||||||
|
|
||||||
|
endstream
|
||||||
|
endobj
|
||||||
|
|
||||||
|
3 0 obj
|
||||||
|
914
|
||||||
|
endobj
|
||||||
|
|
||||||
|
4 0 obj
|
||||||
|
<< /Annots []
|
||||||
|
/Type /Page
|
||||||
|
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||||
|
/Resources 1 0 R
|
||||||
|
/Contents 2 0 R
|
||||||
|
/Parent 5 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
5 0 obj
|
||||||
|
<< /Kids [ 4 0 R ]
|
||||||
|
/Count 1
|
||||||
|
/Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
6 0 obj
|
||||||
|
<< /Pages 5 0 R
|
||||||
|
/Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000010 00000 n
|
||||||
|
0000000034 00000 n
|
||||||
|
0000001004 00000 n
|
||||||
|
0000001026 00000 n
|
||||||
|
0000001199 00000 n
|
||||||
|
0000001273 00000 n
|
||||||
|
trailer
|
||||||
|
<< /ID [ (some) (id) ]
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 7
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1332
|
||||||
|
%%EOF
|
@ -130,6 +130,11 @@ public enum Asset {
|
|||||||
}
|
}
|
||||||
public enum Scene {
|
public enum Scene {
|
||||||
public enum Compose {
|
public enum Compose {
|
||||||
|
public enum Attachment {
|
||||||
|
public static let indicatorButtonBackground = ColorAsset(name: "Scene/Compose/Attachment/indicator.button.background")
|
||||||
|
public static let retry = ImageAsset(name: "Scene/Compose/Attachment/retry")
|
||||||
|
public static let stop = ImageAsset(name: "Scene/Compose/Attachment/stop")
|
||||||
|
}
|
||||||
public static let earth = ImageAsset(name: "Scene/Compose/Earth")
|
public static let earth = ImageAsset(name: "Scene/Compose/Earth")
|
||||||
public static let mention = ImageAsset(name: "Scene/Compose/Mention")
|
public static let mention = ImageAsset(name: "Scene/Compose/Mention")
|
||||||
public static let more = ImageAsset(name: "Scene/Compose/More")
|
public static let more = ImageAsset(name: "Scene/Compose/More")
|
||||||
|
@ -43,6 +43,20 @@ extension Mastodon.API.V2.Media {
|
|||||||
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
|
request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment
|
||||||
let serialStream = query.serialStream
|
let serialStream = query.serialStream
|
||||||
request.httpBodyStream = serialStream.boundStreams.input
|
request.httpBodyStream = serialStream.boundStreams.input
|
||||||
|
|
||||||
|
// total unit count in bytes count
|
||||||
|
// will small than actally count due to multipart protocol meta
|
||||||
|
serialStream.progress.totalUnitCount = {
|
||||||
|
var size = 0
|
||||||
|
size += query.file?.sizeInByte ?? 0
|
||||||
|
size += query.thumbnail?.sizeInByte ?? 0
|
||||||
|
return Int64(size)
|
||||||
|
}()
|
||||||
|
query.progress.addChild(
|
||||||
|
serialStream.progress,
|
||||||
|
withPendingUnitCount: query.progress.totalUnitCount
|
||||||
|
)
|
||||||
|
|
||||||
return session.dataTaskPublisher(for: request)
|
return session.dataTaskPublisher(for: request)
|
||||||
.tryMap { data, response in
|
.tryMap { data, response in
|
||||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response)
|
let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response)
|
||||||
|
@ -54,7 +54,7 @@ extension Mastodon.Query.MediaAttachment {
|
|||||||
return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() }
|
return data.map { "data:" + mimeType + ";base64," + $0.base64EncodedString() }
|
||||||
}
|
}
|
||||||
|
|
||||||
var sizeInByte: Int? {
|
public var sizeInByte: Int? {
|
||||||
switch self {
|
switch self {
|
||||||
case .jpeg(let data), .gif(let data), .png(let data):
|
case .jpeg(let data), .gif(let data), .png(let data):
|
||||||
return data?.count
|
return data?.count
|
||||||
|
@ -10,12 +10,17 @@ import UIKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Introspect
|
import Introspect
|
||||||
import AVKit
|
import AVKit
|
||||||
|
import MastodonAsset
|
||||||
|
|
||||||
public struct AttachmentView: View {
|
public struct AttachmentView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: AttachmentViewModel
|
@ObservedObject var viewModel: AttachmentViewModel
|
||||||
|
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
|
|
||||||
|
var blurEffect: UIBlurEffect {
|
||||||
|
UIBlurEffect(style: .systemUltraThinMaterialDark)
|
||||||
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -23,223 +28,81 @@ public struct AttachmentView: View {
|
|||||||
Image(uiImage: image)
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
|
|
||||||
|
// loading…
|
||||||
|
if viewModel.output == nil, viewModel.error == nil {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.circular)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load failed
|
||||||
|
// cannot re-entry
|
||||||
|
if viewModel.output == nil, let error = viewModel.error {
|
||||||
|
VisualEffectView(effect: blurEffect)
|
||||||
|
VStack {
|
||||||
|
Text("Load Failed") // TODO: i18n
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
.font(.system(size: 12, weight: .regular))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loaded
|
||||||
|
// uploading… or upload failed
|
||||||
|
// could retry upload when error emit
|
||||||
|
if viewModel.output != nil {
|
||||||
|
VisualEffectView(effect: blurEffect)
|
||||||
|
VStack {
|
||||||
|
let image: UIImage = {
|
||||||
|
if let _ = viewModel.error {
|
||||||
|
return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate)
|
||||||
|
} else {
|
||||||
|
return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
Image(uiImage: image)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding()
|
||||||
|
.background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color))
|
||||||
|
.clipShape(Circle())
|
||||||
|
.padding()
|
||||||
|
let title: String = {
|
||||||
|
if let _ = viewModel.error {
|
||||||
|
return "Upload Failed" // TODO: i18n
|
||||||
|
} else {
|
||||||
|
let total = ByteCountFormatter.string(fromByteCount: Int64(viewModel.outputSizeInByte), countStyle: .memory)
|
||||||
|
return "…/\(total)"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
let subtitle: String = {
|
||||||
|
if let error = viewModel.error {
|
||||||
|
return error.localizedDescription
|
||||||
|
} else {
|
||||||
|
return "… remaining"
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 13, weight: .semibold))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal)
|
||||||
|
Text(subtitle)
|
||||||
|
.font(.system(size: 12, weight: .regular))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end ZStack
|
||||||
|
.onChange(of: viewModel.progress) { progress in
|
||||||
|
// not works…
|
||||||
|
print(progress.completedUnitCount)
|
||||||
}
|
}
|
||||||
// Menu {
|
|
||||||
// menu
|
|
||||||
// } label: {
|
|
||||||
// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
|
|
||||||
// Image(uiImage: image)
|
|
||||||
// .resizable()
|
|
||||||
// .aspectRatio(contentMode: .fill)
|
|
||||||
// .frame(width: AttachmentView.size.width, height: AttachmentView.size.height)
|
|
||||||
// .overlay {
|
|
||||||
// ZStack {
|
|
||||||
// // spinner
|
|
||||||
// if viewModel.output == nil {
|
|
||||||
// Color.clear
|
|
||||||
// .background(.ultraThinMaterial)
|
|
||||||
// ProgressView()
|
|
||||||
// .progressViewStyle(CircularProgressViewStyle())
|
|
||||||
// .foregroundStyle(.regularMaterial)
|
|
||||||
// }
|
|
||||||
// // border
|
|
||||||
// RoundedRectangle(cornerRadius: AttachmentView.cornerRadius)
|
|
||||||
// .stroke(Color.black.opacity(0.05))
|
|
||||||
// }
|
|
||||||
// .transition(.opacity)
|
|
||||||
// }
|
|
||||||
// .overlay(alignment: .bottom) {
|
|
||||||
// HStack(alignment: .bottom) {
|
|
||||||
// // alt
|
|
||||||
// VStack(spacing: 2) {
|
|
||||||
// switch viewModel.output {
|
|
||||||
// case .video:
|
|
||||||
// Image(uiImage: Asset.Media.playerRectangle.image)
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 16, height: 12)
|
|
||||||
// default:
|
|
||||||
// EmptyView()
|
|
||||||
// }
|
|
||||||
// if !viewModel.caption.isEmpty {
|
|
||||||
// Image(uiImage: Asset.Media.altRectangle.image)
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 16, height: 12)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Spacer()
|
|
||||||
// // option
|
|
||||||
// Image(systemName: "ellipsis")
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 12, height: 12)
|
|
||||||
// .symbolVariant(.circle)
|
|
||||||
// .symbolVariant(.fill)
|
|
||||||
// .symbolRenderingMode(.palette)
|
|
||||||
// .foregroundStyle(.white, .black)
|
|
||||||
// }
|
|
||||||
// .padding(6)
|
|
||||||
// }
|
|
||||||
// .cornerRadius(AttachmentView.cornerRadius)
|
|
||||||
// } // end Menu
|
|
||||||
// .sheet(isPresented: $isCaptionEditorPresented) {
|
|
||||||
// captionSheet
|
|
||||||
// } // end caption sheet
|
|
||||||
// .sheet(isPresented: $viewModel.isPreviewPresented) {
|
|
||||||
// previewSheet
|
|
||||||
// } // end preview sheet
|
|
||||||
|
|
||||||
} // end body
|
} // end body
|
||||||
|
|
||||||
// var menu: some View {
|
|
||||||
// Group {
|
|
||||||
// Button(
|
|
||||||
// action: {
|
|
||||||
// action(.preview)
|
|
||||||
// },
|
|
||||||
// label: {
|
|
||||||
// Label(L10n.Scene.Compose.Media.preview, systemImage: "photo")
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// // caption
|
|
||||||
// let canAddCaption: Bool = {
|
|
||||||
// switch viewModel.output {
|
|
||||||
// case .image: return true
|
|
||||||
// case .video: return false
|
|
||||||
// case .none: return false
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
// if canAddCaption {
|
|
||||||
// Button(
|
|
||||||
// action: {
|
|
||||||
// action(.caption)
|
|
||||||
// caption = viewModel.caption
|
|
||||||
// isCaptionEditorPresented.toggle()
|
|
||||||
// },
|
|
||||||
// label: {
|
|
||||||
// let title = viewModel.caption.isEmpty ? L10n.Scene.Compose.Media.Caption.add : L10n.Scene.Compose.Media.Caption.update
|
|
||||||
// Label(title, systemImage: "text.bubble")
|
|
||||||
// // FIXME: https://stackoverflow.com/questions/72318730/how-to-customize-swiftui-menu
|
|
||||||
// // add caption subtitle
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// Divider()
|
|
||||||
// // remove
|
|
||||||
// Button(
|
|
||||||
// role: .destructive,
|
|
||||||
// action: {
|
|
||||||
// action(.remove)
|
|
||||||
// },
|
|
||||||
// label: {
|
|
||||||
// Label(L10n.Scene.Compose.Media.remove, systemImage: "minus.circle")
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var captionSheet: some View {
|
|
||||||
// NavigationView {
|
|
||||||
// ScrollView(.vertical) {
|
|
||||||
// VStack {
|
|
||||||
// // preview
|
|
||||||
// switch viewModel.output {
|
|
||||||
// case .image:
|
|
||||||
// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
|
|
||||||
// Image(uiImage: image)
|
|
||||||
// .resizable()
|
|
||||||
// .aspectRatio(contentMode: .fill)
|
|
||||||
// case .video(let url, _):
|
|
||||||
// let player = AVPlayer(url: url)
|
|
||||||
// VideoPlayer(player: player)
|
|
||||||
// .frame(height: 300)
|
|
||||||
// case .none:
|
|
||||||
// EmptyView()
|
|
||||||
// }
|
|
||||||
// // caption textField
|
|
||||||
// TextField(
|
|
||||||
// text: $caption,
|
|
||||||
// prompt: Text(L10n.Scene.Compose.Media.Caption.addADescriptionForThisImage)
|
|
||||||
// ) {
|
|
||||||
// Text(L10n.Scene.Compose.Media.Caption.update)
|
|
||||||
// }
|
|
||||||
// .padding()
|
|
||||||
// .introspectTextField { textField in
|
|
||||||
// textField.becomeFirstResponder()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .navigationTitle(L10n.Scene.Compose.Media.Caption.update)
|
|
||||||
// .navigationBarTitleDisplayMode(.inline)
|
|
||||||
// .toolbar {
|
|
||||||
// ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
// Button {
|
|
||||||
// isCaptionEditorPresented.toggle()
|
|
||||||
// } label: {
|
|
||||||
// Image(systemName: "xmark.circle.fill")
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 30, height: 30, alignment: .center)
|
|
||||||
// .symbolRenderingMode(.hierarchical)
|
|
||||||
// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
// Button {
|
|
||||||
// viewModel.caption = caption.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
// isCaptionEditorPresented.toggle()
|
|
||||||
// } label: {
|
|
||||||
// Text(L10n.Common.Controls.Actions.save)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } // end NavigationView
|
|
||||||
// }
|
|
||||||
|
|
||||||
// design for share extension
|
|
||||||
// preferred UIKit preview in app
|
|
||||||
// var previewSheet: some View {
|
|
||||||
// NavigationView {
|
|
||||||
// ScrollView(.vertical) {
|
|
||||||
// VStack {
|
|
||||||
// // preview
|
|
||||||
// switch viewModel.output {
|
|
||||||
// case .image:
|
|
||||||
// let image = viewModel.thumbnail ?? .placeholder(color: .systemGray3)
|
|
||||||
// Image(uiImage: image)
|
|
||||||
// .resizable()
|
|
||||||
// .aspectRatio(contentMode: .fill)
|
|
||||||
// case .video(let url, _):
|
|
||||||
// let player = AVPlayer(url: url)
|
|
||||||
// VideoPlayer(player: player)
|
|
||||||
// .frame(height: 300)
|
|
||||||
// case .none:
|
|
||||||
// EmptyView()
|
|
||||||
// }
|
|
||||||
// Spacer()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// .navigationTitle(L10n.Scene.Compose.Media.preview)
|
|
||||||
// .navigationBarTitleDisplayMode(.inline)
|
|
||||||
// .toolbar {
|
|
||||||
// ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
// Button {
|
|
||||||
// viewModel.isPreviewPresented.toggle()
|
|
||||||
// } label: {
|
|
||||||
// Image(systemName: "xmark.circle.fill")
|
|
||||||
// .resizable()
|
|
||||||
// .frame(width: 30, height: 30, alignment: .center)
|
|
||||||
// .symbolRenderingMode(.hierarchical)
|
|
||||||
// .foregroundStyle(Color(uiColor: .secondaryLabel), Color(uiColor: .tertiaryLabel))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } // end NavigationView
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentView {
|
extension AttachmentView {
|
||||||
public enum Action: Hashable {
|
public enum Action: Hashable {
|
||||||
case preview
|
|
||||||
case caption
|
|
||||||
case remove
|
case remove
|
||||||
|
case retry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
//
|
||||||
|
// AttachmentViewModel+DragAndDrop.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022/11/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
// MARK: - TypeIdentifiedItemProvider
|
||||||
|
extension AttachmentViewModel: TypeIdentifiedItemProvider {
|
||||||
|
public static var typeIdentifier: String {
|
||||||
|
// must in UTI format
|
||||||
|
// https://developer.apple.com/library/archive/qa/qa1796/_index.html
|
||||||
|
return "org.joinmastodon.app.AttachmentViewModel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NSItemProviderWriting
|
||||||
|
extension AttachmentViewModel: NSItemProviderWriting {
|
||||||
|
|
||||||
|
|
||||||
|
/// Attachment uniform type idendifiers
|
||||||
|
///
|
||||||
|
/// The latest one for in-app drag and drop.
|
||||||
|
/// And use generic `image` and `movie` type to
|
||||||
|
/// allows transformable media in different formats
|
||||||
|
public static var writableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
return [
|
||||||
|
UTType.image.identifier,
|
||||||
|
UTType.movie.identifier,
|
||||||
|
AttachmentViewModel.typeIdentifier,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var writableTypeIdentifiersForItemProvider: [String] {
|
||||||
|
// should append elements in priority order from high to low
|
||||||
|
var typeIdentifiers: [String] = []
|
||||||
|
|
||||||
|
// FIXME: check jpg or png
|
||||||
|
switch input {
|
||||||
|
case .image:
|
||||||
|
typeIdentifiers.append(UTType.png.identifier)
|
||||||
|
case .url(let url):
|
||||||
|
let _uti = UTType(filenameExtension: url.pathExtension)
|
||||||
|
if let uti = _uti {
|
||||||
|
if uti.conforms(to: .image) {
|
||||||
|
typeIdentifiers.append(UTType.png.identifier)
|
||||||
|
} else if uti.conforms(to: .movie) {
|
||||||
|
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .pickerResult(let item):
|
||||||
|
if item.itemProvider.isImage() {
|
||||||
|
typeIdentifiers.append(UTType.png.identifier)
|
||||||
|
} else if item.itemProvider.isMovie() {
|
||||||
|
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
||||||
|
}
|
||||||
|
case .itemProvider(let itemProvider):
|
||||||
|
if itemProvider.isImage() {
|
||||||
|
typeIdentifiers.append(UTType.png.identifier)
|
||||||
|
} else if itemProvider.isMovie() {
|
||||||
|
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typeIdentifiers.append(AttachmentViewModel.typeIdentifier)
|
||||||
|
|
||||||
|
return typeIdentifiers
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadData(
|
||||||
|
withTypeIdentifier typeIdentifier: String,
|
||||||
|
forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
|
||||||
|
) -> Progress? {
|
||||||
|
switch typeIdentifier {
|
||||||
|
case AttachmentViewModel.typeIdentifier:
|
||||||
|
do {
|
||||||
|
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
|
||||||
|
try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey)
|
||||||
|
archiver.finishEncoding()
|
||||||
|
let data = archiver.encodedData
|
||||||
|
completionHandler(data, nil)
|
||||||
|
} catch {
|
||||||
|
assertionFailure()
|
||||||
|
completionHandler(nil, nil)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadingProgress = Progress(totalUnitCount: 100)
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
$output,
|
||||||
|
$error
|
||||||
|
)
|
||||||
|
.sink { [weak self] output, error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// continue when load completed
|
||||||
|
guard output != nil || error != nil else { return }
|
||||||
|
|
||||||
|
switch output {
|
||||||
|
case .image(let data, _):
|
||||||
|
switch typeIdentifier {
|
||||||
|
case UTType.png.identifier:
|
||||||
|
loadingProgress.completedUnitCount = 100
|
||||||
|
completionHandler(data, nil)
|
||||||
|
default:
|
||||||
|
completionHandler(nil, nil)
|
||||||
|
}
|
||||||
|
case .video(let url, _):
|
||||||
|
switch typeIdentifier {
|
||||||
|
case UTType.png.identifier:
|
||||||
|
let _image = AttachmentViewModel.createThumbnailForVideo(url: url)
|
||||||
|
let _data = _image?.pngData()
|
||||||
|
loadingProgress.completedUnitCount = 100
|
||||||
|
completionHandler(_data, nil)
|
||||||
|
case UTType.mpeg4Movie.identifier:
|
||||||
|
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||||
|
completionHandler(data, error)
|
||||||
|
}
|
||||||
|
task.progress.observe(\.fractionCompleted) { progress, change in
|
||||||
|
loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted)
|
||||||
|
}
|
||||||
|
.store(in: &self.observations)
|
||||||
|
task.resume()
|
||||||
|
default:
|
||||||
|
completionHandler(nil, nil)
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
return loadingProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
//
|
||||||
|
// AttachmentViewModel+Load.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022/11/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
extension AttachmentViewModel {
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func load(input: Input) async throws -> Output {
|
||||||
|
switch input {
|
||||||
|
case .image(let image):
|
||||||
|
guard let data = image.pngData() else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
return .image(data, imageKind: .png)
|
||||||
|
case .url(let url):
|
||||||
|
do {
|
||||||
|
let output = try await AttachmentViewModel.load(url: url)
|
||||||
|
return output
|
||||||
|
} catch {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
case .pickerResult(let pickerResult):
|
||||||
|
do {
|
||||||
|
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
||||||
|
return output
|
||||||
|
} catch {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
case .itemProvider(let itemProvider):
|
||||||
|
do {
|
||||||
|
let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
|
||||||
|
return output
|
||||||
|
} catch {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func load(url: URL) async throws -> Output {
|
||||||
|
guard let uti = UTType(filenameExtension: url.pathExtension) else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
|
||||||
|
if uti.conforms(to: .image) {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
let imageData = try Data(contentsOf: url)
|
||||||
|
return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg)
|
||||||
|
} else if uti.conforms(to: .movie) {
|
||||||
|
guard url.startAccessingSecurityScopedResource() else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
defer { url.stopAccessingSecurityScopedResource() }
|
||||||
|
|
||||||
|
let fileName = UUID().uuidString
|
||||||
|
let tempDirectoryURL = FileManager.default.temporaryDirectory
|
||||||
|
let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension)
|
||||||
|
try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
try FileManager.default.copyItem(at: url, to: fileURL)
|
||||||
|
return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4")
|
||||||
|
} else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func load(itemProvider: NSItemProvider) async throws -> Output {
|
||||||
|
if itemProvider.isImage() {
|
||||||
|
guard let result = try await itemProvider.loadImageData() else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
let imageKind: Output.ImageKind = {
|
||||||
|
if let type = result.type {
|
||||||
|
if type == UTType.png {
|
||||||
|
return .png
|
||||||
|
}
|
||||||
|
if type == UTType.jpeg {
|
||||||
|
return .jpg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let imageData = result.data
|
||||||
|
|
||||||
|
if imageData.kf.imageFormat == .PNG {
|
||||||
|
return .png
|
||||||
|
}
|
||||||
|
if imageData.kf.imageFormat == .JPEG {
|
||||||
|
return .jpg
|
||||||
|
}
|
||||||
|
|
||||||
|
assertionFailure("unknown image kind")
|
||||||
|
return .jpg
|
||||||
|
}()
|
||||||
|
return .image(result.data, imageKind: imageKind)
|
||||||
|
} else if itemProvider.isMovie() {
|
||||||
|
guard let result = try await itemProvider.loadVideoData() else {
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
return .video(result.url, mimeType: "video/mp4")
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
throw AttachmentError.invalidAttachmentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AttachmentViewModel {
|
||||||
|
static func createThumbnailForVideo(url: URL) -> UIImage? {
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path) else { return nil }
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
|
||||||
|
assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
|
||||||
|
do {
|
||||||
|
let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
|
return image
|
||||||
|
} catch {
|
||||||
|
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NSItemProvider {
|
||||||
|
func isImage() -> Bool {
|
||||||
|
return hasRepresentationConforming(
|
||||||
|
toTypeIdentifier: UTType.image.identifier,
|
||||||
|
fileOptions: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMovie() -> Bool {
|
||||||
|
return hasRepresentationConforming(
|
||||||
|
toTypeIdentifier: UTType.movie.identifier,
|
||||||
|
fileOptions: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -52,153 +52,25 @@ extension Data {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Twitter Only
|
|
||||||
//extension AttachmentViewModel {
|
|
||||||
// class SliceResult {
|
|
||||||
//
|
|
||||||
// let fileURL: URL
|
|
||||||
// let chunks: Chunked<FileHandle.AsyncBytes>
|
|
||||||
// let chunkCount: Int
|
|
||||||
// let type: UTType
|
|
||||||
// let sizeInBytes: UInt64
|
|
||||||
//
|
|
||||||
// public init?(
|
|
||||||
// url: URL,
|
|
||||||
// type: UTType
|
|
||||||
// ) {
|
|
||||||
// guard let chunks = try? FileHandle(forReadingFrom: url).bytes.chunked else { return nil }
|
|
||||||
// let _sizeInBytes: UInt64? = {
|
|
||||||
// let attribute = try? FileManager.default.attributesOfItem(atPath: url.path)
|
|
||||||
// return attribute?[.size] as? UInt64
|
|
||||||
// }()
|
|
||||||
// guard let sizeInBytes = _sizeInBytes else { return nil }
|
|
||||||
//
|
|
||||||
// self.fileURL = url
|
|
||||||
// self.chunks = chunks
|
|
||||||
// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes)
|
|
||||||
// self.type = type
|
|
||||||
// self.sizeInBytes = sizeInBytes
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public init?(
|
|
||||||
// imageData: Data,
|
|
||||||
// type: UTType
|
|
||||||
// ) {
|
|
||||||
// let _fileURL = try? FileManager.default.createTemporaryFileURL(
|
|
||||||
// filename: UUID().uuidString,
|
|
||||||
// pathExtension: imageData.kf.imageFormat == .PNG ? "png" : "jpeg"
|
|
||||||
// )
|
|
||||||
// guard let fileURL = _fileURL else { return nil }
|
|
||||||
//
|
|
||||||
// do {
|
|
||||||
// try imageData.write(to: fileURL)
|
|
||||||
// } catch {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// guard let chunks = try? FileHandle(forReadingFrom: fileURL).bytes.chunked else {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
// let sizeInBytes = UInt64(imageData.count)
|
|
||||||
//
|
|
||||||
// self.fileURL = fileURL
|
|
||||||
// self.chunks = chunks
|
|
||||||
// self.chunkCount = SliceResult.chunkCount(chunkSize: UInt64(chunks.chunkSize), sizeInBytes: sizeInBytes)
|
|
||||||
// self.type = type
|
|
||||||
// self.sizeInBytes = sizeInBytes
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// static func chunkCount(chunkSize: UInt64, sizeInBytes: UInt64) -> Int {
|
|
||||||
// guard sizeInBytes > 0 else { return 0 }
|
|
||||||
// let count = sizeInBytes / chunkSize
|
|
||||||
// let remains = sizeInBytes % chunkSize
|
|
||||||
// let result = remains > 0 ? count + 1 : count
|
|
||||||
// return Int(result)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// static func slice(output: Output, sizeLimit: SizeLimit) -> SliceResult? {
|
|
||||||
// // needs execute in background
|
|
||||||
// assert(!Thread.isMainThread)
|
|
||||||
//
|
|
||||||
// // try png then use JPEG compress with Q=0.8
|
|
||||||
// // then slice into 1MiB chunks
|
|
||||||
// switch output {
|
|
||||||
// case .image(let data, _):
|
|
||||||
// let maxPayloadSizeInBytes = sizeLimit.image
|
|
||||||
//
|
|
||||||
// // use processed imageData to remove EXIF
|
|
||||||
// guard let image = UIImage(data: data),
|
|
||||||
// var imageData = image.pngData()
|
|
||||||
// else { return nil }
|
|
||||||
//
|
|
||||||
// var didRemoveEXIF = false
|
|
||||||
// repeat {
|
|
||||||
// guard let image = KFCrossPlatformImage(data: imageData) else { return nil }
|
|
||||||
// if imageData.kf.imageFormat == .PNG {
|
|
||||||
// // A. png image
|
|
||||||
// guard let pngData = image.pngData() else { return nil }
|
|
||||||
// didRemoveEXIF = true
|
|
||||||
// if pngData.count > maxPayloadSizeInBytes {
|
|
||||||
// guard let compressedJpegData = image.jpegData(compressionQuality: 0.8) else { return nil }
|
|
||||||
// os_log("%{public}s[%{public}ld], %{public}s: compress png %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024)
|
|
||||||
// imageData = compressedJpegData
|
|
||||||
// } else {
|
|
||||||
// os_log("%{public}s[%{public}ld], %{public}s: png %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(pngData.count) / 1024 / 1024)
|
|
||||||
// imageData = pngData
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// // B. other image
|
|
||||||
// if !didRemoveEXIF {
|
|
||||||
// guard let jpegData = image.jpegData(compressionQuality: 0.8) else { return nil }
|
|
||||||
// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(jpegData.count) / 1024 / 1024)
|
|
||||||
// imageData = jpegData
|
|
||||||
// didRemoveEXIF = true
|
|
||||||
// } else {
|
|
||||||
// let targetSize = CGSize(width: image.size.width * 0.8, height: image.size.height * 0.8)
|
|
||||||
// let scaledImage = image.af.imageScaled(to: targetSize)
|
|
||||||
// guard let compressedJpegData = scaledImage.jpegData(compressionQuality: 0.8) else { return nil }
|
|
||||||
// os_log("%{public}s[%{public}ld], %{public}s: compress jpeg %.2fMiB -> jpeg %.2fMiB", ((#file as NSString).lastPathComponent), #line, #function, Double(imageData.count) / 1024 / 1024, Double(compressedJpegData.count) / 1024 / 1024)
|
|
||||||
// imageData = compressedJpegData
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// } while (imageData.count > maxPayloadSizeInBytes)
|
|
||||||
//
|
|
||||||
// return SliceResult(
|
|
||||||
// imageData: imageData,
|
|
||||||
// type: imageData.kf.imageFormat == .PNG ? UTType.png : UTType.jpeg
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
//// case .gif(let url):
|
|
||||||
//// fatalError()
|
|
||||||
// case .video(let url, _):
|
|
||||||
// return SliceResult(
|
|
||||||
// url: url,
|
|
||||||
// type: .movie
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
extension AttachmentViewModel {
|
||||||
struct UploadContext {
|
struct UploadContext {
|
||||||
let apiService: APIService
|
let apiService: APIService
|
||||||
let authContext: AuthContext
|
let authContext: AuthContext
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UploadResult {
|
public typealias UploadResult = Mastodon.Entity.Attachment
|
||||||
case mastodon(Mastodon.Response.Content<Mastodon.Entity.Attachment>)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
extension AttachmentViewModel {
|
||||||
func upload(context: UploadContext) async throws -> UploadResult {
|
func upload(context: UploadContext) async throws -> UploadResult {
|
||||||
return try await uploadMastodonMedia(
|
return try await uploadMastodonMedia(
|
||||||
context: context
|
context: context
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MainActor is required here to trigger stream upload task
|
||||||
|
@MainActor
|
||||||
private func uploadMastodonMedia(
|
private func uploadMastodonMedia(
|
||||||
context: UploadContext
|
context: UploadContext
|
||||||
) async throws -> UploadResult {
|
) async throws -> UploadResult {
|
||||||
@ -283,7 +155,7 @@ extension AttachmentViewModel {
|
|||||||
|
|
||||||
// escape here
|
// escape here
|
||||||
progress.completedUnitCount = progress.totalUnitCount
|
progress.completedUnitCount = progress.totalUnitCount
|
||||||
return .mastodon(attachmentStatusResponse)
|
return attachmentStatusResponse.value
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
|
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): attachment processing. Retry \(waitProcessRetryCount)/\(waitProcessRetryLimit)")
|
||||||
@ -296,7 +168,7 @@ extension AttachmentViewModel {
|
|||||||
} else {
|
} else {
|
||||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "<nil>")")
|
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment success: \(attachmentUploadResponse.value.url ?? "<nil>")")
|
||||||
|
|
||||||
return .mastodon(attachmentUploadResponse)
|
return attachmentUploadResponse.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import MastodonCore
|
|||||||
final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable {
|
final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable {
|
||||||
|
|
||||||
static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel")
|
static let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel")
|
||||||
|
let logger = Logger(subsystem: "AttachmentViewModel", category: "ViewModel")
|
||||||
|
|
||||||
public let id = UUID()
|
public let id = UUID()
|
||||||
|
|
||||||
@ -22,32 +23,52 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||||||
var observations = Set<NSKeyValueObservation>()
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
|
public let api: APIService
|
||||||
public let authContext: AuthContext
|
public let authContext: AuthContext
|
||||||
public let input: Input
|
public let input: Input
|
||||||
@Published var caption = ""
|
@Published var caption = ""
|
||||||
@Published var sizeLimit = SizeLimit()
|
@Published var sizeLimit = SizeLimit()
|
||||||
@Published public var isPreviewPresented = false
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
@Published public private(set) var output: Output?
|
@Published public private(set) var output: Output?
|
||||||
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
|
@Published public private(set) var thumbnail: UIImage? // original size image thumbnail
|
||||||
|
@Published public private(set) var outputSizeInByte: Int = 0
|
||||||
|
|
||||||
|
@Published public var uploadResult: UploadResult?
|
||||||
@Published var error: Error?
|
@Published var error: Error?
|
||||||
|
|
||||||
let progress = Progress() // upload progress
|
let progress = Progress() // upload progress
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
api: APIService,
|
||||||
authContext: AuthContext,
|
authContext: AuthContext,
|
||||||
input: Input
|
input: Input
|
||||||
) {
|
) {
|
||||||
|
self.api = api
|
||||||
self.authContext = authContext
|
self.authContext = authContext
|
||||||
self.input = input
|
self.input = input
|
||||||
super.init()
|
super.init()
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
defer {
|
progress
|
||||||
Task {
|
.observe(\.fractionCompleted, options: [.initial, .new]) { [weak self] progress, _ in
|
||||||
await load(input: input)
|
guard let self = self else { return }
|
||||||
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
.store(in: &observations)
|
||||||
|
|
||||||
|
progress
|
||||||
|
.observe(\.isFinished, options: [.initial, .new]) { [weak self] progress, _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): publish progress \(progress.fractionCompleted)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &observations)
|
||||||
|
|
||||||
$output
|
$output
|
||||||
.map { output -> UIImage? in
|
.map { output -> UIImage? in
|
||||||
@ -62,6 +83,23 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
|||||||
}
|
}
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: &$thumbnail)
|
.assign(to: &$thumbnail)
|
||||||
|
|
||||||
|
defer {
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
let output = try await load(input: input)
|
||||||
|
self.output = output
|
||||||
|
self.outputSizeInByte = output.asAttachment.sizeInByte ?? 0
|
||||||
|
let uploadResult = try await self.upload(context: .init(
|
||||||
|
apiService: self.api,
|
||||||
|
authContext: self.authContext
|
||||||
|
))
|
||||||
|
self.uploadResult = uploadResult
|
||||||
|
} catch {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
} // end Task
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -112,280 +150,23 @@ extension AttachmentViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum AttachmentError: Error {
|
public enum AttachmentError: Error, LocalizedError {
|
||||||
case invalidAttachmentType
|
case invalidAttachmentType
|
||||||
case attachmentTooLarge
|
case attachmentTooLarge
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private func load(input: Input) async {
|
|
||||||
switch input {
|
|
||||||
case .image(let image):
|
|
||||||
guard let data = image.pngData() else {
|
|
||||||
error = AttachmentError.invalidAttachmentType
|
|
||||||
return
|
|
||||||
}
|
|
||||||
output = .image(data, imageKind: .png)
|
|
||||||
case .url(let url):
|
|
||||||
do {
|
|
||||||
let output = try await AttachmentViewModel.load(url: url)
|
|
||||||
self.output = output
|
|
||||||
} catch {
|
|
||||||
self.error = error
|
|
||||||
}
|
|
||||||
case .pickerResult(let pickerResult):
|
|
||||||
do {
|
|
||||||
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
|
||||||
self.output = output
|
|
||||||
} catch {
|
|
||||||
self.error = error
|
|
||||||
}
|
|
||||||
case .itemProvider(let itemProvider):
|
|
||||||
do {
|
|
||||||
let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
|
|
||||||
self.output = output
|
|
||||||
} catch {
|
|
||||||
self.error = error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func load(url: URL) async throws -> Output {
|
|
||||||
guard let uti = UTType(filenameExtension: url.pathExtension) else {
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
|
||||||
|
|
||||||
if uti.conforms(to: .image) {
|
public var errorDescription: String? {
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
switch self {
|
||||||
throw AttachmentError.invalidAttachmentType
|
case .invalidAttachmentType:
|
||||||
|
return "Can not regonize this media attachment" // TODO: i18n
|
||||||
|
case .attachmentTooLarge:
|
||||||
|
return "Attachment too large"
|
||||||
}
|
}
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
|
||||||
let imageData = try Data(contentsOf: url)
|
|
||||||
return .image(imageData, imageKind: imageData.kf.imageFormat == .PNG ? .png : .jpg)
|
|
||||||
} else if uti.conforms(to: .movie) {
|
|
||||||
guard url.startAccessingSecurityScopedResource() else {
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
|
||||||
defer { url.stopAccessingSecurityScopedResource() }
|
|
||||||
|
|
||||||
let fileName = UUID().uuidString
|
|
||||||
let tempDirectoryURL = FileManager.default.temporaryDirectory
|
|
||||||
let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension)
|
|
||||||
try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
|
||||||
try FileManager.default.copyItem(at: url, to: fileURL)
|
|
||||||
return .video(fileURL, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4")
|
|
||||||
} else {
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func load(itemProvider: NSItemProvider) async throws -> Output {
|
|
||||||
if itemProvider.isImage() {
|
|
||||||
guard let result = try await itemProvider.loadImageData() else {
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
|
||||||
let imageKind: Output.ImageKind = {
|
|
||||||
if let type = result.type {
|
|
||||||
if type == UTType.png {
|
|
||||||
return .png
|
|
||||||
}
|
|
||||||
if type == UTType.jpeg {
|
|
||||||
return .jpg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageData = result.data
|
|
||||||
|
|
||||||
if imageData.kf.imageFormat == .PNG {
|
|
||||||
return .png
|
|
||||||
}
|
|
||||||
if imageData.kf.imageFormat == .JPEG {
|
|
||||||
return .jpg
|
|
||||||
}
|
|
||||||
|
|
||||||
assertionFailure("unknown image kind")
|
|
||||||
return .jpg
|
|
||||||
}()
|
|
||||||
return .image(result.data, imageKind: imageKind)
|
|
||||||
} else if itemProvider.isMovie() {
|
|
||||||
guard let result = try await itemProvider.loadVideoData() else {
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
|
||||||
return .video(result.url, mimeType: "video/mp4")
|
|
||||||
} else {
|
|
||||||
assertionFailure()
|
|
||||||
throw AttachmentError.invalidAttachmentType
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
|
||||||
static func createThumbnailForVideo(url: URL) -> UIImage? {
|
|
||||||
guard FileManager.default.fileExists(atPath: url.path) else { return nil }
|
|
||||||
let asset = AVURLAsset(url: url)
|
|
||||||
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
|
|
||||||
assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation
|
|
||||||
do {
|
|
||||||
let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
|
|
||||||
let image = UIImage(cgImage: cgImage)
|
|
||||||
return image
|
|
||||||
} catch {
|
|
||||||
AttachmentViewModel.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): thumbnail generate fail: \(error.localizedDescription)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - TypeIdentifiedItemProvider
|
|
||||||
extension AttachmentViewModel: TypeIdentifiedItemProvider {
|
|
||||||
public static var typeIdentifier: String {
|
|
||||||
// must in UTI format
|
|
||||||
// https://developer.apple.com/library/archive/qa/qa1796/_index.html
|
|
||||||
return "com.twidere.AttachmentViewModel"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NSItemProviderWriting
|
|
||||||
extension AttachmentViewModel: NSItemProviderWriting {
|
|
||||||
|
|
||||||
|
|
||||||
/// Attachment uniform type idendifiers
|
|
||||||
///
|
|
||||||
/// The latest one for in-app drag and drop.
|
|
||||||
/// And use generic `image` and `movie` type to
|
|
||||||
/// allows transformable media in different formats
|
|
||||||
public static var writableTypeIdentifiersForItemProvider: [String] {
|
|
||||||
return [
|
|
||||||
UTType.image.identifier,
|
|
||||||
UTType.movie.identifier,
|
|
||||||
AttachmentViewModel.typeIdentifier,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
public var writableTypeIdentifiersForItemProvider: [String] {
|
|
||||||
// should append elements in priority order from high to low
|
|
||||||
var typeIdentifiers: [String] = []
|
|
||||||
|
|
||||||
// FIXME: check jpg or png
|
|
||||||
switch input {
|
|
||||||
case .image:
|
|
||||||
typeIdentifiers.append(UTType.png.identifier)
|
|
||||||
case .url(let url):
|
|
||||||
let _uti = UTType(filenameExtension: url.pathExtension)
|
|
||||||
if let uti = _uti {
|
|
||||||
if uti.conforms(to: .image) {
|
|
||||||
typeIdentifiers.append(UTType.png.identifier)
|
|
||||||
} else if uti.conforms(to: .movie) {
|
|
||||||
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .pickerResult(let item):
|
|
||||||
if item.itemProvider.isImage() {
|
|
||||||
typeIdentifiers.append(UTType.png.identifier)
|
|
||||||
} else if item.itemProvider.isMovie() {
|
|
||||||
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
|
||||||
}
|
|
||||||
case .itemProvider(let itemProvider):
|
|
||||||
if itemProvider.isImage() {
|
|
||||||
typeIdentifiers.append(UTType.png.identifier)
|
|
||||||
} else if itemProvider.isMovie() {
|
|
||||||
typeIdentifiers.append(UTType.mpeg4Movie.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typeIdentifiers.append(AttachmentViewModel.typeIdentifier)
|
|
||||||
|
|
||||||
return typeIdentifiers
|
|
||||||
}
|
|
||||||
|
|
||||||
public func loadData(
|
|
||||||
withTypeIdentifier typeIdentifier: String,
|
|
||||||
forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
|
|
||||||
) -> Progress? {
|
|
||||||
switch typeIdentifier {
|
|
||||||
case AttachmentViewModel.typeIdentifier:
|
|
||||||
do {
|
|
||||||
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
|
|
||||||
try archiver.encodeEncodable(id, forKey: NSKeyedArchiveRootObjectKey)
|
|
||||||
archiver.finishEncoding()
|
|
||||||
let data = archiver.encodedData
|
|
||||||
completionHandler(data, nil)
|
|
||||||
} catch {
|
|
||||||
assertionFailure()
|
|
||||||
completionHandler(nil, nil)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadingProgress = Progress(totalUnitCount: 100)
|
|
||||||
|
|
||||||
Publishers.CombineLatest(
|
|
||||||
$output,
|
|
||||||
$error
|
|
||||||
)
|
|
||||||
.sink { [weak self] output, error in
|
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
// continue when load completed
|
|
||||||
guard output != nil || error != nil else { return }
|
|
||||||
|
|
||||||
switch output {
|
|
||||||
case .image(let data, _):
|
|
||||||
switch typeIdentifier {
|
|
||||||
case UTType.png.identifier:
|
|
||||||
loadingProgress.completedUnitCount = 100
|
|
||||||
completionHandler(data, nil)
|
|
||||||
default:
|
|
||||||
completionHandler(nil, nil)
|
|
||||||
}
|
|
||||||
case .video(let url, _):
|
|
||||||
switch typeIdentifier {
|
|
||||||
case UTType.png.identifier:
|
|
||||||
let _image = AttachmentViewModel.createThumbnailForVideo(url: url)
|
|
||||||
let _data = _image?.pngData()
|
|
||||||
loadingProgress.completedUnitCount = 100
|
|
||||||
completionHandler(_data, nil)
|
|
||||||
case UTType.mpeg4Movie.identifier:
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
|
||||||
completionHandler(data, error)
|
|
||||||
}
|
|
||||||
task.progress.observe(\.fractionCompleted) { progress, change in
|
|
||||||
loadingProgress.completedUnitCount = Int64(100 * progress.fractionCompleted)
|
|
||||||
}
|
|
||||||
.store(in: &self.observations)
|
|
||||||
task.resume()
|
|
||||||
default:
|
|
||||||
completionHandler(nil, nil)
|
|
||||||
}
|
|
||||||
case nil:
|
|
||||||
completionHandler(nil, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &disposeBag)
|
|
||||||
|
|
||||||
return loadingProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NSItemProvider {
|
|
||||||
fileprivate func isImage() -> Bool {
|
|
||||||
return hasRepresentationConforming(
|
|
||||||
toTypeIdentifier: UTType.image.identifier,
|
|
||||||
fileOptions: []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func isMovie() -> Bool {
|
|
||||||
return hasRepresentationConforming(
|
|
||||||
toTypeIdentifier: UTType.movie.identifier,
|
|
||||||
fileOptions: []
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -326,7 +326,11 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate {
|
|||||||
picker.dismiss(animated: true, completion: nil)
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
let attachmentViewModels: [AttachmentViewModel] = results.map { result in
|
let attachmentViewModels: [AttachmentViewModel] = results.map { result in
|
||||||
AttachmentViewModel(authContext: viewModel.authContext, input: .pickerResult(result))
|
AttachmentViewModel(
|
||||||
|
api: viewModel.context.apiService,
|
||||||
|
authContext: viewModel.authContext,
|
||||||
|
input: .pickerResult(result)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
viewModel.attachmentViewModels += attachmentViewModels
|
viewModel.attachmentViewModels += attachmentViewModels
|
||||||
}
|
}
|
||||||
|
@ -119,13 +119,16 @@ extension MastodonStatusPublisher: StatusPublisher {
|
|||||||
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
progress.addChild(attachmentViewModel.progress, withPendingUnitCount: publishAttachmentTaskWeight)
|
||||||
// upload media
|
// upload media
|
||||||
do {
|
do {
|
||||||
let result = try await attachmentViewModel.upload(context: uploadContext)
|
guard let attachment = attachmentViewModel.uploadResult else {
|
||||||
guard case let .mastodon(response) = result else {
|
// precondition: all media uploaded
|
||||||
assertionFailure()
|
throw AppError.badRequest
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
let attachmentID = response.value.id
|
attachmentIDs.append(attachment.id)
|
||||||
attachmentIDs.append(attachmentID)
|
|
||||||
|
// TODO: allow background upload
|
||||||
|
// let attachment = try await attachmentViewModel.upload(context: uploadContext)
|
||||||
|
// let attachmentID = attachment.id
|
||||||
|
// attachmentIDs.append(attachmentID)
|
||||||
} catch {
|
} catch {
|
||||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
|
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): upload attachment fail: \(error.localizedDescription)")
|
||||||
_state = .failure(error)
|
_state = .failure(error)
|
||||||
|
15
MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift
vendored
Normal file
15
MastodonSDK/Sources/MastodonUI/Vendor/VisualEffectView.swift
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// VisualEffectView.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022/11/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// ref: https://stackoverflow.com/a/59111492/3797903
|
||||||
|
public struct VisualEffectView: UIViewRepresentable {
|
||||||
|
public var effect: UIVisualEffect?
|
||||||
|
public func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { UIVisualEffectView() }
|
||||||
|
public func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { uiView.effect = effect }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user