IceCubesApp/Packages/StatusKit/Sources/StatusKit/Editor/Components/AccessoryView.swift
Thomas Ricouard e79ead5efe visionOS fix
2024-01-08 21:21:14 +01:00

313 lines
9.4 KiB
Swift

import DesignSystem
import Env
#if !os(visionOS) && !DEBUG
import GiphyUISDK
#endif
import Models
import NukeUI
import PhotosUI
import SwiftUI
extension StatusEditor {
@MainActor
struct AccessoryView: View {
@Environment(UserPreferences.self) private var preferences
@Environment(Theme.self) private var theme
@Environment(CurrentInstance.self) private var currentInstance
@Environment(\.colorScheme) private var colorScheme
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
let focusedSEVM: ViewModel
@Binding var followUpSEVMs: [ViewModel]
@State private var isCustomEmojisSheetDisplay: Bool = false
@State private var isLoadingAIRequest: Bool = false
@State private var isPhotosPickerPresented: Bool = false
@State private var isFileImporterPresented: Bool = false
@State private var isCameraPickerPresented: Bool = false
@State private var isGIFPickerPresented: Bool = false
var body: some View {
@Bindable var viewModel = focusedSEVM
VStack(spacing: 0) {
#if os(visionOS)
HStack {
contentView
}
.frame(height: 24)
.padding(16)
.background(.thinMaterial)
.cornerRadius(8)
#else
Divider()
HStack {
contentView
}
.frame(height: 20)
.padding(.vertical, 12)
.background(.thinMaterial)
#endif
}
}
@ViewBuilder
private var contentView: some View {
#if os(visionOS)
HStack(spacing: 8) {
actionsView
}
#else
HStack(alignment: .center, spacing: 16) {
actionsView
}
.padding(.horizontal, .layoutPadding)
#endif
}
@ViewBuilder
private var actionsView: some View {
@Bindable var viewModel = focusedSEVM
Menu {
Button {
isPhotosPickerPresented = true
} label: {
Label("status.editor.photo-library", systemImage: "photo")
}
#if !targetEnvironment(macCatalyst)
Button {
isCameraPickerPresented = true
} label: {
Label("status.editor.camera-picker", systemImage: "camera")
}
#endif
Button {
isFileImporterPresented = true
} label: {
Label("status.editor.browse-file", systemImage: "folder")
}
#if !os(visionOS)
Button {
isGIFPickerPresented = true
} label: {
Label("GIPHY", systemImage: "party.popper")
}
#endif
} label: {
if viewModel.isMediasLoading {
ProgressView()
} else {
Image(systemName: "photo.on.rectangle.angled")
}
}
.photosPicker(isPresented: $isPhotosPickerPresented,
selection: $viewModel.mediaPickers,
maxSelectionCount: 4,
matching: .any(of: [.images, .videos]),
photoLibrary: .shared())
.fileImporter(isPresented: $isFileImporterPresented,
allowedContentTypes: [.image, .video],
allowsMultipleSelection: true)
{ result in
if let urls = try? result.get() {
viewModel.processURLs(urls: urls)
}
}
.fullScreenCover(isPresented: $isCameraPickerPresented, content: {
CameraPickerView(selectedImage: .init(get: {
nil
}, set: { image in
if let image {
viewModel.processCameraPhoto(image: image)
}
}))
.background(.black)
})
.sheet(isPresented: $isGIFPickerPresented, content: {
#if !os(visionOS) && !DEBUG
#if targetEnvironment(macCatalyst)
NavigationStack {
giphyView
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
isGIFPickerPresented = false
} label: {
Image(systemName: "xmark.circle")
}
}
}
}
.presentationDetents([.medium, .large])
#else
giphyView
.presentationDetents([.medium, .large])
#endif
#else
EmptyView()
#endif
})
.accessibilityLabel("accessibility.editor.button.attach-photo")
.disabled(viewModel.showPoll)
Button {
// all SEVM have the same visibility value
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
} label: {
Image(systemName: "arrowshape.turn.up.left.circle.fill")
}
.disabled(!canAddNewSEVM)
if !viewModel.customEmojiContainer.isEmpty {
Button {
isCustomEmojisSheetDisplay = true
} label: {
// This is a workaround for an apparent bug in the `face.smiling` SF Symbol.
// See https://github.com/Dimillian/IceCubesApp/issues/1193
let customEmojiSheetIconName = colorScheme == .light ? "face.smiling" : "face.smiling.inverse"
Image(systemName: customEmojiSheetIconName)
}
.accessibilityLabel("accessibility.editor.button.custom-emojis")
.popover(isPresented: $isCustomEmojisSheetDisplay) {
if UIDevice.current.userInterfaceIdiom == .phone {
customEmojisSheet
} else {
customEmojisSheet
.frame(width: 400, height: 500)
}
}
}
if preferences.isOpenAIEnabled {
AIMenu.disabled(!viewModel.canPost)
}
Spacer()
Button {
viewModel.insertStatusText(text: "@")
} label: {
Image(systemName: "at")
}
Button {
viewModel.insertStatusText(text: "#")
} label: {
Image(systemName: "number")
}
}
private var canAddNewSEVM: Bool {
guard followUpSEVMs.count < 5 else { return false }
if followUpSEVMs.isEmpty, // there is only mainSEVM on the editor
!focusedSEVM.statusText.string.isEmpty // focusedSEVM is also mainSEVM
{ return true }
if let lastSEVMs = followUpSEVMs.last,
!lastSEVMs.statusText.string.isEmpty
{ return true }
return false
}
#if !os(visionOS) && !DEBUG
@ViewBuilder
private var giphyView: some View {
@Bindable var viewModel = focusedSEVM
GifPickerView { url in
GPHCache.shared.downloadAssetData(url) { data, _ in
guard let data else { return }
viewModel.processGIFData(data: data)
}
isGIFPickerPresented = false
} onShouldDismissGifPicker: {
isGIFPickerPresented = false
}
}
#endif
private var AIMenu: some View {
Menu {
ForEach(AIPrompt.allCases, id: \.self) { prompt in
Button {
Task {
isLoadingAIRequest = true
await focusedSEVM.runOpenAI(prompt: prompt.toRequestPrompt(text: focusedSEVM.statusText.string))
isLoadingAIRequest = false
}
} label: {
prompt.label
}
}
if let backup = focusedSEVM.backupStatusText {
Button {
focusedSEVM.replaceTextWith(text: backup.string)
focusedSEVM.backupStatusText = nil
} label: {
Label("status.editor.restore-previous", systemImage: "arrow.uturn.right")
}
}
} label: {
if isLoadingAIRequest {
ProgressView()
} else {
Image(systemName: "faxmachine")
.accessibilityLabel("accessibility.editor.button.ai-prompt")
}
}
}
private var customEmojisSheet: some View {
NavigationStack {
ScrollView {
ForEach(focusedSEVM.customEmojiContainer) { container in
VStack(alignment: .leading) {
Text(container.categoryName)
.font(.scaledFootnote)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40))], spacing: 9) {
ForEach(container.emojis) { emoji in
LazyImage(url: emoji.url) { state in
if let image = state.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.accessibilityLabel(emoji.shortcode.replacingOccurrences(of: "_", with: " "))
.accessibilityAddTraits(.isButton)
} else if state.isLoading {
Rectangle()
.fill(Color.gray)
.frame(width: 40, height: 40)
.accessibility(hidden: true)
.shimmering()
}
}
.onTapGesture {
focusedSEVM.insertStatusText(text: " :\(emoji.shortcode): ")
}
}
}
}
.padding(.horizontal)
.padding(.bottom)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("action.cancel", action: { isCustomEmojisSheetDisplay = false })
}
}
.scrollContentBackground(.hidden)
.background(theme.primaryBackgroundColor)
.navigationTitle("status.editor.emojis.navigation-title")
.navigationBarTitleDisplayMode(.inline)
}
.presentationDetents([.medium])
}
}
}