From 1e71f0c147a0ba4665e05a4e61ec6f18caf8b48b Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 14 Nov 2022 00:57:44 +0800 Subject: [PATCH] feat: restore media description text field --- .../Attachment/AttachmentView.swift | 282 +++++++++++------- .../Publisher/MastodonStatusPublisher.swift | 15 + .../View/ComposeContentView.swift | 5 +- 3 files changed, 183 insertions(+), 119 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift index 2dc8bf12f..f55793591 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Attachment/AttachmentView.swift @@ -11,6 +11,8 @@ import SwiftUI import Introspect import AVKit import MastodonAsset +import MastodonLocalization +import Introspect public struct AttachmentView: View { @@ -21,129 +23,179 @@ public struct AttachmentView: View { } public var body: some View { - ZStack { - let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) - Image(uiImage: image) - .resizable() - .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)) + Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) + .overlay( + ZStack { + let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill) + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) } - } - - // 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) + ) + .overlay( + ZStack { + Color.clear + .overlay( + VStack(alignment: .leading) { + let placeholder: String = { + switch viewModel.output { + case .image: return L10n.Scene.Compose.Attachment.descriptionPhoto + case .video: return L10n.Scene.Compose.Attachment.descriptionVideo + case nil: return "" + } + }() + Spacer() + TextField(placeholder, text: $viewModel.caption) + .textFieldStyle(.plain) + .foregroundColor(.white) + .placeholder(placeholder, when: viewModel.caption.isEmpty) + .padding(8) } - }() - Image(uiImage: image) - .foregroundColor(.white) - .padding() - .background(Color(Asset.Scene.Compose.Attachment.indicatorButtonBackground.color)) - .overlay( - Group { + ) + + // 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, 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 { case .compressing: - CircleProgressView(progress: viewModel.videoCompressProgress) - .animation(.default, value: viewModel.videoCompressProgress) - case .uploading: - CircleProgressView(progress: viewModel.fractionCompleted) - .animation(.default, value: viewModel.fractionCompleted) + return "Comporessing..." // TODO: i18n 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()) - .padding() + }() + 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) + } } - - 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 ZStack + ) } // end body } + +// https://stackoverflow.com/a/57715771/3797903 +extension View { + fileprivate func placeholder( + 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)) + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift index 31568552c..93f3dd3a1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Publisher/MastodonStatusPublisher.swift @@ -125,6 +125,21 @@ extension MastodonStatusPublisher: StatusPublisher { } 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 // let attachment = try await attachmentViewModel.upload(context: uploadContext) // let attachmentID = attachment.id diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift index e5f9b56be..456816d6d 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeContentView.swift @@ -219,10 +219,7 @@ extension ComposeContentView { var mediaView: some View { VStack(spacing: 16) { ForEach(viewModel.attachmentViewModels, id: \.self) { attachmentViewModel in - Color.clear.aspectRatio(358.0/232.0, contentMode: .fill) - .overlay( - AttachmentView(viewModel: attachmentViewModel) - ) + AttachmentView(viewModel: attachmentViewModel) .clipShape(Rectangle()) .badgeView( Button {