feat: add mediaView for compose scene
This commit is contained in:
parent
9c9edcb717
commit
fc3750c377
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// View.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by MainasuK on 2022/11/8.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
public func badgeView<Content>(_ content: Content) -> some View where Content: View {
|
||||||
|
overlay(
|
||||||
|
ZStack {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.alignmentGuide(.top) { $0.height / 2 }
|
||||||
|
.alignmentGuide(.trailing) { $0.width / 2 }
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,18 +13,17 @@ import AVKit
|
||||||
|
|
||||||
public struct AttachmentView: View {
|
public struct AttachmentView: View {
|
||||||
|
|
||||||
static let size = CGSize(width: 56, height: 56)
|
|
||||||
static let cornerRadius: CGFloat = 8
|
|
||||||
|
|
||||||
@ObservedObject var viewModel: AttachmentViewModel
|
@ObservedObject var viewModel: AttachmentViewModel
|
||||||
|
|
||||||
let action: (Action) -> Void
|
let action: (Action) -> Void
|
||||||
|
|
||||||
@State var isCaptionEditorPresented = false
|
|
||||||
@State var caption = ""
|
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Text("Hello")
|
ZStack {
|
||||||
|
let image = viewModel.thumbnail ?? .placeholder(color: .secondarySystemFill)
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
}
|
||||||
// Menu {
|
// Menu {
|
||||||
// menu
|
// menu
|
||||||
// } label: {
|
// } label: {
|
||||||
|
|
|
@ -22,6 +22,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
||||||
var observations = Set<NSKeyValueObservation>()
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
|
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()
|
||||||
|
@ -33,13 +34,19 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
||||||
@Published var error: Error?
|
@Published var error: Error?
|
||||||
let progress = Progress() // upload progress
|
let progress = Progress() // upload progress
|
||||||
|
|
||||||
public init(input: Input) {
|
public init(
|
||||||
|
authContext: AuthContext,
|
||||||
|
input: Input
|
||||||
|
) {
|
||||||
|
self.authContext = authContext
|
||||||
self.input = input
|
self.input = input
|
||||||
super.init()
|
super.init()
|
||||||
// end init
|
// end init
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
load(input: input)
|
Task {
|
||||||
|
await load(input: input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$output
|
$output
|
||||||
|
@ -53,6 +60,7 @@ final public class AttachmentViewModel: NSObject, ObservableObject, Identifiable
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: &$thumbnail)
|
.assign(to: &$thumbnail)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,13 +94,6 @@ extension AttachmentViewModel {
|
||||||
case png
|
case png
|
||||||
case jpg
|
case jpg
|
||||||
}
|
}
|
||||||
|
|
||||||
public var twitterMediaCategory: TwitterMediaCategory {
|
|
||||||
switch self {
|
|
||||||
case .image: return .image
|
|
||||||
case .video: return .amplifyVideo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SizeLimit {
|
public struct SizeLimit {
|
||||||
|
@ -116,17 +117,12 @@ extension AttachmentViewModel {
|
||||||
case attachmentTooLarge
|
case attachmentTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TwitterMediaCategory: String {
|
|
||||||
case image = "TWEET_IMAGE"
|
|
||||||
case GIF = "TWEET_GIF"
|
|
||||||
case video = "TWEET_VIDEO"
|
|
||||||
case amplifyVideo = "AMPLIFY_VIDEO"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentViewModel {
|
extension AttachmentViewModel {
|
||||||
|
|
||||||
private func load(input: Input) {
|
@MainActor
|
||||||
|
private func load(input: Input) async {
|
||||||
switch input {
|
switch input {
|
||||||
case .image(let image):
|
case .image(let image):
|
||||||
guard let data = image.pngData() else {
|
guard let data = image.pngData() else {
|
||||||
|
@ -135,32 +131,26 @@ extension AttachmentViewModel {
|
||||||
}
|
}
|
||||||
output = .image(data, imageKind: .png)
|
output = .image(data, imageKind: .png)
|
||||||
case .url(let url):
|
case .url(let url):
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
do {
|
||||||
let output = try await AttachmentViewModel.load(url: url)
|
let output = try await AttachmentViewModel.load(url: url)
|
||||||
self.output = output
|
self.output = output
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
} // end Task
|
|
||||||
case .pickerResult(let pickerResult):
|
case .pickerResult(let pickerResult):
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
do {
|
||||||
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
let output = try await AttachmentViewModel.load(itemProvider: pickerResult.itemProvider)
|
||||||
self.output = output
|
self.output = output
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
} // end Task
|
|
||||||
case .itemProvider(let itemProvider):
|
case .itemProvider(let itemProvider):
|
||||||
Task { @MainActor in
|
|
||||||
do {
|
do {
|
||||||
let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
|
let output = try await AttachmentViewModel.load(itemProvider: itemProvider)
|
||||||
self.output = output
|
self.output = output
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
} // end Task
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -325,16 +325,10 @@ extension ComposeContentViewController: PHPickerViewControllerDelegate {
|
||||||
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
picker.dismiss(animated: true, completion: nil)
|
picker.dismiss(animated: true, completion: nil)
|
||||||
|
|
||||||
// TODO:
|
let attachmentViewModels: [AttachmentViewModel] = results.map { result in
|
||||||
// let attachmentServices: [MastodonAttachmentService] = results.map { result in
|
AttachmentViewModel(authContext: viewModel.authContext, input: .pickerResult(result))
|
||||||
// let service = MastodonAttachmentService(
|
}
|
||||||
// context: context,
|
viewModel.attachmentViewModels += attachmentViewModels
|
||||||
// pickerResult: result,
|
|
||||||
// initialAuthenticationBox: viewModel.authenticationBox
|
|
||||||
// )
|
|
||||||
// return service
|
|
||||||
// }
|
|
||||||
// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import MastodonAsset
|
||||||
import MastodonCore
|
import MastodonCore
|
||||||
import MastodonLocalization
|
import MastodonLocalization
|
||||||
import Stripes
|
import Stripes
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
public struct ComposeContentView: View {
|
public struct ComposeContentView: View {
|
||||||
|
|
||||||
|
@ -108,6 +109,9 @@ public struct ComposeContentView: View {
|
||||||
// poll
|
// poll
|
||||||
pollView
|
pollView
|
||||||
.padding(.horizontal, ComposeContentView.margin)
|
.padding(.horizontal, ComposeContentView.margin)
|
||||||
|
// media
|
||||||
|
mediaView
|
||||||
|
.padding(.horizontal, ComposeContentView.margin)
|
||||||
}
|
}
|
||||||
.background(
|
.background(
|
||||||
GeometryReader { proxy in
|
GeometryReader { proxy in
|
||||||
|
@ -194,6 +198,29 @@ extension ComposeContentView {
|
||||||
}
|
}
|
||||||
} // end VStack
|
} // end VStack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - media
|
||||||
|
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) { action in
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clipShape(Rectangle())
|
||||||
|
.badgeView(
|
||||||
|
Button {
|
||||||
|
viewModel.attachmentViewModels.removeAll(where: { $0 === attachmentViewModel })
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "minus.circle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} // end ForEach
|
||||||
|
} // end VStack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//private struct ScrollOffsetPreferenceKey: PreferenceKey {
|
//private struct ScrollOffsetPreferenceKey: PreferenceKey {
|
||||||
|
|
|
@ -77,19 +77,6 @@ struct StatusAttachmentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
|
||||||
func badgeView<Content>(_ content: Content) -> some View where Content: View {
|
|
||||||
overlay(
|
|
||||||
ZStack {
|
|
||||||
content
|
|
||||||
}
|
|
||||||
.alignmentGuide(.top) { $0.height / 2 }
|
|
||||||
.alignmentGuide(.trailing) { $0.width / 2 }
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ref: https://stackoverflow.com/a/57715771/3797903
|
/// ref: https://stackoverflow.com/a/57715771/3797903
|
||||||
extension View {
|
extension View {
|
||||||
func placeholder<Content: View>(
|
func placeholder<Content: View>(
|
||||||
|
|
Loading…
Reference in New Issue