Re-add more missing stuff

This commit is contained in:
Shadowfacts 2025-02-06 23:58:23 -05:00
parent 042110ec5e
commit 1be3cb77b6
8 changed files with 217 additions and 21 deletions

View File

@ -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 }
}
}

View File

@ -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 }
}
}

View File

@ -67,6 +67,10 @@ public class Draft: NSManagedObject, Identifiable {
lastModified = Date()
}
public func addAttachment(_ attachment: DraftAttachment) {
attachments.add(attachment)
}
}
extension Draft {

View File

@ -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
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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 {

View File

@ -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) {