261 lines
9.5 KiB
Swift
261 lines
9.5 KiB
Swift
//
|
|
// AttachmentsListController.swift
|
|
// ComposeUI
|
|
//
|
|
// Created by Shadowfacts on 3/8/23.
|
|
//
|
|
|
|
import SwiftUI
|
|
import PhotosUI
|
|
import PencilKit
|
|
|
|
class AttachmentsListController: ViewController {
|
|
|
|
unowned let parent: ComposeController
|
|
var draft: Draft { parent.draft }
|
|
|
|
var isValid: Bool {
|
|
!requiresAttachmentDescriptions && validAttachmentCombination
|
|
}
|
|
|
|
private var requiresAttachmentDescriptions: Bool {
|
|
if parent.config.requireAttachmentDescriptions {
|
|
if draft.attachments.count == 0 {
|
|
return false
|
|
} else {
|
|
return !parent.attachmentsMissingDescriptions.isEmpty
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var validAttachmentCombination: Bool {
|
|
if !parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
|
|
return true
|
|
} else if draft.attachments.count > 1,
|
|
draft.draftAttachments.contains(where: { $0.type == .video }) {
|
|
return false
|
|
} else if draft.attachments.count > 4 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
init(parent: ComposeController) {
|
|
self.parent = parent
|
|
}
|
|
|
|
var canAddAttachment: Bool {
|
|
if parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
|
|
return draft.attachments.count < 4 && draft.draftAttachments.allSatisfy { $0.type == .image } && draft.poll == nil
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
private var canAddPoll: Bool {
|
|
if parent.mastodonController.instanceFeatures.pollsAndAttachments {
|
|
return true
|
|
} else {
|
|
return draft.attachments.count == 0
|
|
}
|
|
}
|
|
|
|
var view: some View {
|
|
AttachmentsList()
|
|
}
|
|
|
|
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
|
// just using moveObjects(at:to:) on the draft.attachments NSMutableOrderedSet
|
|
// results in the order switching back to the previous order and then to the correct one
|
|
// on the subsequent 2 view updates. creating a new set with the proper order doesn't have that problem
|
|
var array = draft.draftAttachments
|
|
array.move(fromOffsets: source, toOffset: destination)
|
|
draft.attachments = NSMutableOrderedSet(array: array)
|
|
}
|
|
|
|
private func deleteAttachments(at indices: IndexSet) {
|
|
var array = draft.draftAttachments
|
|
array.remove(atOffsets: indices)
|
|
draft.attachments = NSMutableOrderedSet(array: array)
|
|
}
|
|
|
|
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) {
|
|
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 {
|
|
guard self.canAddAttachment else { return }
|
|
DraftsPersistentContainer.shared.viewContext.insert(attachment)
|
|
attachment.draft = self.draft
|
|
self.draft.attachments.add(attachment)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func addImage() {
|
|
parent.deleteDraftOnDisappear = false
|
|
parent.config.presentAssetPicker?({ results in
|
|
self.insertAttachments(at: self.draft.attachments.count, itemProviders: results.map(\.itemProvider))
|
|
})
|
|
}
|
|
|
|
private func addDrawing() {
|
|
parent.deleteDraftOnDisappear = false
|
|
parent.config.presentDrawing?(PKDrawing()) { drawing in
|
|
let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext)
|
|
attachment.id = UUID()
|
|
attachment.drawing = drawing
|
|
attachment.draft = self.draft
|
|
self.draft.attachments.add(attachment)
|
|
}
|
|
}
|
|
|
|
private func togglePoll() {
|
|
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
|
|
withAnimation {
|
|
draft.poll = draft.poll == nil ? Poll(context: DraftsPersistentContainer.shared.viewContext) : nil
|
|
}
|
|
}
|
|
|
|
struct AttachmentsList: View {
|
|
private let cellHeight: CGFloat = 80
|
|
private let cellPadding: CGFloat = 12
|
|
|
|
@EnvironmentObject private var controller: AttachmentsListController
|
|
@EnvironmentObject private var draft: Draft
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
|
|
|
var body: some View {
|
|
attachmentsList
|
|
|
|
Group {
|
|
if controller.parent.config.presentAssetPicker != nil {
|
|
addImageButton
|
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
|
}
|
|
|
|
if controller.parent.config.presentDrawing != nil {
|
|
addDrawingButton
|
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
|
}
|
|
|
|
togglePollButton
|
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
|
}
|
|
#if os(visionOS)
|
|
.buttonStyle(.bordered)
|
|
.labelStyle(AttachmentButtonLabelStyle())
|
|
#endif
|
|
}
|
|
|
|
private var attachmentsList: some View {
|
|
ForEach(draft.attachments.array as! [DraftAttachment]) { attachment in
|
|
ControllerView(controller: { AttachmentRowController(parent: controller.parent, attachment: attachment) })
|
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
|
.id(attachment.id)
|
|
}
|
|
.onMove(perform: controller.moveAttachments)
|
|
.onDelete(perform: controller.deleteAttachments)
|
|
.conditionally(controller.canAddAttachment) {
|
|
$0.onInsert(of: DraftAttachment.readableTypeIdentifiersForItemProvider, perform: { offset, providers in
|
|
controller.insertAttachments(at: offset, itemProviders: providers)
|
|
})
|
|
}
|
|
// only sort of works, see #240
|
|
.onDrop(of: DraftAttachment.readableTypeIdentifiersForItemProvider, isTargeted: nil) { providers in
|
|
controller.insertAttachments(at: 0, itemProviders: providers)
|
|
return true
|
|
}
|
|
}
|
|
|
|
private var addImageButton: some View {
|
|
Button(action: controller.addImage) {
|
|
Label("Add photo or video", systemImage: colorScheme == .dark ? "photo.fill" : "photo")
|
|
}
|
|
.disabled(!controller.canAddAttachment)
|
|
.foregroundColor(.accentColor)
|
|
.frame(height: cellHeight / 2)
|
|
}
|
|
|
|
private var addDrawingButton: some View {
|
|
Button(action: controller.addDrawing) {
|
|
Label("Draw something", systemImage: "hand.draw")
|
|
}
|
|
.disabled(!controller.canAddAttachment)
|
|
.foregroundColor(.accentColor)
|
|
.frame(height: cellHeight / 2)
|
|
}
|
|
|
|
private var togglePollButton: some View {
|
|
Button(action: controller.togglePoll) {
|
|
Label(draft.poll == nil ? "Add a poll" : "Remove poll", systemImage: "chart.bar.doc.horizontal")
|
|
}
|
|
.disabled(!controller.canAddPoll)
|
|
.foregroundColor(.accentColor)
|
|
.frame(height: cellHeight / 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate extension View {
|
|
@ViewBuilder
|
|
func conditionally(_ condition: Bool, body: (Self) -> some View) -> some View {
|
|
if condition {
|
|
body(self)
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
|
|
@available(iOS, obsoleted: 16.0)
|
|
@ViewBuilder
|
|
func sheetOrPopover(isPresented: Binding<Bool>, @ViewBuilder content: @escaping () -> some View) -> some View {
|
|
if #available(iOS 16.0, *) {
|
|
self.modifier(SheetOrPopover(isPresented: isPresented, view: content))
|
|
} else {
|
|
self.popover(isPresented: isPresented, content: content)
|
|
}
|
|
}
|
|
|
|
@available(iOS, obsoleted: 16.0)
|
|
@ViewBuilder
|
|
func withSheetDetentsIfAvailable() -> some View {
|
|
if #available(iOS 16.0, *) {
|
|
self
|
|
.presentationDetents([.medium, .large])
|
|
.presentationDragIndicator(.visible)
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
fileprivate struct SheetOrPopover<V: View>: ViewModifier {
|
|
@Binding var isPresented: Bool
|
|
@ViewBuilder let view: () -> V
|
|
|
|
@Environment(\.horizontalSizeClass) var sizeClass
|
|
|
|
func body(content: Content) -> some View {
|
|
if sizeClass == .compact {
|
|
content.sheet(isPresented: $isPresented, content: view)
|
|
} else {
|
|
content.popover(isPresented: $isPresented, content: view)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(visionOS 1.0, *)
|
|
fileprivate struct AttachmentButtonLabelStyle: LabelStyle {
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
DefaultLabelStyle().makeBody(configuration: configuration)
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|