forked from shadowfacts/Tusker
243 lines
7.6 KiB
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()
|
|
//}
|