feat: restore media description text field

This commit is contained in:
CMK 2022-11-14 00:57:44 +08:00
parent 82abc68486
commit 1e71f0c147
3 changed files with 183 additions and 119 deletions

View File

@ -11,6 +11,8 @@ import SwiftUI
import Introspect import Introspect
import AVKit import AVKit
import MastodonAsset import MastodonAsset
import MastodonLocalization
import Introspect
public struct AttachmentView: View { public struct AttachmentView: View {
@ -21,129 +23,179 @@ public struct AttachmentView: View {
} }
public var body: some View { public var body: some View {
ZStack { Color.clear.aspectRatio(358.0/232.0, contentMode: .fill)
let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) .overlay(
Image(uiImage: image) ZStack {
.resizable() let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill)
.aspectRatio(contentMode: .fill) Image(uiImage: image)
.resizable()
// loading .aspectRatio(contentMode: .fill)
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))
} }
} )
.overlay(
// loaded ZStack {
// uploading or upload failed Color.clear
// could retry upload when error emit .overlay(
if viewModel.output != nil, viewModel.uploadState != .finish { VStack(alignment: .leading) {
VisualEffectView(effect: blurEffect) let placeholder: String = {
VStack { switch viewModel.output {
let action: AttachmentViewModel.Action = { case .image: return L10n.Scene.Compose.Attachment.descriptionPhoto
if let _ = viewModel.error { case .video: return L10n.Scene.Compose.Attachment.descriptionVideo
return .retry case nil: return ""
} else { }
return .remove }()
} Spacer()
}() TextField(placeholder, text: $viewModel.caption)
Button { .textFieldStyle(.plain)
viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action) .foregroundColor(.white)
} label: { .placeholder(placeholder, when: viewModel.caption.isEmpty)
let image: UIImage = { .padding(8)
switch action {
case .remove:
return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate)
case .retry:
return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate)
} }
}() )
Image(uiImage: image)
.foregroundColor(.white) // loading
.padding() if viewModel.output == nil, viewModel.error == nil {
.background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) ProgressView()
.overlay( .progressViewStyle(.circular)
Group { }
// 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, viewModel.uploadState != .finish {
VisualEffectView(effect: blurEffect)
VStack {
let action: AttachmentViewModel.Action = {
if let _ = viewModel.error {
return .retry
} else {
return .remove
}
}()
Button {
viewModel.delegate?.attachmentViewModel(viewModel, actionButtonDidPressed: action)
} label: {
let image: UIImage = {
switch action {
case .remove:
return Asset.Scene.Compose.Attachment.stop.image.withRenderingMode(.alwaysTemplate)
case .retry:
return Asset.Scene.Compose.Attachment.retry.image.withRenderingMode(.alwaysTemplate)
}
}()
Image(uiImage: image)
.foregroundColor(.white)
.padding()
.background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color))
.overlay(
Group {
switch viewModel.uploadState {
case .compressing:
CircleProgressView(progress: viewModel.videoCompressProgress)
.animation(.default, value: viewModel.videoCompressProgress)
case .uploading:
CircleProgressView(progress: viewModel.fractionCompleted)
.animation(.default, value: viewModel.fractionCompleted)
default:
EmptyView()
}
}
)
.clipShape(Circle())
.padding()
}
let title: String = {
switch action {
case .remove:
switch viewModel.uploadState { switch viewModel.uploadState {
case .compressing: case .compressing:
CircleProgressView(progress: viewModel.videoCompressProgress) return "Comporessing..." // TODO: i18n
.animation(.default, value: viewModel.videoCompressProgress)
case .uploading:
CircleProgressView(progress: viewModel.fractionCompleted)
.animation(.default, value: viewModel.fractionCompleted)
default: default:
EmptyView() if viewModel.fractionCompleted < 0.9 {
let totalSizeInByte = viewModel.outputSizeInByte
let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1
let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte))
let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte))
return "\(upload) / \(total)"
} else {
return "Server Processing..." // TODO: i18n
}
} }
case .retry:
return "Upload Failed" // TODO: i18n
} }
) }()
.clipShape(Circle()) let subtitle: String = {
.padding() switch action {
case .remove:
if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading {
if viewModel.progress.fractionCompleted < 0.9 {
return viewModel.remainTimeLocalizedString ?? ""
} else {
return ""
}
} else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing {
return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? ""
} else {
return ""
}
case .retry:
return viewModel.error?.localizedDescription ?? ""
}
}()
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.white)
.padding(.horizontal)
Text(subtitle)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.white)
.padding(.horizontal)
.lineLimit(nil)
.multilineTextAlignment(.center)
.frame(maxWidth: 240)
}
} }
} // end ZStack
let title: String = { )
switch action {
case .remove:
switch viewModel.uploadState {
case .compressing:
return "Comporessing..." // TODO: i18n
default:
if viewModel.fractionCompleted < 0.9 {
let totalSizeInByte = viewModel.outputSizeInByte
let uploadSizeInByte = Double(totalSizeInByte) * min(1.0, viewModel.fractionCompleted + 0.1) // 9:1
let total = viewModel.byteCountFormatter.string(fromByteCount: Int64(totalSizeInByte))
let upload = viewModel.byteCountFormatter.string(fromByteCount: Int64(uploadSizeInByte))
return "\(upload) / \(total)"
} else {
return "Server Processing..." // TODO: i18n
}
}
case .retry:
return "Upload Failed" // TODO: i18n
}
}()
let subtitle: String = {
switch action {
case .remove:
if viewModel.progress.fractionCompleted < 1, viewModel.uploadState == .uploading {
if viewModel.progress.fractionCompleted < 0.9 {
return viewModel.remainTimeLocalizedString ?? ""
} else {
return ""
}
} else if viewModel.videoCompressProgress < 1, viewModel.uploadState == .compressing {
return viewModel.percentageFormatter.string(from: NSNumber(floatLiteral: viewModel.videoCompressProgress)) ?? ""
} else {
return ""
}
case .retry:
return viewModel.error?.localizedDescription ?? ""
}
}()
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.white)
.padding(.horizontal)
Text(subtitle)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.white)
.padding(.horizontal)
.lineLimit(nil)
.multilineTextAlignment(.center)
.frame(maxWidth: 240)
}
}
} // end ZStack
} // end body } // end body
} }
// https://stackoverflow.com/a/57715771/3797903
extension View {
fileprivate func placeholder<Content: View>(
when shouldShow: Bool,
alignment: Alignment = .leading,
@ViewBuilder placeholder: () -> Content) -> some View {
ZStack(alignment: alignment) {
placeholder().opacity(shouldShow ? 1 : 0)
self
}
}
fileprivate func placeholder(
_ text: String,
when shouldShow: Bool,
alignment: Alignment = .leading) -> some View {
placeholder(when: shouldShow, alignment: alignment) {
Text(text)
.foregroundColor(.white.opacity(0.7))
}
}
}

View File

@ -125,6 +125,21 @@ extension MastodonStatusPublisher: StatusPublisher {
} }
attachmentIDs.append(attachment.id) attachmentIDs.append(attachment.id)
let caption = attachmentViewModel.caption
guard !caption.isEmpty else { continue }
_ = try await api.updateMedia(
domain: authContext.mastodonAuthenticationBox.domain,
attachmentID: attachment.id,
query: .init(
file: nil,
thumbnail: nil,
description: caption,
focus: nil
),
mastodonAuthenticationBox: authContext.mastodonAuthenticationBox
).singleOutput()
// TODO: allow background upload // TODO: allow background upload
// let attachment = try await attachmentViewModel.upload(context: uploadContext) // let attachment = try await attachmentViewModel.upload(context: uploadContext)
// let attachmentID = attachment.id // let attachmentID = attachment.id

View File

@ -219,10 +219,7 @@ extension ComposeContentView {
var mediaView: some View { var mediaView: some View {
VStack(spacing: 16) { VStack(spacing: 16) {
ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in
Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) AttachmentView(viewModel: attachmentViewModel)
.overlay(
AttachmentView(viewModel: attachmentViewModel)
)
.clipShape(Rectangle()) .clipShape(Rectangle())
.badgeView( .badgeView(
Button { Button {