forked from shadowfacts/Tusker
parent
24fb0e0e7b
commit
a6d64282c0
|
@ -48,7 +48,7 @@ class PostService: ObservableObject {
|
|||
sensitive: sensitive,
|
||||
spoilerText: contentWarning,
|
||||
visibility: draft.visibility,
|
||||
language: nil,
|
||||
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
||||
pollOptions: draft.poll?.pollOptions.map(\.text),
|
||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||
pollMultiple: draft.poll?.multiple,
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import Foundation
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
protocol ComposeInput: AnyObject, ObservableObject {
|
||||
var toolbarElements: [ToolbarElement] { get }
|
||||
var textInputMode: UITextInputMode? { get }
|
||||
|
||||
var autocompleteState: AutocompleteState? { get }
|
||||
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { get }
|
||||
|
|
|
@ -50,6 +50,7 @@ public final class ComposeController: ViewController {
|
|||
@Published var poster: PostService?
|
||||
@Published var postError: PostService.Error?
|
||||
@Published public private(set) var didPostSuccessfully = false
|
||||
@Published var hasChangedLanguageSelection = false
|
||||
|
||||
var isPosting: Bool {
|
||||
poster != nil
|
||||
|
@ -107,6 +108,10 @@ public final class ComposeController: ViewController {
|
|||
self.autocompleteController = AutocompleteController(parent: self)
|
||||
self.toolbarController = ToolbarController(parent: self)
|
||||
self.attachmentsListController = AttachmentsListController(parent: self)
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(currentInputModeChanged), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public var view: some View {
|
||||
|
@ -225,6 +230,16 @@ public final class ComposeController: ViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
@objc private func currentInputModeChanged() {
|
||||
guard let mode = currentInput?.textInputMode,
|
||||
let code = LanguagePicker.codeFromInputMode(mode),
|
||||
!hasChangedLanguageSelection && !draft.hasContent else {
|
||||
return
|
||||
}
|
||||
draft.language = code.identifier
|
||||
}
|
||||
|
||||
struct ComposeView: View {
|
||||
@OptionalObservedObject var poster: PostService?
|
||||
@EnvironmentObject var controller: ComposeController
|
||||
|
|
|
@ -79,6 +79,11 @@ class ToolbarController: ViewController {
|
|||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if #available(iOS 16.0, *),
|
||||
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
|
||||
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.frame(minWidth: minWidth)
|
||||
|
|
|
@ -26,6 +26,7 @@ public class Draft: NSManagedObject, Identifiable {
|
|||
@NSManaged public var id: UUID
|
||||
@NSManaged public var initialText: String
|
||||
@NSManaged public var inReplyToID: String?
|
||||
@NSManaged public var language: String? // ISO 639 language code
|
||||
@NSManaged public var lastModified: Date
|
||||
@NSManaged public var localOnly: Bool
|
||||
@NSManaged public var text: String
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="initialText" attributeType="String"/>
|
||||
<attribute name="inReplyToID" optional="YES" attributeType="String"/>
|
||||
<attribute name="language" optional="YES" attributeType="String"/>
|
||||
<attribute name="lastModified" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="text" attributeType="String" defaultValueString=""/>
|
||||
|
|
|
@ -123,6 +123,10 @@ struct EmojiTextField: UIViewRepresentable {
|
|||
|
||||
var toolbarElements: [ToolbarElement] { [.emojiPicker] }
|
||||
|
||||
var textInputMode: UITextInputMode? {
|
||||
textField?.textInputMode
|
||||
}
|
||||
|
||||
func applyFormat(_ format: StatusFormat) {
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
//
|
||||
// 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 else {
|
||||
return nil
|
||||
}
|
||||
var maybeIso639Code = bcp47Lang[..<bcp47Lang.index(bcp47Lang.startIndex, offsetBy: 3)]
|
||||
if maybeIso639Code.last == "-" {
|
||||
maybeIso639Code = maybeIso639Code[..<maybeIso639Code.index(before: maybeIso639Code.endIndex)]
|
||||
}
|
||||
let code = Locale.LanguageCode(String(maybeIso639Code))
|
||||
if code.isISOLanguage {
|
||||
return code
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var codeFromPreferredLanguages: Locale.LanguageCode? {
|
||||
if let identifier = Locale.preferredLanguages.first {
|
||||
let code = Locale.LanguageCode(identifier)
|
||||
if code.isISOLanguage {
|
||||
return code
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} 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")
|
||||
.sheet(isPresented: $isShowingSheet) {
|
||||
NavigationStack {
|
||||
LanguagePickerList(languageCode: languageCode, hasChangedSelection: $hasChangedSelection, isPresented: $isShowingSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
private struct LanguagePickerList: View {
|
||||
@Binding var languageCode: Locale.LanguageCode
|
||||
@Binding var hasChangedSelection: Bool
|
||||
@Binding var isPresented: Bool
|
||||
@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)
|
||||
}
|
||||
} header: {
|
||||
Text("Recently Used")
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(filteredLangs ?? langs) { lang in
|
||||
button(for: lang)
|
||||
}
|
||||
} header: {
|
||||
Text("All Languages")
|
||||
}
|
||||
}
|
||||
.searchable(text: $query)
|
||||
.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
|
||||
.map { Lang(code: .init($0)) }
|
||||
.sorted { $0.name < $1.name }
|
||||
|
||||
langs = Locale.LanguageCode.isoLanguageCodes
|
||||
.map { Lang(code: $0) }
|
||||
.sorted { $0.name < $1.name }
|
||||
}
|
||||
.onChange(of: query) { newValue in
|
||||
if newValue.isEmpty {
|
||||
filteredLangs = nil
|
||||
} else {
|
||||
filteredLangs = langs.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(newValue) || $0.code.identifier.localizedCaseInsensitiveContains(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -256,6 +256,10 @@ fileprivate struct MainWrappedTextViewRepresentable: UIViewRepresentable {
|
|||
[.emojiPicker, .formattingButtons]
|
||||
}
|
||||
|
||||
var textInputMode: UITextInputMode? {
|
||||
textView?.textInputMode
|
||||
}
|
||||
|
||||
func autocomplete(with string: String) {
|
||||
textView?.autocomplete(with: string, permittedModes: .all, autocompleteState: &autocompleteState)
|
||||
}
|
||||
|
|
|
@ -111,6 +111,10 @@ public class InstanceFeatures: ObservableObject {
|
|||
instanceType.isMastodon
|
||||
}
|
||||
|
||||
public var createStatusWithLanguage: Bool {
|
||||
instanceType.isMastodon || instanceType.isPleroma(.akkoma(nil))
|
||||
}
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
|
@ -199,6 +203,15 @@ extension InstanceFeatures {
|
|||
}
|
||||
}
|
||||
|
||||
func isMastodon(_ subtype: MastodonType) -> Bool {
|
||||
if case .mastodon(let t, _) = self,
|
||||
t.equalsIgnoreVersion(subtype) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isPleroma: Bool {
|
||||
if case .pleroma(_) = self {
|
||||
return true
|
||||
|
@ -206,17 +219,50 @@ extension InstanceFeatures {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isPleroma(_ subtype: PleromaType) -> Bool {
|
||||
if case .pleroma(let t) = self,
|
||||
t.equalsIgnoreVersion(subtype) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MastodonType {
|
||||
case vanilla
|
||||
case hometown(Version?)
|
||||
case glitch
|
||||
|
||||
func equalsIgnoreVersion(_ other: MastodonType) -> Bool {
|
||||
switch (self, other) {
|
||||
case (.vanilla, .vanilla):
|
||||
return true
|
||||
case (.hometown(_), .hometown(_)):
|
||||
return true
|
||||
case (.glitch, .glitch):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PleromaType {
|
||||
case vanilla(Version?)
|
||||
case akkoma(Version?)
|
||||
|
||||
func equalsIgnoreVersion(_ other: PleromaType) -> Bool {
|
||||
switch (self, other) {
|
||||
case (.vanilla(_), .vanilla(_)):
|
||||
return true
|
||||
case (.akkoma(_), .akkoma(_)):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -385,7 +385,7 @@ public class Client {
|
|||
sensitive: Bool? = nil,
|
||||
spoilerText: String? = nil,
|
||||
visibility: Visibility? = nil,
|
||||
language: String? = nil,
|
||||
language: String? = nil, // language supported by mastodon and akkoma
|
||||
pollOptions: [String]? = nil,
|
||||
pollExpiresIn: Int? = nil,
|
||||
pollMultiple: Bool? = nil,
|
||||
|
|
Loading…
Reference in New Issue