mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-06-01 19:44:09 +00:00
250 lines
7.3 KiB
Swift
250 lines
7.3 KiB
Swift
import AVKit
|
|
import DesignSystem
|
|
import Env
|
|
import MediaUI
|
|
import Models
|
|
import NukeUI
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
struct StatusEditorMediaView: View {
|
|
@Environment(Theme.self) private var theme
|
|
@Environment(CurrentInstance.self) private var currentInstance
|
|
var viewModel: StatusEditorViewModel
|
|
@Binding var editingMediaContainer: StatusEditorMediaContainer?
|
|
|
|
@State private var isErrorDisplayed: Bool = false
|
|
|
|
@Namespace var mediaSpace
|
|
@State private var scrollID: String?
|
|
|
|
var body: some View {
|
|
ScrollView(.horizontal, showsIndicators: showsScrollIndicators) {
|
|
switch count {
|
|
case 1: mediaLayout
|
|
case 2: mediaLayout
|
|
case 3: mediaLayout
|
|
case 4: mediaLayout
|
|
default: mediaLayout
|
|
}
|
|
}
|
|
.scrollPosition(id: $scrollID, anchor: .trailing)
|
|
.padding(.horizontal, .layoutPadding)
|
|
.frame(height: count > 0 ? containerHeight : 0)
|
|
.animation(.spring(duration: 0.3), value: count)
|
|
.onChange(of: count) { oldValue, newValue in
|
|
if oldValue < newValue {
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(0.5))
|
|
withAnimation(.bouncy(duration: 0.5)) {
|
|
scrollID = containers.last?.id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var count: Int { viewModel.mediaContainers.count }
|
|
private var containers: [StatusEditorMediaContainer] { viewModel.mediaContainers }
|
|
private let containerHeight: CGFloat = 300
|
|
private var containerWidth: CGFloat { containerHeight / 1.5 }
|
|
|
|
#if targetEnvironment(macCatalyst)
|
|
private var showsScrollIndicators: Bool { count > 1 }
|
|
private var scrollBottomPadding: CGFloat?
|
|
#else
|
|
private var showsScrollIndicators: Bool = false
|
|
private var scrollBottomPadding: CGFloat? = 0
|
|
#endif
|
|
|
|
init(viewModel: StatusEditorViewModel, editingMediaContainer: Binding<StatusEditorMediaContainer?>) {
|
|
self.viewModel = viewModel
|
|
_editingMediaContainer = editingMediaContainer
|
|
}
|
|
|
|
private func pixel(at index: Int) -> some View {
|
|
Rectangle().frame(width: 0, height: 0)
|
|
.matchedGeometryEffect(id: index, in: mediaSpace, anchor: .leading)
|
|
}
|
|
|
|
private var mediaLayout: some View {
|
|
HStack(alignment: .center, spacing: count > 1 ? 8 : 0) {
|
|
if count > 0 {
|
|
if count == 1 {
|
|
makeMediaItem(at: 0)
|
|
.containerRelativeFrame(.horizontal, alignment: .leading)
|
|
} else {
|
|
makeMediaItem(at: 0)
|
|
}
|
|
} else { pixel(at: 0) }
|
|
if count > 1 { makeMediaItem(at: 1) } else { pixel(at: 1) }
|
|
if count > 2 { makeMediaItem(at: 2) } else { pixel(at: 2) }
|
|
if count > 3 { makeMediaItem(at: 3) } else { pixel(at: 3) }
|
|
}
|
|
.padding(.bottom, scrollBottomPadding)
|
|
.scrollTargetLayout()
|
|
}
|
|
|
|
private func makeMediaItem(at index: Int) -> some View {
|
|
let container = viewModel.mediaContainers[index]
|
|
|
|
return Menu {
|
|
makeImageMenu(container: container)
|
|
} label: {
|
|
RoundedRectangle(cornerRadius: 8).fill(.clear)
|
|
.overlay {
|
|
if let attachement = container.mediaAttachment {
|
|
makeRemoteMediaView(mediaAttachement: attachement)
|
|
} else if container.image != nil {
|
|
makeLocalImageView(container: container)
|
|
} else if let error = container.error as? ServerError {
|
|
makeErrorView(error: error)
|
|
} else {
|
|
placeholderView
|
|
}
|
|
}
|
|
}
|
|
.overlay(alignment: .bottomTrailing) {
|
|
makeAltMarker(container: container)
|
|
}
|
|
.overlay(alignment: .topTrailing) {
|
|
makeDiscardMarker(container: container)
|
|
}
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
.frame(minWidth: count == 1 ? nil : containerWidth, maxWidth: 600)
|
|
.id(container.id)
|
|
.matchedGeometryEffect(id: container.id, in: mediaSpace, anchor: .leading)
|
|
.matchedGeometryEffect(id: index, in: mediaSpace, anchor: .leading)
|
|
}
|
|
|
|
private func makeLocalImageView(container: StatusEditorMediaContainer) -> some View {
|
|
ZStack(alignment: .center) {
|
|
Image(uiImage: container.image!)
|
|
.resizable()
|
|
.blur(radius: container.mediaAttachment == nil ? 20 : 0)
|
|
.scaledToFill()
|
|
.cornerRadius(8)
|
|
if container.error != nil {
|
|
Text("status.editor.error.upload")
|
|
} else if container.mediaAttachment == nil {
|
|
ProgressView()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func makeRemoteMediaView(mediaAttachement: MediaAttachment) -> some View {
|
|
ZStack(alignment: .center) {
|
|
switch mediaAttachement.supportedType {
|
|
case .gifv, .video, .audio:
|
|
if let url = mediaAttachement.url {
|
|
MediaUIAttachmentVideoView(viewModel: .init(url: url, forceAutoPlay: true))
|
|
} else {
|
|
placeholderView
|
|
}
|
|
case .image:
|
|
if let url = mediaAttachement.url ?? mediaAttachement.previewUrl {
|
|
LazyImage(url: url) { state in
|
|
if let image = state.image {
|
|
image
|
|
.resizable()
|
|
.scaledToFill()
|
|
} else {
|
|
placeholderView
|
|
}
|
|
}
|
|
}
|
|
case .none:
|
|
EmptyView()
|
|
}
|
|
}
|
|
.cornerRadius(8)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func makeImageMenu(container: StatusEditorMediaContainer) -> some View {
|
|
if container.mediaAttachment?.url != nil {
|
|
if currentInstance.isEditAltTextSupported || !viewModel.mode.isEditing {
|
|
Button {
|
|
editingMediaContainer = container
|
|
} label: {
|
|
Label(container.mediaAttachment?.description?.isEmpty == false ?
|
|
"status.editor.description.edit" : "status.editor.description.add",
|
|
systemImage: "pencil.line")
|
|
}
|
|
}
|
|
} else if container.error != nil {
|
|
Button {
|
|
isErrorDisplayed = true
|
|
} label: {
|
|
Label("action.view.error", systemImage: "exclamationmark.triangle")
|
|
}
|
|
}
|
|
|
|
Button(role: .destructive) {
|
|
deleteAction(container: container)
|
|
} label: {
|
|
Label("action.delete", systemImage: "trash")
|
|
}
|
|
}
|
|
|
|
private func makeErrorView(error: ServerError) -> some View {
|
|
ZStack {
|
|
placeholderView
|
|
Text("status.editor.error.upload")
|
|
}
|
|
.alert("alert.error", isPresented: $isErrorDisplayed) {
|
|
Button("Ok", action: {})
|
|
} message: {
|
|
Text(error.error ?? "")
|
|
}
|
|
}
|
|
|
|
private func makeAltMarker(container: StatusEditorMediaContainer) -> some View {
|
|
Button {
|
|
editingMediaContainer = container
|
|
} label: {
|
|
Text("status.image.alt-text.abbreviation")
|
|
.font(.caption2)
|
|
}
|
|
.padding(8)
|
|
.background(.thinMaterial)
|
|
.cornerRadius(8)
|
|
.padding(4)
|
|
}
|
|
|
|
private func makeDiscardMarker(container: StatusEditorMediaContainer) -> some View {
|
|
Button(role: .destructive) {
|
|
deleteAction(container: container)
|
|
} label: {
|
|
Image(systemName: "xmark")
|
|
.font(.caption2)
|
|
.foregroundStyle(.tint)
|
|
.padding(8)
|
|
.background(Circle().fill(.thinMaterial))
|
|
}
|
|
.padding(4)
|
|
}
|
|
|
|
private func deleteAction(container: StatusEditorMediaContainer) {
|
|
viewModel.mediaPickers.removeAll(where: {
|
|
if let id = $0.itemIdentifier {
|
|
return id == container.id
|
|
}
|
|
return false
|
|
})
|
|
viewModel.mediaContainers.removeAll {
|
|
$0.id == container.id
|
|
}
|
|
}
|
|
|
|
private var placeholderView: some View {
|
|
ZStack(alignment: .center) {
|
|
Rectangle()
|
|
.foregroundColor(theme.secondaryBackgroundColor)
|
|
.accessibilityHidden(true)
|
|
ProgressView()
|
|
}
|
|
.cornerRadius(8)
|
|
}
|
|
}
|