More layout rework for the composer

This commit is contained in:
Thomas Ricouard 2024-01-07 07:03:39 +01:00
parent 8e8737b040
commit 34a482f01f
5 changed files with 193 additions and 158 deletions

View file

@ -14,7 +14,7 @@ import SwiftUI
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = true
@AppStorage("social_keyboard_composer") public var isSocialKeyboardEnabled: Bool = false
@AppStorage("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false

View file

@ -57,13 +57,10 @@ extension StatusEditor {
actionsView
}
#else
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 16) {
actionsView
}
.padding(.horizontal, .layoutPadding)
HStack(alignment: .center, spacing: 16) {
actionsView
}
Spacer()
.padding(.horizontal, .layoutPadding)
#endif
}
@ -153,6 +150,7 @@ extension StatusEditor {
.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)))
@ -160,28 +158,7 @@ extension StatusEditor {
Image(systemName: "arrowshape.turn.up.left.circle.fill")
}
.disabled(!canAddNewSEVM)
Button {
withAnimation {
viewModel.showPoll.toggle()
viewModel.resetPollDefaults()
}
} label: {
Image(systemName: "chart.bar")
}
.accessibilityLabel("accessibility.editor.button.poll")
.disabled(viewModel.shouldDisablePollButton)
Button {
withAnimation {
viewModel.spoilerOn.toggle()
}
isSpoilerTextFocused = viewModel.id
} label: {
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
}
.accessibilityLabel("accessibility.editor.button.spoiler")
if !viewModel.customEmojiContainer.isEmpty {
Button {
isCustomEmojisSheetDisplay = true
@ -202,20 +179,23 @@ extension StatusEditor {
}
}
Button {
viewModel.insertStatusText(text: "#")
} label: {
Image(systemName: "number")
if preferences.isOpenAIEnabled {
AIMenu.disabled(!viewModel.canPost)
}
Spacer()
Button {
viewModel.insertStatusText(text: "@")
} label: {
Image(systemName: "at")
}
if preferences.isOpenAIEnabled {
AIMenu.disabled(!viewModel.canPost)
Button {
viewModel.insertStatusText(text: "#")
} label: {
Image(systemName: "number")
}
}

View file

@ -0,0 +1,129 @@
import DesignSystem
import Env
import SwiftUI
import Models
extension StatusEditor {
@MainActor
struct LangButton: View {
@Environment(Theme.self) private var theme
@Environment(CurrentInstance.self) private var currentInstance
@Environment(UserPreferences.self) private var preferences
@State private var isLanguageSheetDisplayed: Bool = false
@State private var languageSearch: String = ""
var viewModel: ViewModel
var body: some View {
Button {
isLanguageSheetDisplayed.toggle()
} label: {
HStack(alignment: .center) {
Image(systemName: "text.bubble")
if let language = viewModel.selectedLanguage {
Text(language.uppercased())
} else {
Image(systemName: "globe")
}
}
.font(.footnote)
}
.buttonStyle(.bordered)
.onAppear {
viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage)
}
.accessibilityLabel("accessibility.editor.button.language")
.popover(isPresented: $isLanguageSheetDisplayed) {
if UIDevice.current.userInterfaceIdiom == .phone {
languageSheetView
} else {
languageSheetView
.frame(width: 400, height: 500)
}
}
}
private var languageSheetView: some View {
NavigationStack {
List {
if languageSearch.isEmpty {
if !recentlyUsedLanguages.isEmpty {
Section("status.editor.language-select.recently-used") {
languageSheetSection(languages: recentlyUsedLanguages)
}
}
Section {
languageSheetSection(languages: otherLanguages)
}
} else {
languageSheetSection(languages: languageSearchResult(query: languageSearch))
}
}
.searchable(text: $languageSearch)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("action.cancel", action: { isLanguageSheetDisplayed = false })
}
}
.navigationTitle("status.editor.language-select.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
}
@ViewBuilder
private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View {
if let nativeName, let name {
Text("\(nativeName) (\(name))")
} else {
Text(isoCode.uppercased())
}
}
private func languageSheetSection(languages: [Language]) -> some View {
ForEach(languages) { language in
HStack {
languageTextView(
isoCode: language.isoCode,
nativeName: language.nativeName,
name: language.localizedName
).tag(language.isoCode)
Spacer()
if language.isoCode == viewModel.selectedLanguage {
Image(systemName: "checkmark")
}
}
.listRowBackground(theme.primaryBackgroundColor)
.contentShape(Rectangle())
.onTapGesture {
viewModel.selectedLanguage = language.isoCode
viewModel.hasExplicitlySelectedLanguage = true
isLanguageSheetDisplayed = false
}
}
}
private var recentlyUsedLanguages: [Language] {
preferences.recentlyUsedLanguages.compactMap { isoCode in
Language.allAvailableLanguages.first { $0.isoCode == isoCode }
}
}
private var otherLanguages: [Language] {
Language.allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) }
}
private func languageSearchResult(query: String) -> [Language] {
Language.allAvailableLanguages.filter { language in
guard !languageSearch.isEmpty else {
return true
}
return language.nativeName?.lowercased().hasPrefix(query.lowercased()) == true
|| language.localizedName?.lowercased().hasPrefix(query.lowercased()) == true
}
}
}
}

View file

@ -23,9 +23,6 @@ extension StatusEditor {
@Bindable var viewModel: ViewModel
@Binding var followUpSEVMs: [ViewModel]
@Binding var editingMediaContainer: MediaContainer?
@State private var isLanguageSheetDisplayed: Bool = false
@State private var languageSearch: String = ""
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
@FocusState<EditorFocusState?>.Binding var editorFocusState: EditorFocusState?
@ -47,10 +44,10 @@ extension StatusEditor {
VStack(spacing: 0) {
accountHeaderView
textInput
pollView
characterCountAndLangView
MediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer)
embeddedStatus
pollView
}
.padding(.vertical)
@ -128,9 +125,19 @@ extension StatusEditor {
@ViewBuilder
private var embeddedStatus: some View {
if viewModel.replyToStatus != nil { Divider().padding(.top, 20) }
if let status = viewModel.embeddedStatus ?? viewModel.replyToStatus {
if let status = viewModel.replyToStatus {
Divider().padding(.vertical, .statusComponentSpacing)
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: RouterPath(),
showActions: false))
.accessibilityLabel(status.content.asRawText)
.allowsHitTesting(false)
.environment(\.isStatusFocused, false)
.padding(.horizontal, .layoutPadding)
.padding(.vertical, .statusComponentSpacing)
} else if let status = viewModel.embeddedStatus {
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
.padding(.horizontal, .layoutPadding)
.disabled(true)
@ -150,130 +157,49 @@ extension StatusEditor {
private var characterCountAndLangView: some View {
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
HStack(alignment: .center) {
LangButton(viewModel: viewModel)
.padding(.leading, .layoutPadding)
Button {
withAnimation {
viewModel.showPoll.toggle()
viewModel.resetPollDefaults()
}
} label: {
Image(systemName: viewModel.showPoll ? "chart.bar.fill" : "chart.bar")
}
.buttonStyle(.bordered)
.accessibilityLabel("accessibility.editor.button.poll")
.disabled(viewModel.shouldDisablePollButton)
Button {
withAnimation {
viewModel.spoilerOn.toggle()
}
isSpoilerTextFocused = viewModel.id
} label: {
Image(systemName: viewModel.spoilerOn ? "exclamationmark.triangle.fill" : "exclamationmark.triangle")
}
.buttonStyle(.bordered)
.accessibilityLabel("accessibility.editor.button.spoiler")
Spacer()
Text("\(value)")
.foregroundColor(value < 0 ? .red : .secondary)
.font(.scaledCallout)
.font(.callout.monospacedDigit())
.contentTransition(.numericText(value: Double(value)))
.animation(.default, value: value)
.accessibilityLabel("accessibility.editor.button.characters-remaining")
.accessibilityValue("\(value)")
.accessibilityRemoveTraits(.isStaticText)
.accessibilityAddTraits(.updatesFrequently)
.accessibilityRespondsToUserInteraction(false)
.padding(.leading, .layoutPadding)
Button {
isLanguageSheetDisplayed.toggle()
} label: {
HStack(alignment: .center) {
if let language = viewModel.selectedLanguage {
Image(systemName: "text.bubble")
Text(language.uppercased())
} else {
Image(systemName: "globe")
}
}
.font(.footnote)
}
.buttonStyle(.bordered)
.onAppear {
viewModel.setInitialLanguageSelection(preference: preferences.recentlyUsedLanguages.first ?? preferences.serverPreferences?.postLanguage)
}
.accessibilityLabel("accessibility.editor.button.language")
.popover(isPresented: $isLanguageSheetDisplayed) {
if UIDevice.current.userInterfaceIdiom == .phone {
languageSheetView
} else {
languageSheetView
.frame(width: 400, height: 500)
}
}
Spacer()
.padding(.trailing, .layoutPadding)
}
.padding(.bottom, 8)
.padding(.vertical, 8)
}
private var languageSheetView: some View {
NavigationStack {
List {
if languageSearch.isEmpty {
if !recentlyUsedLanguages.isEmpty {
Section("status.editor.language-select.recently-used") {
languageSheetSection(languages: recentlyUsedLanguages)
}
}
Section {
languageSheetSection(languages: otherLanguages)
}
} else {
languageSheetSection(languages: languageSearchResult(query: languageSearch))
}
}
.searchable(text: $languageSearch)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("action.cancel", action: { isLanguageSheetDisplayed = false })
}
}
.navigationTitle("status.editor.language-select.navigation-title")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
}
@ViewBuilder
private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View {
if let nativeName, let name {
Text("\(nativeName) (\(name))")
} else {
Text(isoCode.uppercased())
}
}
private func languageSheetSection(languages: [Language]) -> some View {
ForEach(languages) { language in
HStack {
languageTextView(
isoCode: language.isoCode,
nativeName: language.nativeName,
name: language.localizedName
).tag(language.isoCode)
Spacer()
if language.isoCode == viewModel.selectedLanguage {
Image(systemName: "checkmark")
}
}
.listRowBackground(theme.primaryBackgroundColor)
.contentShape(Rectangle())
.onTapGesture {
viewModel.selectedLanguage = language.isoCode
viewModel.hasExplicitlySelectedLanguage = true
isLanguageSheetDisplayed = false
}
}
}
private var recentlyUsedLanguages: [Language] {
preferences.recentlyUsedLanguages.compactMap { isoCode in
Language.allAvailableLanguages.first { $0.isoCode == isoCode }
}
}
private var otherLanguages: [Language] {
Language.allAvailableLanguages.filter { !preferences.recentlyUsedLanguages.contains($0.isoCode) }
}
private func languageSearchResult(query: String) -> [Language] {
Language.allAvailableLanguages.filter { language in
guard !languageSearch.isEmpty else {
return true
}
return language.nativeName?.lowercased().hasPrefix(query.lowercased()) == true
|| language.localizedName?.lowercased().hasPrefix(query.lowercased()) == true
}
}
private func setupViewModel() {
viewModel.client = client
viewModel.currentAccount = currentAccount.account

View file

@ -125,7 +125,7 @@ extension StatusEditor {
}
var shouldDisablePollButton: Bool {
!mediaPickers.isEmpty
!mediaContainers.isEmpty
}
var shouldDisplayDismissWarning: Bool {