forked from shadowfacts/Tusker
parent
24fb0e0e7b
commit
a6d64282c0
|
@ -48,7 +48,7 @@ class PostService: ObservableObject {
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: draft.visibility,
|
visibility: draft.visibility,
|
||||||
language: nil,
|
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
||||||
pollOptions: draft.poll?.pollOptions.map(\.text),
|
pollOptions: draft.poll?.pollOptions.map(\.text),
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
pollMultiple: draft.poll?.multiple,
|
pollMultiple: draft.poll?.multiple,
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
|
||||||
protocol ComposeInput: AnyObject, ObservableObject {
|
protocol ComposeInput: AnyObject, ObservableObject {
|
||||||
var toolbarElements: [ToolbarElement] { get }
|
var toolbarElements: [ToolbarElement] { get }
|
||||||
|
var textInputMode: UITextInputMode? { get }
|
||||||
|
|
||||||
var autocompleteState: AutocompleteState? { get }
|
var autocompleteState: AutocompleteState? { get }
|
||||||
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { get }
|
var autocompleteStatePublisher: Published<AutocompleteState?>.Publisher { get }
|
||||||
|
|
|
@ -50,6 +50,7 @@ public final class ComposeController: ViewController {
|
||||||
@Published var poster: PostService?
|
@Published var poster: PostService?
|
||||||
@Published var postError: PostService.Error?
|
@Published var postError: PostService.Error?
|
||||||
@Published public private(set) var didPostSuccessfully = false
|
@Published public private(set) var didPostSuccessfully = false
|
||||||
|
@Published var hasChangedLanguageSelection = false
|
||||||
|
|
||||||
var isPosting: Bool {
|
var isPosting: Bool {
|
||||||
poster != nil
|
poster != nil
|
||||||
|
@ -107,6 +108,10 @@ public final class ComposeController: ViewController {
|
||||||
self.autocompleteController = AutocompleteController(parent: self)
|
self.autocompleteController = AutocompleteController(parent: self)
|
||||||
self.toolbarController = ToolbarController(parent: self)
|
self.toolbarController = ToolbarController(parent: self)
|
||||||
self.attachmentsListController = AttachmentsListController(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 {
|
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 {
|
struct ComposeView: View {
|
||||||
@OptionalObservedObject var poster: PostService?
|
@OptionalObservedObject var poster: PostService?
|
||||||
@EnvironmentObject var controller: ComposeController
|
@EnvironmentObject var controller: ComposeController
|
||||||
|
|
|
@ -79,6 +79,11 @@ class ToolbarController: ViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if #available(iOS 16.0, *),
|
||||||
|
composeController.mastodonController.instanceFeatures.createStatusWithLanguage {
|
||||||
|
LanguagePicker(draftLanguage: $draft.language, hasChangedSelection: $composeController.hasChangedLanguageSelection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.frame(minWidth: minWidth)
|
.frame(minWidth: minWidth)
|
||||||
|
|
|
@ -26,6 +26,7 @@ public class Draft: NSManagedObject, Identifiable {
|
||||||
@NSManaged public var id: UUID
|
@NSManaged public var id: UUID
|
||||||
@NSManaged public var initialText: String
|
@NSManaged public var initialText: String
|
||||||
@NSManaged public var inReplyToID: String?
|
@NSManaged public var inReplyToID: String?
|
||||||
|
@NSManaged public var language: String? // ISO 639 language code
|
||||||
@NSManaged public var lastModified: Date
|
@NSManaged public var lastModified: Date
|
||||||
@NSManaged public var localOnly: Bool
|
@NSManaged public var localOnly: Bool
|
||||||
@NSManaged public var text: String
|
@NSManaged public var text: String
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="initialText" attributeType="String"/>
|
<attribute name="initialText" attributeType="String"/>
|
||||||
<attribute name="inReplyToID" optional="YES" 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="lastModified" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
<attribute name="text" attributeType="String" defaultValueString=""/>
|
<attribute name="text" attributeType="String" defaultValueString=""/>
|
||||||
|
|
|
@ -123,6 +123,10 @@ struct EmojiTextField: UIViewRepresentable {
|
||||||
|
|
||||||
var toolbarElements: [ToolbarElement] { [.emojiPicker] }
|
var toolbarElements: [ToolbarElement] { [.emojiPicker] }
|
||||||
|
|
||||||
|
var textInputMode: UITextInputMode? {
|
||||||
|
textField?.textInputMode
|
||||||
|
}
|
||||||
|
|
||||||
func applyFormat(_ format: StatusFormat) {
|
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]
|
[.emojiPicker, .formattingButtons]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textInputMode: UITextInputMode? {
|
||||||
|
textView?.textInputMode
|
||||||
|
}
|
||||||
|
|
||||||
func autocomplete(with string: String) {
|
func autocomplete(with string: String) {
|
||||||
textView?.autocomplete(with: string, permittedModes: .all, autocompleteState: &autocompleteState)
|
textView?.autocomplete(with: string, permittedModes: .all, autocompleteState: &autocompleteState)
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,10 @@ public class InstanceFeatures: ObservableObject {
|
||||||
instanceType.isMastodon
|
instanceType.isMastodon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var createStatusWithLanguage: Bool {
|
||||||
|
instanceType.isMastodon || instanceType.isPleroma(.akkoma(nil))
|
||||||
|
}
|
||||||
|
|
||||||
public init() {
|
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 {
|
var isPleroma: Bool {
|
||||||
if case .pleroma(_) = self {
|
if case .pleroma(_) = self {
|
||||||
return true
|
return true
|
||||||
|
@ -206,17 +219,50 @@ extension InstanceFeatures {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPleroma(_ subtype: PleromaType) -> Bool {
|
||||||
|
if case .pleroma(let t) = self,
|
||||||
|
t.equalsIgnoreVersion(subtype) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MastodonType {
|
enum MastodonType {
|
||||||
case vanilla
|
case vanilla
|
||||||
case hometown(Version?)
|
case hometown(Version?)
|
||||||
case glitch
|
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 {
|
enum PleromaType {
|
||||||
case vanilla(Version?)
|
case vanilla(Version?)
|
||||||
case akkoma(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,
|
sensitive: Bool? = nil,
|
||||||
spoilerText: String? = nil,
|
spoilerText: String? = nil,
|
||||||
visibility: Visibility? = nil,
|
visibility: Visibility? = nil,
|
||||||
language: String? = nil,
|
language: String? = nil, // language supported by mastodon and akkoma
|
||||||
pollOptions: [String]? = nil,
|
pollOptions: [String]? = nil,
|
||||||
pollExpiresIn: Int? = nil,
|
pollExpiresIn: Int? = nil,
|
||||||
pollMultiple: Bool? = nil,
|
pollMultiple: Bool? = nil,
|
||||||
|
|
Loading…
Reference in New Issue