Relayout media on status editor (#1728)

* relayout media display

* animate media layout

* fix layout
This commit is contained in:
Thai D. V 2023-12-09 16:59:10 +07:00 committed by GitHub
parent 52208ab20e
commit f3ef79b297
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 110 additions and 48 deletions

View File

@ -14,35 +14,108 @@ struct StatusEditorMediaView: View {
@State private var isErrorDisplayed: Bool = false @State private var isErrorDisplayed: Bool = false
@Namespace var mediaSpace
@State private var scrollID: String?
var body: some View { var body: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: showsScrollIndicators) {
HStack(spacing: 8) { switch count {
ForEach(viewModel.mediaContainers) { container in case 1: mediaLayout
Menu { case 2: mediaLayout
makeImageMenu(container: container) case 3: mediaLayout
} label: { case 4: mediaLayout
if let attachement = container.mediaAttachment { default: mediaLayout
makeLazyImage(mediaAttachement: attachement) }
} else if container.image != nil { }
makeLocalImage(container: container) .scrollPosition(id: $scrollID, anchor: .trailing)
} else if container.movieTransferable != nil || container.gifTransferable != nil { .padding(.horizontal, .layoutPadding)
makeVideoAttachement(container: container) .frame(height: count > 0 ? containerHeight : 0)
} else if let error = container.error as? ServerError { .animation(.spring(duration: 0.3), value: count)
makeErrorView(error: error) .onChange(of: count) { oldValue, newValue in
} if oldValue < newValue {
} Task {
.overlay(alignment: .bottomTrailing) { try? await Task.sleep(for: .seconds(0.5))
makeAltMarker(container: container) withAnimation(.bouncy(duration: 0.5)) {
} scrollID = containers.last?.id
.overlay(alignment: .topTrailing) {
makeDiscardMarker(container: container)
} }
} }
} }
.padding(.horizontal, .layoutPadding)
} }
} }
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? = nil
#else
private var showsScrollIndicators : Bool = false
private var scrollBottomPadding : CGFloat? = 0
#endif
init(viewModel: StatusEditorViewModel, editingContainer: Binding<StatusEditorMediaContainer?>) {
self.viewModel = viewModel
self._editingContainer = editingContainer
}
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 {
makeLazyImage(mediaAttachement: attachement)
} else if container.image != nil {
makeLocalImage(container: container)
} else if container.movieTransferable != nil || container.gifTransferable != nil {
makeVideoAttachement(container: container)
} else if let error = container.error as? ServerError {
makeErrorView(error: error)
}
}
}
.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 makeVideoAttachement(container: StatusEditorMediaContainer) -> some View { private func makeVideoAttachement(container: StatusEditorMediaContainer) -> some View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
placeholderView placeholderView
@ -51,7 +124,6 @@ struct StatusEditorMediaView: View {
} }
} }
.cornerRadius(8) .cornerRadius(8)
.frame(width: 150, height: 150)
} }
private func makeLocalImage(container: StatusEditorMediaContainer) -> some View { private func makeLocalImage(container: StatusEditorMediaContainer) -> some View {
@ -59,8 +131,7 @@ struct StatusEditorMediaView: View {
Image(uiImage: container.image!) Image(uiImage: container.image!)
.resizable() .resizable()
.blur(radius: container.mediaAttachment == nil ? 20 : 0) .blur(radius: container.mediaAttachment == nil ? 20 : 0)
.aspectRatio(contentMode: .fill) .scaledToFill()
.frame(width: 150, height: 150)
.cornerRadius(8) .cornerRadius(8)
if container.error != nil { if container.error != nil {
Text("status.editor.error.upload") Text("status.editor.error.upload")
@ -77,8 +148,7 @@ struct StatusEditorMediaView: View {
if let image = state.image { if let image = state.image {
image image
.resizable() .resizable()
.aspectRatio(contentMode: .fill) .scaledToFill()
.frame(width: 150, height: 150)
} else { } else {
placeholderView placeholderView
} }
@ -97,7 +167,6 @@ struct StatusEditorMediaView: View {
.tint(.white) .tint(.white)
} }
} }
.frame(width: 150, height: 150)
.cornerRadius(8) .cornerRadius(8)
} }
@ -122,15 +191,7 @@ struct StatusEditorMediaView: View {
} }
Button(role: .destructive) { Button(role: .destructive) {
withAnimation { deleteAction(container: container)
viewModel.mediaPickers.removeAll(where: {
if let id = $0.itemIdentifier {
return id == container.id
}
return false
})
}
} label: { } label: {
Label("action.delete", systemImage: "trash") Label("action.delete", systemImage: "trash")
} }
@ -155,7 +216,7 @@ struct StatusEditorMediaView: View {
Text("status.image.alt-text.abbreviation") Text("status.image.alt-text.abbreviation")
.font(.caption2) .font(.caption2)
} }
.padding(4) .padding(8)
.background(.thinMaterial) .background(.thinMaterial)
.cornerRadius(8) .cornerRadius(8)
.padding(4) .padding(4)
@ -163,28 +224,29 @@ struct StatusEditorMediaView: View {
private func makeDiscardMarker(container: StatusEditorMediaContainer) -> some View { private func makeDiscardMarker(container: StatusEditorMediaContainer) -> some View {
Button(role: .destructive) { Button(role: .destructive) {
withAnimation { deleteAction(container: container)
viewModel.mediaPickers.removeAll(where: {
if let id = $0.itemIdentifier {
return id == container.id
}
return false
})
}
} label: { } label: {
Image(systemName: "xmark") Image(systemName: "xmark")
.font(.caption2) .font(.caption2)
.foregroundStyle(.tint) .foregroundStyle(.tint)
.padding(4) .padding(8)
.background(Circle().fill(.thinMaterial)) .background(Circle().fill(.thinMaterial))
} }
.padding(4) .padding(4)
} }
private func deleteAction(container: StatusEditorMediaContainer) {
viewModel.mediaPickers.removeAll(where: {
if let id = $0.itemIdentifier {
return id == container.id
}
return false
})
}
private var placeholderView: some View { private var placeholderView: some View {
Rectangle() Rectangle()
.foregroundColor(theme.secondaryBackgroundColor) .foregroundColor(theme.secondaryBackgroundColor)
.frame(width: 150, height: 150)
.accessibilityHidden(true) .accessibilityHidden(true)
} }
} }