mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-24 06:48:10 +00:00
More layout rework for the composer
This commit is contained in:
parent
8e8737b040
commit
34a482f01f
5 changed files with 193 additions and 158 deletions
|
@ -14,7 +14,7 @@ import SwiftUI
|
||||||
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
|
@AppStorage("is_open_ai_enabled") public var isOpenAIEnabled: Bool = true
|
||||||
|
|
||||||
@AppStorage("recently_used_languages") public var recentlyUsedLanguages: [String] = []
|
@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("use_instance_content_settings") public var useInstanceContentSettings: Bool = true
|
||||||
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
|
@AppStorage("app_auto_expand_spoilers") public var appAutoExpandSpoilers = false
|
||||||
|
|
|
@ -57,13 +57,10 @@ extension StatusEditor {
|
||||||
actionsView
|
actionsView
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
HStack(alignment: .center, spacing: 16) {
|
||||||
HStack(alignment: .center, spacing: 16) {
|
actionsView
|
||||||
actionsView
|
|
||||||
}
|
|
||||||
.padding(.horizontal, .layoutPadding)
|
|
||||||
}
|
}
|
||||||
Spacer()
|
.padding(.horizontal, .layoutPadding)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +150,7 @@ extension StatusEditor {
|
||||||
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
.accessibilityLabel("accessibility.editor.button.attach-photo")
|
||||||
.disabled(viewModel.showPoll)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
// all SEVM have the same visibility value
|
// all SEVM have the same visibility value
|
||||||
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
|
followUpSEVMs.append(ViewModel(mode: .new(visibility: focusedSEVM.visibility)))
|
||||||
|
@ -161,27 +159,6 @@ extension StatusEditor {
|
||||||
}
|
}
|
||||||
.disabled(!canAddNewSEVM)
|
.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 {
|
if !viewModel.customEmojiContainer.isEmpty {
|
||||||
Button {
|
Button {
|
||||||
isCustomEmojisSheetDisplay = true
|
isCustomEmojisSheetDisplay = true
|
||||||
|
@ -202,20 +179,23 @@ extension StatusEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.insertStatusText(text: "#")
|
if preferences.isOpenAIEnabled {
|
||||||
} label: {
|
AIMenu.disabled(!viewModel.canPost)
|
||||||
Image(systemName: "number")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.insertStatusText(text: "@")
|
viewModel.insertStatusText(text: "@")
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "at")
|
Image(systemName: "at")
|
||||||
}
|
}
|
||||||
|
|
||||||
if preferences.isOpenAIEnabled {
|
Button {
|
||||||
AIMenu.disabled(!viewModel.canPost)
|
viewModel.insertStatusText(text: "#")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "number")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,9 +24,6 @@ extension StatusEditor {
|
||||||
@Binding var followUpSEVMs: [ViewModel]
|
@Binding var followUpSEVMs: [ViewModel]
|
||||||
@Binding var editingMediaContainer: MediaContainer?
|
@Binding var editingMediaContainer: MediaContainer?
|
||||||
|
|
||||||
@State private var isLanguageSheetDisplayed: Bool = false
|
|
||||||
@State private var languageSearch: String = ""
|
|
||||||
|
|
||||||
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
|
@FocusState<UUID?>.Binding var isSpoilerTextFocused: UUID?
|
||||||
@FocusState<EditorFocusState?>.Binding var editorFocusState: EditorFocusState?
|
@FocusState<EditorFocusState?>.Binding var editorFocusState: EditorFocusState?
|
||||||
let assignedFocusState: EditorFocusState
|
let assignedFocusState: EditorFocusState
|
||||||
|
@ -47,10 +44,10 @@ extension StatusEditor {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
accountHeaderView
|
accountHeaderView
|
||||||
textInput
|
textInput
|
||||||
|
pollView
|
||||||
characterCountAndLangView
|
characterCountAndLangView
|
||||||
MediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer)
|
MediaView(viewModel: viewModel, editingMediaContainer: $editingMediaContainer)
|
||||||
embeddedStatus
|
embeddedStatus
|
||||||
pollView
|
|
||||||
}
|
}
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
|
|
||||||
|
@ -128,9 +125,19 @@ extension StatusEditor {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var embeddedStatus: some View {
|
private var embeddedStatus: some View {
|
||||||
if viewModel.replyToStatus != nil { Divider().padding(.top, 20) }
|
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)
|
||||||
|
|
||||||
if let status = viewModel.embeddedStatus ?? viewModel.replyToStatus {
|
} else if let status = viewModel.embeddedStatus {
|
||||||
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
|
@ -150,128 +157,47 @@ extension StatusEditor {
|
||||||
private var characterCountAndLangView: some View {
|
private var characterCountAndLangView: some View {
|
||||||
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
|
let value = (currentInstance.instance?.configuration?.statuses.maxCharacters ?? 500) + viewModel.statusTextCharacterLength
|
||||||
HStack(alignment: .center) {
|
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)")
|
Text("\(value)")
|
||||||
.foregroundColor(value < 0 ? .red : .secondary)
|
.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")
|
.accessibilityLabel("accessibility.editor.button.characters-remaining")
|
||||||
.accessibilityValue("\(value)")
|
.accessibilityValue("\(value)")
|
||||||
.accessibilityRemoveTraits(.isStaticText)
|
.accessibilityRemoveTraits(.isStaticText)
|
||||||
.accessibilityAddTraits(.updatesFrequently)
|
.accessibilityAddTraits(.updatesFrequently)
|
||||||
.accessibilityRespondsToUserInteraction(false)
|
.accessibilityRespondsToUserInteraction(false)
|
||||||
.padding(.leading, .layoutPadding)
|
.padding(.trailing, .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(.bottom, 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
|
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupViewModel() {
|
private func setupViewModel() {
|
||||||
|
|
|
@ -125,7 +125,7 @@ extension StatusEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldDisablePollButton: Bool {
|
var shouldDisablePollButton: Bool {
|
||||||
!mediaPickers.isEmpty
|
!mediaContainers.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldDisplayDismissWarning: Bool {
|
var shouldDisplayDismissWarning: Bool {
|
||||||
|
|
Loading…
Reference in a new issue