forked from shadowfacts/Tusker
222 lines
7.1 KiB
Swift
222 lines
7.1 KiB
Swift
//
|
|
// LanguagePicker.swift
|
|
// ComposeUI
|
|
//
|
|
// Created by Shadowfacts on 5/4/23.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
@available(iOS 16.0, *)
|
|
struct LanguagePicker: View {
|
|
@Binding var draftLanguage: String?
|
|
@Binding var hasChangedSelection: Bool
|
|
@State private var isShowingSheet = false
|
|
|
|
private var codeFromDraft: Locale.LanguageCode? {
|
|
draftLanguage.map(Locale.LanguageCode.init(_:))
|
|
}
|
|
|
|
private var codeFromActiveInputMode: Locale.LanguageCode? {
|
|
UITextInputMode.activeInputModes.first.flatMap(Self.codeFromInputMode(_:))
|
|
}
|
|
|
|
static func codeFromInputMode(_ mode: UITextInputMode) -> Locale.LanguageCode? {
|
|
guard let bcp47Lang = mode.primaryLanguage,
|
|
!bcp47Lang.isEmpty else {
|
|
return nil
|
|
}
|
|
var maybeIso639Code = bcp47Lang[..<bcp47Lang.index(bcp47Lang.startIndex, offsetBy: min(3, bcp47Lang.count))]
|
|
if maybeIso639Code.last == "-" {
|
|
maybeIso639Code = maybeIso639Code[..<maybeIso639Code.index(before: maybeIso639Code.endIndex)]
|
|
}
|
|
let identifier = String(maybeIso639Code)
|
|
// mul (for multiple languages) and unk (unknown) are ISO codes, but not ones that akkoma permits, so we ignore them on all platforms
|
|
guard identifier != "mul",
|
|
identifier != "und" else {
|
|
return nil
|
|
}
|
|
let code = Locale.LanguageCode(identifier)
|
|
if code.isISOLanguage {
|
|
return code
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private var codeFromPreferredLanguages: Locale.LanguageCode? {
|
|
if let identifier = Locale.preferredLanguages.first,
|
|
case let code = Locale.LanguageCode(identifier),
|
|
code.isISOLanguage {
|
|
return code
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private var languageCode: Binding<Locale.LanguageCode> {
|
|
Binding {
|
|
return codeFromDraft ?? codeFromActiveInputMode ?? codeFromPreferredLanguages ?? .english
|
|
} set: { newValue in
|
|
draftLanguage = newValue.identifier
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
Button {
|
|
isShowingSheet = true
|
|
} label: {
|
|
Text((languageCode.wrappedValue.identifier(.alpha2) ?? languageCode.wrappedValue.identifier).uppercased())
|
|
}
|
|
.accessibilityLabel("Post Language")
|
|
.padding(5)
|
|
.hoverEffect()
|
|
.sheet(isPresented: $isShowingSheet) {
|
|
NavigationStack {
|
|
LanguagePickerList(languageCode: languageCode, hasChangedSelection: $hasChangedSelection, isPresented: $isShowingSheet)
|
|
}
|
|
.presentationDetents([.large, .medium])
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
private struct LanguagePickerList: View {
|
|
@Binding var languageCode: Locale.LanguageCode
|
|
@Binding var hasChangedSelection: Bool
|
|
@Binding var isPresented: Bool
|
|
@Environment(\.composeUIConfig.groupedBackgroundColor) private var groupedBackgroundColor
|
|
@Environment(\.composeUIConfig.groupedCellBackgroundColor) private var groupedCellBackgroundColor
|
|
@State private var recentLangs: [Lang] = []
|
|
@State private var langs: [Lang] = []
|
|
@State private var filteredLangs: [Lang]?
|
|
@State private var query = ""
|
|
|
|
private var defaults: UserDefaults {
|
|
UserDefaults(suiteName: "group.space.vaccor.Tusker") ?? .standard
|
|
}
|
|
|
|
private var recentIdentifiers: [String] {
|
|
get {
|
|
defaults.object(forKey: "LanguagePickerRecents") as? [String] ?? []
|
|
}
|
|
nonmutating set {
|
|
defaults.set(newValue, forKey: "LanguagePickerRecents")
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
List {
|
|
Section {
|
|
ForEach(recentLangs) { lang in
|
|
button(for: lang)
|
|
}
|
|
.listRowBackground(groupedCellBackgroundColor)
|
|
} header: {
|
|
Text("Recently Used")
|
|
}
|
|
|
|
Section {
|
|
ForEach(filteredLangs ?? langs) { lang in
|
|
button(for: lang)
|
|
}
|
|
.listRowBackground(groupedCellBackgroundColor)
|
|
} header: {
|
|
Text("All Languages")
|
|
}
|
|
}
|
|
.listStyle(.insetGrouped)
|
|
.scrollContentBackground(.hidden)
|
|
.background(groupedBackgroundColor.edgesIgnoringSafeArea(.all))
|
|
.searchable(text: $query)
|
|
#if !os(visionOS)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
#endif
|
|
.navigationTitle("Post Language")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
Button("Done") {
|
|
isPresented = false
|
|
}
|
|
}
|
|
}
|
|
.onAppear {
|
|
// make sure recents always contains the currently selected lang
|
|
let recents = addRecentLang(languageCode)
|
|
recentLangs = recents
|
|
.filter { $0 != "mul" && $0 != "und" }
|
|
.map { Lang(code: .init($0)) }
|
|
.sorted { $0.name < $1.name }
|
|
|
|
langs = Locale.LanguageCode.isoLanguageCodes
|
|
.filter { $0.identifier != "mul" && $0.identifier != "und" }
|
|
.map { Lang(code: $0) }
|
|
.sorted { $0.name < $1.name }
|
|
}
|
|
#if os(visionOS)
|
|
.onChange(of: query, initial: true) {
|
|
filteredLangsChanged(query: query)
|
|
}
|
|
#else
|
|
.onChange(of: query) { newValue in
|
|
filteredLangsChanged(query: newValue)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private func filteredLangsChanged(query: String) {
|
|
if query.isEmpty {
|
|
filteredLangs = nil
|
|
} else {
|
|
filteredLangs = langs.filter {
|
|
$0.name.localizedCaseInsensitiveContains(query) || $0.code.identifier.localizedCaseInsensitiveContains(query)
|
|
}
|
|
}
|
|
}
|
|
|
|
@discardableResult
|
|
private func addRecentLang(_ code: Locale.LanguageCode) -> [String] {
|
|
var recents = recentIdentifiers
|
|
if !recents.contains(languageCode.identifier) {
|
|
recents.insert(languageCode.identifier, at: 0)
|
|
if recents.count > 5 {
|
|
recents = Array(recents[..<5])
|
|
}
|
|
recentIdentifiers = recents
|
|
}
|
|
return recents
|
|
}
|
|
|
|
private func button(for lang: Lang) -> some View {
|
|
Button {
|
|
languageCode = lang.code
|
|
hasChangedSelection = true
|
|
isPresented = false
|
|
addRecentLang(lang.code)
|
|
} label: {
|
|
HStack {
|
|
Text(lang.name)
|
|
Spacer()
|
|
if lang.code == languageCode {
|
|
Image(systemName: "checkmark")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Lang: Identifiable {
|
|
let code: Locale.LanguageCode
|
|
let name: String
|
|
|
|
var id: String {
|
|
code.identifier
|
|
}
|
|
|
|
init(code: Locale.LanguageCode) {
|
|
self.code = code
|
|
self.name = Locale.current.localizedString(forLanguageCode: code.identifier) ?? code.identifier
|
|
}
|
|
}
|
|
}
|