Re-add more missing stuff
This commit is contained in:
parent
042110ec5e
commit
1be3cb77b6
@ -47,5 +47,12 @@ public struct ComposeUIConfig {
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeUIConfig {
|
||||
private struct ComposeUIConfigEnvironmentKey: EnvironmentKey {
|
||||
static let defaultValue = ComposeUIConfig()
|
||||
}
|
||||
extension EnvironmentValues {
|
||||
var composeUIConfig: ComposeUIConfig {
|
||||
get { self[ComposeUIConfigEnvironmentKey.self] }
|
||||
set { self[ComposeUIConfigEnvironmentKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
@ -513,13 +513,3 @@ private struct GlobalFrameOutsideListPrefKey: PreferenceKey {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
private struct ComposeUIConfigEnvironmentKey: EnvironmentKey {
|
||||
static let defaultValue = ComposeUIConfig()
|
||||
}
|
||||
extension EnvironmentValues {
|
||||
var composeUIConfig: ComposeUIConfig {
|
||||
get { self[ComposeUIConfigEnvironmentKey.self] }
|
||||
set { self[ComposeUIConfigEnvironmentKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,10 @@ public class Draft: NSManagedObject, Identifiable {
|
||||
lastModified = Date()
|
||||
}
|
||||
|
||||
public func addAttachment(_ attachment: DraftAttachment) {
|
||||
attachments.add(attachment)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Draft {
|
||||
|
@ -32,7 +32,7 @@ public final class DraftAttachment: NSManagedObject, Identifiable {
|
||||
@NSManaged internal var fileType: String?
|
||||
@NSManaged public var id: UUID!
|
||||
|
||||
@NSManaged internal var draft: Draft
|
||||
@NSManaged public var draft: Draft
|
||||
|
||||
public var drawing: PKDrawing? {
|
||||
get {
|
||||
@ -89,7 +89,7 @@ public final class DraftAttachment: NSManagedObject, Identifiable {
|
||||
}
|
||||
|
||||
extension DraftAttachment {
|
||||
var type: AttachmentType {
|
||||
public var type: AttachmentType {
|
||||
if let editedAttachmentKind {
|
||||
switch editedAttachmentKind {
|
||||
case .image:
|
||||
@ -129,7 +129,7 @@ extension DraftAttachment {
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachmentType {
|
||||
public enum AttachmentType {
|
||||
case image, video, unknown
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,12 @@
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import InstanceFeatures
|
||||
import Vision
|
||||
|
||||
struct AttachmentCollectionViewCellView: View {
|
||||
let attachment: DraftAttachment?
|
||||
@State private var recognizingText = false
|
||||
|
||||
var body: some View {
|
||||
if let attachment {
|
||||
@ -19,18 +22,111 @@ struct AttachmentCollectionViewCellView: View {
|
||||
RoundedSquare(cornerRadius: 5)
|
||||
.fill(.quaternary)
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
AttachmentDescriptionLabel(attachment: attachment)
|
||||
.overlay(alignment: .bottomLeading) {
|
||||
if recognizingText {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
} else {
|
||||
AttachmentDescriptionLabel(attachment: attachment)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
AttachmentRemoveButton(attachment: attachment)
|
||||
}
|
||||
.clipShape(RoundedSquare(cornerRadius: 5))
|
||||
// TODO: context menu preview?
|
||||
.contextMenu {
|
||||
if attachment.drawingData != nil {
|
||||
EditDrawingButton(attachment: attachment)
|
||||
} else if attachment.type == .image {
|
||||
RecognizeTextButton(attachment: attachment, recognizingText: $recognizingText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct RecognizeTextButton: View {
|
||||
let attachment: DraftAttachment
|
||||
@Binding var recognizingText: Bool
|
||||
@State private var error: (any Error)?
|
||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.recognizeText) {
|
||||
Label("Recognize Text", systemImage: "doc.text.viewfinder")
|
||||
}
|
||||
.alertWithData("Text Recognition Failed", data: $error) { _ in
|
||||
Button("OK") {}
|
||||
} message: { error in
|
||||
Text(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
private func recognizeText() {
|
||||
recognizingText = true
|
||||
attachment.getData(features: instanceFeatures) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
self.recognizingText = false
|
||||
self.error = error
|
||||
case .success(let (data, _)):
|
||||
let handler = VNImageRequestHandler(data: data)
|
||||
let request = VNRecognizeTextRequest { request, error in
|
||||
DispatchQueue.main.async {
|
||||
if let results = request.results as? [VNRecognizedTextObservation] {
|
||||
var text = ""
|
||||
for observation in results {
|
||||
let result = observation.topCandidates(1).first!
|
||||
text.append(result.string)
|
||||
text.append("\n")
|
||||
}
|
||||
self.attachment.attachmentDescription = text
|
||||
}
|
||||
self.recognizingText = false
|
||||
}
|
||||
}
|
||||
request.recognitionLevel = .accurate
|
||||
request.usesLanguageCorrection = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
try handler.perform([request])
|
||||
} catch let error as NSError where error.code == 1 {
|
||||
// The perform call throws an error with code 1 if the request is cancelled, which we don't want to show an alert for.
|
||||
return
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.recognizingText = false
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EditDrawingButton: View {
|
||||
let attachment: DraftAttachment
|
||||
@Environment(\.composeUIConfig.presentDrawing) private var presentDrawing
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.editDrawing) {
|
||||
Label("Edit Drawing", systemImage: "hand.draw")
|
||||
}
|
||||
}
|
||||
|
||||
private func editDrawing() {
|
||||
guard let drawing = attachment.drawing else {
|
||||
return
|
||||
}
|
||||
presentDrawing?(drawing) { drawing in
|
||||
self.attachment.drawing = drawing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AttachmentRemoveButton: View {
|
||||
let attachment: DraftAttachment
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
import TuskerComponents
|
||||
|
||||
// State owned by the compose UI but that needs to be accessible from outside.
|
||||
public final class ComposeViewState: ObservableObject {
|
||||
@ -51,6 +52,7 @@ public struct ComposeView: View {
|
||||
)
|
||||
.environment(\.composeUIConfig, config)
|
||||
.environment(\.currentAccount, currentAccount)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextObjectsDidChange, object: DraftsPersistentContainer.shared.viewContext), perform: self.managedObjectsDidChange)
|
||||
}
|
||||
|
||||
private func setDraft(_ draft: Draft) {
|
||||
@ -64,6 +66,13 @@ public struct ComposeView: View {
|
||||
}
|
||||
DraftsPersistentContainer.shared.save()
|
||||
}
|
||||
|
||||
private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||
if let deleted = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>,
|
||||
deleted.contains(where: { $0.objectID == state.draft.objectID }) {
|
||||
config.dismiss(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see if this can be broken up further
|
||||
@ -93,6 +102,11 @@ private struct ComposeViewBody: View {
|
||||
}
|
||||
)
|
||||
}
|
||||
.alertWithData("Error Posting", data: $postError, actions: { _ in
|
||||
Button("OK") {}
|
||||
}, message: { error in
|
||||
Text(error.localizedDescription)
|
||||
})
|
||||
.onDisappear(perform: self.deleteOrSaveDraft)
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,8 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
import TuskerPreferences
|
||||
|
||||
struct NewMainTextView: View {
|
||||
static var minHeight: CGFloat { 150 }
|
||||
@ -37,8 +39,9 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||
@Environment(\.composeUIConfig.fillColor) private var fillColor
|
||||
@Environment(\.composeUIConfig.useTwitterKeyboard) private var useTwitterKeyboard
|
||||
@Environment(\.composeUIConfig.textSelectionStartsAtBeginning) private var textSelectionStartsAtBeginning
|
||||
@PreferenceObserving(\.$statusContentType) private var statusContentType
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
func makeUIView(context: Context) -> WrappedTextView {
|
||||
// TODO: if we're not doing the pill background, reevaluate whether this version fork is necessary
|
||||
let view = if #available(iOS 16.0, *) {
|
||||
WrappedTextView(usingTextLayoutManager: true)
|
||||
@ -79,6 +82,8 @@ private struct NewMainTextViewRepresentable: UIViewRepresentable {
|
||||
|
||||
uiView.isEditable = isEnabled
|
||||
uiView.keyboardType = useTwitterKeyboard ? .twitter : .default
|
||||
|
||||
uiView.contentType = statusContentType
|
||||
// #if !os(visionOS)
|
||||
// uiView.backgroundColor = colorScheme == .dark ? UIColor(fillColor) : .secondarySystemBackground
|
||||
// #endif
|
||||
@ -247,6 +252,35 @@ extension WrappedTextViewCoordinator: UITextViewDelegate {
|
||||
|
||||
func textViewDidChangeSelection(_ textView: UITextView) {
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
guard let textView = textView as? WrappedTextView else {
|
||||
return nil
|
||||
}
|
||||
var actions = suggestedActions
|
||||
if textView.contentType != .plain,
|
||||
let index = suggestedActions.firstIndex(where: { ($0 as? UIMenu)?.identifier.rawValue == "com.apple.menu.format" }) {
|
||||
if range.length > 0 {
|
||||
let formatMenu = suggestedActions[index] as! UIMenu
|
||||
let newFormatMenu = formatMenu.replacingChildren(StatusFormat.allCases.map { fmt in
|
||||
return UIAction(title: fmt.accessibilityLabel, image: UIImage(systemName: fmt.imageName)) { _ in
|
||||
// TODO
|
||||
}
|
||||
})
|
||||
actions[index] = newFormatMenu
|
||||
} else {
|
||||
actions.remove(at: index)
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
// if range.length == 0 {
|
||||
// actions.append(UIAction(title: "Insert Emoji", image: UIImage(systemName: "face.smiling"), handler: { [weak self] _ in
|
||||
// self?.controller.shouldEmojiAutocompletionBeginExpanded = true
|
||||
// self?.beginAutocompletingEmoji()
|
||||
// }))
|
||||
// }
|
||||
return UIMenu(children: actions)
|
||||
}
|
||||
}
|
||||
|
||||
//extension WrappedTextViewCoordinator: ComposeInput {
|
||||
@ -269,6 +303,35 @@ extension WrappedTextViewCoordinator: UIDropInteractionDelegate {
|
||||
}
|
||||
|
||||
private final class WrappedTextView: UITextView {
|
||||
var contentType: StatusContentType = .plain
|
||||
|
||||
private static var formattingActions: [Selector] {
|
||||
[#selector(toggleBoldface(_:)), #selector(toggleItalics(_:))]
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if Self.formattingActions.contains(action) {
|
||||
return contentType != .plain
|
||||
}
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
override func toggleBoldface(_ sender: Any?) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override func toggleItalics(_ sender: Any?) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
override func validate(_ command: UICommand) {
|
||||
super.validate(command)
|
||||
|
||||
if Self.formattingActions.contains(command.action),
|
||||
contentType != .plain {
|
||||
command.attributes.remove(.disabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSAttributedString.Key {
|
||||
|
@ -120,13 +120,35 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
}
|
||||
|
||||
override func canPaste(_ itemProviders: [NSItemProvider]) -> Bool {
|
||||
// return controller.canPaste(itemProviders: itemProviders)
|
||||
return false
|
||||
// TODO: pasting
|
||||
guard itemProviders.allSatisfy({ $0.canLoadObject(ofClass: DraftAttachment.self) }) else {
|
||||
return false
|
||||
}
|
||||
if mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
|
||||
let existing = state.draft.draftAttachments
|
||||
if existing.allSatisfy({ $0.type == .image }) {
|
||||
// if providers are videos, this technically allows invalid video/image combinations
|
||||
return itemProviders.count + existing.count <= 4
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func paste(itemProviders: [NSItemProvider]) {
|
||||
// controller.paste(itemProviders: itemProviders)
|
||||
for provider in itemProviders where provider.canLoadObject(ofClass: DraftAttachment.self) {
|
||||
provider.loadObject(ofClass: DraftAttachment.self) { object, error in
|
||||
guard let attachment = object as? DraftAttachment else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
||||
attachment.draft = self.state.draft
|
||||
self.state.draft.addAttachment(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dismiss(mode: DismissMode) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user