mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-29 03:31:02 +00:00
* Implement language selection in status editor * Apply the correct language on replies and edits * Use sheet for language selector Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
parent
e0f8c9a3c9
commit
382ebcf8f7
4 changed files with 106 additions and 21 deletions
|
@ -50,6 +50,7 @@ public protocol AnyStatus {
|
||||||
var spoilerText: String { get }
|
var spoilerText: String { get }
|
||||||
var filtered: [Filtered]? { get }
|
var filtered: [Filtered]? { get }
|
||||||
var sensitive: Bool { get }
|
var sensitive: Bool { get }
|
||||||
|
var language: String? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,6 +84,7 @@ public struct Status: AnyStatus, Codable, Identifiable {
|
||||||
public let spoilerText: String
|
public let spoilerText: String
|
||||||
public let filtered: [Filtered]?
|
public let filtered: [Filtered]?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
public let language: String?
|
||||||
|
|
||||||
public static func placeholder() -> Status {
|
public static func placeholder() -> Status {
|
||||||
.init(id: UUID().uuidString,
|
.init(id: UUID().uuidString,
|
||||||
|
@ -109,7 +111,8 @@ public struct Status: AnyStatus, Codable, Identifiable {
|
||||||
poll: nil,
|
poll: nil,
|
||||||
spoilerText: "",
|
spoilerText: "",
|
||||||
filtered: [],
|
filtered: [],
|
||||||
sensitive: false)
|
sensitive: false,
|
||||||
|
language: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func placeholders() -> [Status] {
|
public static func placeholders() -> [Status] {
|
||||||
|
@ -146,4 +149,5 @@ public struct ReblogStatus: AnyStatus, Codable, Identifiable {
|
||||||
public let spoilerText: String
|
public let spoilerText: String
|
||||||
public let filtered: [Filtered]?
|
public let filtered: [Filtered]?
|
||||||
public let sensitive: Bool
|
public let sensitive: Bool
|
||||||
|
public let language: String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,8 @@ public struct StatusData: Encodable {
|
||||||
public let spoilerText: String?
|
public let spoilerText: String?
|
||||||
public let mediaIds: [String]?
|
public let mediaIds: [String]?
|
||||||
public let poll: PollData?
|
public let poll: PollData?
|
||||||
|
public let language: String?
|
||||||
|
|
||||||
public struct PollData: Encodable {
|
public struct PollData: Encodable {
|
||||||
public let options: [String]
|
public let options: [String]
|
||||||
public let multiple: Bool
|
public let multiple: Bool
|
||||||
|
@ -98,12 +99,14 @@ public struct StatusData: Encodable {
|
||||||
inReplyToId: String? = nil,
|
inReplyToId: String? = nil,
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
mediaIds: [String]? = nil,
|
mediaIds: [String]? = nil,
|
||||||
poll: PollData? = nil) {
|
poll: PollData? = nil,
|
||||||
|
language: String? = nil) {
|
||||||
self.status = status
|
self.status = status
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.inReplyToId = inReplyToId
|
self.inReplyToId = inReplyToId
|
||||||
self.spoilerText = spoilerText
|
self.spoilerText = spoilerText
|
||||||
self.mediaIds = mediaIds
|
self.mediaIds = mediaIds
|
||||||
self.poll = poll
|
self.poll = poll
|
||||||
|
self.language = language
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,16 @@ import Models
|
||||||
import Env
|
import Env
|
||||||
|
|
||||||
struct StatusEditorAccessoryView: View {
|
struct StatusEditorAccessoryView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@EnvironmentObject private var preferences: UserPreferences
|
@EnvironmentObject private var preferences: UserPreferences
|
||||||
@EnvironmentObject private var theme: Theme
|
@EnvironmentObject private var theme: Theme
|
||||||
@EnvironmentObject private var currentInstance: CurrentInstance
|
@EnvironmentObject private var currentInstance: CurrentInstance
|
||||||
|
|
||||||
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
|
@FocusState<Bool>.Binding var isSpoilerTextFocused: Bool
|
||||||
@ObservedObject var viewModel: StatusEditorViewModel
|
@ObservedObject var viewModel: StatusEditorViewModel
|
||||||
|
|
||||||
@State private var isDrafsSheetDisplayed: Bool = false
|
@State private var isDrafsSheetDisplayed: Bool = false
|
||||||
|
@State private var isLanguageSheetDisplayed: Bool = false
|
||||||
|
@State private var languageSearch: String = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
@ -25,18 +26,6 @@ struct StatusEditorAccessoryView: View {
|
||||||
}
|
}
|
||||||
.disabled(viewModel.showPoll)
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.insertStatusText(text: " @")
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "at")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
viewModel.insertStatusText(text: " #")
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "number")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.showPoll.toggle()
|
viewModel.showPoll.toggle()
|
||||||
|
@ -61,9 +50,18 @@ struct StatusEditorAccessoryView: View {
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "archivebox")
|
Image(systemName: "archivebox")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
isLanguageSheetDisplayed.toggle()
|
||||||
|
} label: {
|
||||||
|
if let language = viewModel.selectedLanguage {
|
||||||
|
Text(language.uppercased())
|
||||||
|
} else {
|
||||||
|
Image(systemName: "globe")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
characterCountView
|
characterCountView
|
||||||
|
@ -76,6 +74,54 @@ struct StatusEditorAccessoryView: View {
|
||||||
.sheet(isPresented: $isDrafsSheetDisplayed) {
|
.sheet(isPresented: $isDrafsSheetDisplayed) {
|
||||||
draftsSheetView
|
draftsSheetView
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $isLanguageSheetDisplayed, content: {
|
||||||
|
languageSheetView
|
||||||
|
})
|
||||||
|
.onAppear {
|
||||||
|
viewModel.setInitialLanguageSelection(preference: preferences.serverPreferences?.postLanguage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func languageTextView(isoCode: String, nativeName: String?, name: String?) -> some View {
|
||||||
|
if let nativeName = nativeName, let name = name {
|
||||||
|
Text("\(nativeName) (\(name))")
|
||||||
|
} else {
|
||||||
|
Text(isoCode.uppercased())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var languageSheetView: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
ForEach(availableLanguages, id: \.0) { (isoCode, nativeName, name) in
|
||||||
|
HStack {
|
||||||
|
languageTextView(isoCode: isoCode, nativeName: nativeName, name: name)
|
||||||
|
.tag(isoCode)
|
||||||
|
Spacer()
|
||||||
|
if isoCode == viewModel.selectedLanguage {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.selectedLanguage = isoCode
|
||||||
|
isLanguageSheetDisplayed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.searchable(text: $languageSearch)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel", action: { isLanguageSheetDisplayed = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Select Languages")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.secondaryBackgroundColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var draftsSheetView: some View {
|
private var draftsSheetView: some View {
|
||||||
|
@ -98,7 +144,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
}
|
}
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button("Cancel", action: { dismiss() })
|
Button("Cancel", action: { isDrafsSheetDisplayed = false })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
@ -115,4 +161,23 @@ struct StatusEditorAccessoryView: View {
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var availableLanguages: [(String, String?, String?)] {
|
||||||
|
Locale.LanguageCode.isoLanguageCodes
|
||||||
|
.filter { $0.identifier.count == 2 } // Mastodon only supports ISO 639-1 (two-letter) codes
|
||||||
|
.map { lang in
|
||||||
|
let nativeLocale = Locale(languageComponents: Locale.Language.Components(languageCode: lang))
|
||||||
|
return (
|
||||||
|
lang.identifier,
|
||||||
|
nativeLocale.localizedString(forLanguageCode: lang.identifier),
|
||||||
|
Locale.current.localizedString(forLanguageCode: lang.identifier)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filter { (identifier, nativeLocale, locale) in
|
||||||
|
guard !languageSearch.isEmpty else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return nativeLocale?.lowercased().hasPrefix(languageSearch.lowercased()) == true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
|
|
||||||
@Published var mentionsSuggestions: [Account] = []
|
@Published var mentionsSuggestions: [Account] = []
|
||||||
@Published var tagsSuggestions: [Tag] = []
|
@Published var tagsSuggestions: [Tag] = []
|
||||||
|
@Published var selectedLanguage: String?
|
||||||
private var currentSuggestionRange: NSRange?
|
private var currentSuggestionRange: NSRange?
|
||||||
|
|
||||||
private var embededStatusURL: URL? {
|
private var embededStatusURL: URL? {
|
||||||
|
@ -94,6 +95,17 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
statusText = .init(string: text)
|
statusText = .init(string: text)
|
||||||
selectedRange = .init(location: text.utf16.count, length: 0)
|
selectedRange = .init(location: text.utf16.count, length: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setInitialLanguageSelection(preference: String?) {
|
||||||
|
switch mode {
|
||||||
|
case .replyTo(let status), .edit(let status):
|
||||||
|
selectedLanguage = status.language
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedLanguage = selectedLanguage ?? preference ?? currentAccount?.source?.language
|
||||||
|
}
|
||||||
|
|
||||||
private func getPollOptionsForAPI() -> [String]? {
|
private func getPollOptionsForAPI() -> [String]? {
|
||||||
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
||||||
|
@ -116,7 +128,8 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
inReplyToId: mode.replyToStatus?.id,
|
inReplyToId: mode.replyToStatus?.id,
|
||||||
spoilerText: spoilerOn ? spoilerText : nil,
|
spoilerText: spoilerOn ? spoilerText : nil,
|
||||||
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
||||||
poll: pollData)
|
poll: pollData,
|
||||||
|
language: selectedLanguage)
|
||||||
switch mode {
|
switch mode {
|
||||||
case .new, .replyTo, .quote, .mention, .shareExtension:
|
case .new, .replyTo, .quote, .mention, .shareExtension:
|
||||||
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
|
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
|
||||||
|
|
Loading…
Reference in a new issue