IceCubesApp/Packages/Status/Sources/Status/Editor/StatusEditorView.swift

261 lines
7 KiB
Swift
Raw Normal View History

import SwiftUI
2022-12-25 05:55:33 +00:00
import Accounts
import Env
import DesignSystem
import TextView
2022-12-25 07:17:16 +00:00
import Models
import Network
2022-12-25 18:15:35 +00:00
import PhotosUI
2022-12-27 15:16:25 +00:00
import NukeUI
public struct StatusEditorView: View {
2022-12-29 09:39:34 +00:00
@EnvironmentObject private var theme: Theme
2022-12-27 15:16:25 +00:00
@EnvironmentObject private var quicklook: QuickLook
2022-12-25 07:17:16 +00:00
@EnvironmentObject private var client: Client
@EnvironmentObject private var currentInstance: CurrentInstance
2022-12-25 05:55:33 +00:00
@EnvironmentObject private var currentAccount: CurrentAccount
@Environment(\.dismiss) private var dismiss
2022-12-25 07:17:16 +00:00
@StateObject private var viewModel: StatusEditorViewModel
2022-12-28 09:45:05 +00:00
@FocusState private var isSpoilerTextFocused: Bool
2022-12-25 05:55:33 +00:00
2022-12-26 07:24:55 +00:00
public init(mode: StatusEditorViewModel.Mode) {
_viewModel = StateObject(wrappedValue: .init(mode: mode))
}
public var body: some View {
NavigationStack {
2022-12-25 07:17:16 +00:00
ZStack(alignment: .bottom) {
2022-12-27 12:38:10 +00:00
ScrollView {
2022-12-27 18:10:31 +00:00
Divider()
2022-12-28 09:45:05 +00:00
spoilerTextView
2022-12-27 12:38:10 +00:00
VStack(spacing: 12) {
accountHeaderView
2022-12-27 18:10:31 +00:00
.padding(.horizontal, DS.Constants.layoutPadding)
TextView($viewModel.statusText, $viewModel.selectedRange)
2022-12-27 12:38:10 +00:00
.placeholder("What's on your mind")
2022-12-27 18:10:31 +00:00
.padding(.horizontal, DS.Constants.layoutPadding)
2022-12-27 12:38:10 +00:00
if let status = viewModel.embededStatus {
StatusEmbededView(status: status)
2022-12-27 18:10:31 +00:00
.padding(.horizontal, DS.Constants.layoutPadding)
2022-12-27 12:38:10 +00:00
}
mediasView
Spacer()
}
2022-12-27 18:10:31 +00:00
.padding(.top, 8)
2022-12-25 07:17:16 +00:00
}
accessoryView
}
.onAppear {
viewModel.client = client
2022-12-26 07:24:55 +00:00
viewModel.prepareStatusText()
2022-12-27 07:31:57 +00:00
if !client.isAuth {
dismiss()
}
}
2022-12-29 09:39:34 +00:00
.background(theme.primaryBackgroundColor)
2022-12-26 07:24:55 +00:00
.navigationTitle(viewModel.mode.title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
2022-12-25 07:17:16 +00:00
Task {
2022-12-25 16:46:51 +00:00
let status = await viewModel.postStatus()
if status != nil {
dismiss()
}
2022-12-25 07:17:16 +00:00
}
} label: {
2022-12-25 16:46:51 +00:00
if viewModel.isPosting {
ProgressView()
} else {
Text("Post")
}
}
}
ToolbarItem(placement: .navigationBarLeading) {
Button {
dismiss()
} label: {
Text("Cancel")
}
}
}
}
}
2022-12-25 05:55:33 +00:00
2022-12-28 09:45:05 +00:00
@ViewBuilder
private var spoilerTextView: some View {
if viewModel.spoilerOn {
VStack {
TextField("Spoiler Text", text: $viewModel.spoilerText)
.focused($isSpoilerTextFocused)
.padding(.horizontal, DS.Constants.layoutPadding)
}
.frame(height: 35)
2022-12-29 09:39:34 +00:00
.background(theme.tintColor.opacity(0.20))
2022-12-28 09:45:05 +00:00
.offset(y: -8)
}
}
2022-12-25 18:15:35 +00:00
@ViewBuilder
private var accountHeaderView: some View {
2022-12-25 05:55:33 +00:00
if let account = currentAccount.account {
HStack {
AvatarView(url: account.avatar, size: .status)
VStack(alignment: .leading, spacing: 0) {
account.displayNameWithEmojis
.font(.subheadline)
.fontWeight(.semibold)
Text("@\(account.acct)")
.font(.footnote)
.foregroundColor(.gray)
}
Spacer()
}
}
}
2022-12-25 07:17:16 +00:00
2022-12-25 18:15:35 +00:00
private var mediasView: some View {
2022-12-27 18:10:31 +00:00
ScrollView(.horizontal, showsIndicators: false) {
2022-12-27 15:16:25 +00:00
HStack(spacing: 8) {
2022-12-25 18:15:35 +00:00
ForEach(viewModel.mediasImages) { container in
2022-12-27 18:10:31 +00:00
if container.image != nil {
makeLocalImage(container: container)
2022-12-27 15:16:25 +00:00
} else if let url = container.mediaAttachement?.url {
ZStack(alignment: .topTrailing) {
makeLazyImage(url: url)
Button {
withAnimation {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
}
} label: {
Image(systemName: "xmark.circle")
}
.padding(8)
}
}
2022-12-25 18:15:35 +00:00
}
}
2022-12-27 18:10:31 +00:00
.padding(.horizontal, DS.Constants.layoutPadding)
2022-12-25 18:15:35 +00:00
}
}
2022-12-27 18:10:31 +00:00
private func makeLocalImage(container: StatusEditorViewModel.ImageContainer) -> some View {
2022-12-27 15:16:25 +00:00
ZStack(alignment: .center) {
2022-12-27 18:10:31 +00:00
Image(uiImage: container.image!)
2022-12-27 15:16:25 +00:00
.resizable()
.blur(radius: 20 )
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.cornerRadius(8)
2022-12-27 18:10:31 +00:00
if container.error != nil {
VStack {
Text("Error uploading")
Button {
withAnimation {
viewModel.mediasImages.removeAll(where: { $0.id == container.id })
}
} label: {
VStack {
Text("Delete")
}
}
.buttonStyle(.bordered)
Button {
Task {
await viewModel.upload(container: container)
}
} label: {
VStack {
Text("Retry")
}
}
.buttonStyle(.bordered)
}
} else {
ProgressView()
}
2022-12-27 15:16:25 +00:00
}
}
private func makeLazyImage(url: URL?) -> some View {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizingMode(.aspectFill)
.frame(width: 150, height: 150)
} else {
Rectangle()
.frame(width: 150, height: 150)
}
}
.frame(width: 150, height: 150)
.cornerRadius(8)
}
2022-12-25 07:17:16 +00:00
private var accessoryView: some View {
2022-12-27 18:10:31 +00:00
VStack(spacing: 0) {
Divider()
2022-12-28 09:45:05 +00:00
HStack(alignment: .center, spacing: 16) {
2022-12-27 18:10:31 +00:00
PhotosPicker(selection: $viewModel.selectedMedias,
matching: .images) {
Image(systemName: "photo.fill.on.rectangle.fill")
}
Button {
viewModel.insertStatusText(text: " @")
} label: {
Image(systemName: "at")
}
Button {
viewModel.insertStatusText(text: " #")
} label: {
Image(systemName: "number")
}
2022-12-28 09:45:05 +00:00
Button {
withAnimation {
viewModel.spoilerOn.toggle()
}
isSpoilerTextFocused.toggle()
} label: {
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill": "exclamationmark.triangle")
}
visibilityMenu
2022-12-27 18:10:31 +00:00
Spacer()
characterCountView
2022-12-25 07:17:16 +00:00
}
2022-12-28 09:45:05 +00:00
.frame(height: 20)
2022-12-27 18:10:31 +00:00
.padding(.horizontal, DS.Constants.layoutPadding)
.padding(.vertical, 12)
.background(.ultraThinMaterial)
2022-12-25 07:17:16 +00:00
}
}
2022-12-27 18:10:31 +00:00
private var characterCountView: some View {
Text("\((currentInstance.instance?.configuration.statuses.maxCharacters ?? 500) - viewModel.statusText.string.utf16.count)")
.foregroundColor(.gray)
.font(.callout)
}
2022-12-27 18:10:31 +00:00
private var visibilityMenu: some View {
Menu {
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
Button {
viewModel.visibility = visibility
} label: {
Label(visibility.title, systemImage: visibility.iconName)
}
}
} label: {
Image(systemName: viewModel.visibility.iconName)
}
}
2022-12-25 05:55:33 +00:00
}