2022-12-23 09:41:55 +00:00
|
|
|
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
|
2022-12-23 09:41:55 +00:00
|
|
|
|
|
|
|
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
|
2022-12-28 07:06:46 +00:00
|
|
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
2022-12-25 05:55:33 +00:00
|
|
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
2022-12-23 09:41:55 +00:00
|
|
|
@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))
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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-23 09:41:55 +00:00
|
|
|
}
|
2022-12-29 09:39:34 +00:00
|
|
|
.background(theme.primaryBackgroundColor)
|
2022-12-26 07:24:55 +00:00
|
|
|
.navigationTitle(viewModel.mode.title)
|
2022-12-23 09:41:55 +00:00
|
|
|
.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
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
} label: {
|
2022-12-25 16:46:51 +00:00
|
|
|
if viewModel.isPosting {
|
|
|
|
ProgressView()
|
|
|
|
} else {
|
|
|
|
Text("Post")
|
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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 07:06:46 +00:00
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2022-12-28 07:06:46 +00:00
|
|
|
visibilityMenu
|
2022-12-27 18:10:31 +00:00
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
2022-12-28 07:06:46 +00:00
|
|
|
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
|
|
|
|
2022-12-28 07:06:46 +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
|
|
|
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|