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

247 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ComposeContentToolbarView.swift
//
//
// Created by MainasuK on 22/10/18.
//
import os.log
import SwiftUI
import MastodonAsset
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 {
let logger = Logger(subsystem: "ComposeContentToolbarView", category: "View")
static var toolbarHeight: CGFloat { 48 }
@ObservedObject var viewModel: ViewModel
@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) {
ForEach(ComposeContentToolbarView.ViewModel.Action.allCases, id: \.self) { action in
switch action {
case .attachment:
Menu {
ForEach(ComposeContentToolbarView.ViewModel.AttachmentAction.allCases, id: \.self) { attachmentAction in
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public), \(attachmentAction.title)")
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)
}
.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)
}
} label: {
label(for: viewModel.visibility.image)
.accessibilityLabel(L10n.Scene.Compose.Keyboard.selectVisibilityEntry(viewModel.visibility.title))
.opacity(viewModel.isVisibilityButtonEnabled ? 1.0 : 0.5)
}
.disabled(!viewModel.isVisibilityButtonEnabled)
.frame(width: 48, height: 48)
case .poll:
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")
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
}
} 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()
}
.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
}
}
default:
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(String(describing: action))")
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))
}
.padding(.leading, 4) // 4 + 12 = 16
.padding(.trailing, 16)
.frame(height: ComposeContentToolbarView.toolbarHeight)
.background(Color(viewModel.backgroundColor))
.accessibilityElement(children: .contain)
.accessibilityLabel(L10n.Scene.Compose.Accessibility.postOptions)
}
}
extension ComposeContentToolbarView {
func label(for action: ComposeContentToolbarView.ViewModel.Action) -> some View {
Image(uiImage: viewModel.image(for: action))
.foregroundColor(Color(Asset.Scene.Compose.buttonTint.color))
.frame(width: 24, height: 24, alignment: .center)
.accessibilityLabel(viewModel.label(for: action))
}
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
}
}
}
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)
}
}
}