mastodon-ios/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/Toolbar/ComposeContentToolbarView.s...

249 lines
13 KiB
Swift
Raw Normal View History

//
// ComposeContentToolbarView.swift
//
//
// Created by MainasuK on 22/10/18.
//
2022-10-21 13:12:44 +02:00
import os.log
import SwiftUI
import MastodonAsset
2022-10-21 13:12:44 +02:00
import MastodonLocalization
import MastodonSDK
protocol ComposeContentToolbarViewDelegate: AnyObject {
func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, toolbarItemDidPressed action: ComposeContentToolbarView.ViewModel.Action)
func composeContentToolbarView(_ viewModel: ComposeContentToolbarView.ViewModel, attachmentMenuDidPressed action: ComposeContentToolbarView.ViewModel.AttachmentAction)
}
struct ComposeContentToolbarView: View {
static var toolbarHeight: CGFloat { 48 }
@ObservedObject var viewModel: ViewModel
let isZoomed = (UIScreen.main.scale != UIScreen.main.nativeScale)
@State private var showingLanguagePicker = false
@State private var didChangeLanguage = false
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
var body: some View {
HStack(spacing: .zero) {
ScrollView(isZoomed ? .horizontal : []) {
2023-04-13 22:12:51 +02:00
HStack(spacing: .zero) {
ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in
switch action {
case .attachment:
Menu {
ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in
Button {
viewModel.delegate?.composeContentToolbarView(viewModel, attachmentMenuDidPressed: attachmentAction)
} label: {
Label {
Text(attachmentAction.title)
} icon: {
Image(uiImage: attachmentAction.image)
}
}
}
} label: {
label(for: action)
.opacity(viewModel.isAttachmentButtonEnabled ? 1.0 : 0.5)
2022-10-21 13:12:44 +02:00
}
2023-04-13 22:12:51 +02:00
.disabled(!viewModel.isAttachmentButtonEnabled)
.frame(width: 48, height: 48)
case .visibility:
Menu {
Picker(selection: $viewModel.visibility) {
ForEach(viewModel.allVisibilities, id: \.self) { visibility in
Label {
Text(visibility.title)
} icon: {
Image(uiImage: visibility.image)
}
}
} label: {
Text(viewModel.visibility.title)
}
2023-04-13 22:12:51 +02:00
} label: {
label(for: viewModel.visibility.image)
.accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
.opacity(viewModel.isVisibilityButtonEnabled ? 1.0 : 0.5)
}
2023-04-13 22:12:51 +02:00
.disabled(!viewModel.isVisibilityButtonEnabled)
.frame(width: 48, height: 48)
case .poll:
Button {
viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action)
} label: {
label(for: action)
.opacity(viewModel.isPollButtonEnabled ? 1.0 : 0.5)
}
.disabled(!viewModel.isPollButtonEnabled)
.frame(width: 48, height: 48)
case .language:
Menu {
Section {} // workaround a bug where the Suggested section doesnt appear
if !viewModel.suggestedLanguages.isEmpty {
Section(L10n.Scene.Compose.Language.suggested) {
ForEach(viewModel.suggestedLanguages.compactMap(Language.init(id:))) { lang in
Toggle(isOn: languageBinding(for: lang.id)) {
Text(lang.label)
}
}
}
}
let recent = viewModel.recentLanguages.filter { !viewModel.suggestedLanguages.contains($0) }
if !recent.isEmpty {
Section(L10n.Scene.Compose.Language.recent) {
ForEach(recent.compactMap(Language.init(id:))) { lang in
Toggle(isOn: languageBinding(for: lang.id)) {
Text(lang.label)
}
}
}
}
if !(recent + viewModel.suggestedLanguages).contains(viewModel.language) {
Toggle(isOn: languageBinding(for: viewModel.language)) {
Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)"))
}
}
Button(L10n.Scene.Compose.Language.other) {
showingLanguagePicker = true
}
2023-04-13 22:12:51 +02:00
} label: {
let font: SwiftUI.Font = {
if #available(iOS 16, *) {
return .system(size: 11, weight: .semibold).width(viewModel.language.count == 3 ? .compressed : .standard)
} else {
return .system(size: 11, weight: .semibold)
}
}()
Text(viewModel.language)
.font(font)
.textCase(.uppercase)
.padding(.horizontal, 4)
.minimumScaleFactor(0.5)
.frame(width: 24, height: 24, alignment: .center)
.overlay { RoundedRectangle(cornerRadius: 7).inset(by: 3).stroke(lineWidth: 1.5) }
.accessibilityLabel(L10n.Scene.Compose.Language.title)
.accessibilityValue(Text(Language(id: viewModel.language)?.label ?? AttributedString("\(viewModel.language)")))
.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color))
.overlay(alignment: .topTrailing) {
Group {
if let suggested = viewModel.highConfidenceSuggestedLanguage,
suggested != viewModel.language,
!didChangeLanguage {
Circle().fill(.blue)
.frame(width: 8, height: 8)
}
}
.transition(.opacity)
.animation(.default, value: [viewModel.highConfidenceSuggestedLanguage, viewModel.language])
}
// fixes weird appearance when drawing at low opacity (eg when pressed)
.drawingGroup()
}
2023-04-13 22:12:51 +02:00
.frame(width: 48, height: 48)
.popover(isPresented: $showingLanguagePicker) {
let picker = LanguagePicker { newLanguage in
viewModel.language = newLanguage
didChangeLanguage = true
showingLanguagePicker = false
}
if verticalSizeClass == .regular && horizontalSizeClass == .regular {
// explicitly size picker when its a popover
picker.frame(width: 400, height: 500)
} else {
picker
}
}
2023-04-13 22:12:51 +02:00
default:
Button {
viewModel.delegate?.composeContentToolbarView(viewModel, toolbarItemDidPressed: action)
} label: {
label(for: action)
}
.frame(width: 48, height: 48)
}
}
}
}
Spacer()
let count: Int = {
if viewModel.isContentWarningActive {
return viewModel.contentWeightedLength + viewModel.contentWarningWeightedLength
} else {
return viewModel.contentWeightedLength
}
}()
let remains = viewModel.maxTextInputLimit - count
let isOverflow = remains < 0
Text("\(remains)")
.foregroundColor(Color(isOverflow ? UIColor.systemRed : UIColor.secondaryLabel))
.font(.system(size: isOverflow ? 18 : 16, weight: isOverflow ? .medium : .regular))
.accessibilityLabel(L10n.A11y.Plural.Count.charactersLeft(remains))
2023-04-13 22:12:51 +02:00
}
.padding(.leading, 4) // 4 + 12 = 16
.padding(.trailing, 16)
.frame(height: ComposeContentToolbarView.toolbarHeight)
.background(Color(viewModel.backgroundColor))
.accessibilityElement(children: .contain)
2022-11-08 19:50:23 +01:00
.accessibilityLabel(L10n.Scene.Compose.Accessibility.postOptions)
}
}
extension ComposeContentToolbarView {
func label(for action: ComposeContentToolbarView.ViewModel.Action) -> some View {
2022-10-21 13:12:44 +02:00
Image(uiImage: viewModel.image(for: action))
.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color))
.frame(width: 24, height: 24, alignment: .center)
2022-11-07 02:49:12 +01:00
.accessibilityLabel(viewModel.label(for: action))
}
2022-10-21 13:12:44 +02:00
func label(for image: UIImage) -> some View {
Image(uiImage: image)
.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color))
.frame(width: 24, height: 24, alignment: .center)
}
private func languageBinding(for code: String) -> Binding<Bool> {
Binding {
code == viewModel.language
} set: { newValue in
if newValue {
viewModel.language = code
}
didChangeLanguage = true
}
}
2022-10-21 13:12:44 +02:00
}
extension Mastodon.Entity.Status.Visibility {
fileprivate var title: String {
switch self {
case .public: return L10n.Scene.Compose.Visibility.public
case .unlisted: return L10n.Scene.Compose.Visibility.unlisted
case .private: return L10n.Scene.Compose.Visibility.private
case .direct: return L10n.Scene.Compose.Visibility.direct
case ._other(let value): return value
}
}
fileprivate var image: UIImage {
switch self {
case .public: return Asset.Scene.Compose.earth.image.withRenderingMode(.alwaysTemplate)
case .unlisted: return Asset.Scene.Compose.people.image.withRenderingMode(.alwaysTemplate)
case .private: return Asset.Scene.Compose.peopleAdd.image.withRenderingMode(.alwaysTemplate)
case .direct: return Asset.Scene.Compose.mention.image.withRenderingMode(.alwaysTemplate)
case ._other: return Asset.Scene.Compose.more.image.withRenderingMode(.alwaysTemplate)
}
}
}