Tusker/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeToolbarView.swift

243 lines
7.6 KiB
Swift

//
// ComposeToolbarView.swift
// ComposeUI
//
// Created by Shadowfacts on 8/10/24.
//
import SwiftUI
import TuskerComponents
import InstanceFeatures
import Pachyderm
import TuskerPreferences
struct ComposeToolbarView: View {
static let height: CGFloat = 44
@ObservedObject var draft: Draft
let mastodonController: any ComposeMastodonContext
@FocusState.Binding var focusedField: FocusableField?
var body: some View {
#if os(visionOS)
buttons
#else
ToolbarScrollView {
buttons
.padding(.horizontal, 16)
}
.frame(height: Self.height)
.background(.regularMaterial, ignoresSafeAreaEdges: [.bottom, .leading, .trailing])
.overlay(alignment: .top) {
Divider()
.ignoresSafeArea(edges: [.leading, .trailing])
}
#endif
}
private var buttons: some View {
HStack(spacing: 0) {
ContentWarningButton(enabled: $draft.contentWarningEnabled, focusedField: $focusedField)
VisibilityButton(draft: draft, instanceFeatures: mastodonController.instanceFeatures)
LocalOnlyButton(enabled: $draft.localOnly, mastodonController: mastodonController)
InsertEmojiButton()
FormatButtons()
Spacer()
}
}
}
#if !os(visionOS)
private struct ToolbarScrollView<Content: View>: View {
@ViewBuilder let content: Content
@State private var minWidth: CGFloat?
@State private var realWidth: CGFloat?
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
content
.frame(minWidth: minWidth)
.background {
GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) {
realWidth = $0
}
}
}
}
.scrollDisabledIfAvailable(realWidth ?? 0 <= minWidth ?? 0)
.frame(maxWidth: .infinity)
.background {
GeometryReader { proxy in
Color.clear
.preference(key: ToolbarWidthPrefKey.self, value: proxy.size.width)
.onPreferenceChange(ToolbarWidthPrefKey.self) {
minWidth = $0
}
}
}
}
}
#endif
private struct ToolbarWidthPrefKey: SwiftUI.PreferenceKey {
static var defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
value = value ?? nextValue()
}
}
private struct ContentWarningButton: View {
@Binding var enabled: Bool
@FocusState.Binding var focusedField: FocusableField?
var body: some View {
Button("CW", action: toggleContentWarning)
.accessibilityLabel(enabled ? "Remove content warning" : "Add content warning")
.padding(5)
.hoverEffect()
}
private func toggleContentWarning() {
enabled.toggle()
if focusedField != nil {
if enabled {
focusedField = .contentWarning
} else if focusedField == .contentWarning {
focusedField = .body
}
}
}
}
private struct VisibilityButton: View {
@ObservedObject var draft: Draft
@ObservedObject var instanceFeatures: InstanceFeatures
private var visibilityBinding: Binding<Pachyderm.Visibility> {
// On instances that conflate visibliity and local only, we still show two separate controls but don't allow
// changing the visibility when local-only.
if draft.localOnly,
instanceFeatures.localOnlyPostsVisibility {
return .constant(.public)
} else {
return $draft.visibility
}
}
private var visibilityOptions: [MenuPicker<Pachyderm.Visibility>.Option] {
let visibilities: [Pachyderm.Visibility]
if !instanceFeatures.composeDirectStatuses {
visibilities = [.public, .unlisted, .private]
} else {
visibilities = Pachyderm.Visibility.allCases
}
return visibilities.map { vis in
.init(value: vis, title: vis.displayName, subtitle: vis.subtitle, image: UIImage(systemName: vis.unfilledImageName), accessibilityLabel: "Visibility: \(vis.displayName)")
}
}
var body: some View {
MenuPicker(selection: visibilityBinding, options: visibilityOptions, buttonStyle: .iconOnly)
.disabled(draft.editedStatusID != nil)
.disabled(instanceFeatures.localOnlyPostsVisibility && draft.localOnly)
}
}
private struct LocalOnlyButton: View {
@Binding var enabled: Bool
var mastodonController: any ComposeMastodonContext
@ObservedObject private var instanceFeatures: InstanceFeatures
init(enabled: Binding<Bool>, mastodonController: any ComposeMastodonContext) {
self._enabled = enabled
self.mastodonController = mastodonController
self.instanceFeatures = mastodonController.instanceFeatures
}
private var options: [MenuPicker<Bool>.Option] {
let domain = mastodonController.accountInfo!.instanceURL.host!
return [
.init(value: true, title: "Local-only", subtitle: "Only \(domain)", image: UIImage(named: "link.broken")),
.init(value: false, title: "Federated", image: UIImage(systemName: "link")),
]
}
var body: some View {
if mastodonController.instanceFeatures.localOnlyPosts {
MenuPicker(selection: $enabled, options: options, buttonStyle: .iconOnly)
}
}
}
private struct InsertEmojiButton: View {
@FocusedValue(\.composeInput) private var input
@ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22
var body: some View {
if input??.toolbarElements.contains(.emojiPicker) == true {
Button(action: beginAutocompletingEmoji) {
Label("Insert custom emoji", systemImage: "face.smiling")
}
.labelStyle(.iconOnly)
.font(.system(size: imageSize))
.padding(5)
.hoverEffect()
.transition(.opacity.animation(.linear(duration: 0.2)))
}
}
private func beginAutocompletingEmoji() {
input??.beginAutocompletingEmoji()
}
}
private struct FormatButtons: View {
@FocusedValue(\.composeInput) private var input
@PreferenceObserving(\.$statusContentType) private var contentType
var body: some View {
if let input = input.flatMap(\.self),
input.toolbarElements.contains(.formattingButtons),
contentType != .plain {
Spacer()
ForEach(StatusFormat.allCases) { format in
FormatButton(format: format, input: input)
}
}
}
}
private struct FormatButton: View {
let format: StatusFormat
let input: any ComposeInput
@ScaledMetric(relativeTo: .body) private var imageSize: CGFloat = 22
var body: some View {
Button(action: applyFormat) {
Image(systemName: format.imageName)
.font(.system(size: imageSize))
}
.accessibilityLabel(format.accessibilityLabel)
.padding(5)
.hoverEffect()
.transition(.opacity.animation(.linear(duration: 0.2)))
}
private func applyFormat() {
input.applyFormat(format)
}
}
//#Preview {
// ComposeToolbarView()
//}