234 lines
8.0 KiB
Swift
234 lines
8.0 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 {
|
||
|
return draft.attachments.allSatisfy {
|
||
|
!$0.attachmentDescription.isEmpty
|
||
|
}
|
||
|
} else {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private var validAttachmentCombination: Bool {
|
||
|
if !parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
|
||
|
return true
|
||
|
} else if draft.attachments.contains(where: { $0.data.type == .video }) &&
|
||
|
draft.attachments.count > 1 {
|
||
|
return false
|
||
|
} else if draft.attachments.count > 4 {
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
init(parent: ComposeController) {
|
||
|
self.parent = parent
|
||
|
}
|
||
|
|
||
|
private var canAddAttachment: Bool {
|
||
|
if parent.mastodonController.instanceFeatures.mastodonAttachmentRestrictions {
|
||
|
return draft.attachments.count < 4 && draft.attachments.allSatisfy { $0.data.type == .image } && draft.poll == nil
|
||
|
} else {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private var canAddPoll: Bool {
|
||
|
if parent.mastodonController.instanceFeatures.pollsAndAttachments {
|
||
|
return true
|
||
|
} else {
|
||
|
return draft.attachments.isEmpty
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var view: some View {
|
||
|
AttachmentsList()
|
||
|
}
|
||
|
|
||
|
private func moveAttachments(from source: IndexSet, to destination: Int) {
|
||
|
draft.attachments.move(fromOffsets: source, toOffset: destination)
|
||
|
}
|
||
|
|
||
|
private func deleteAttachments(at indices: IndexSet) {
|
||
|
draft.attachments.remove(atOffsets: indices)
|
||
|
}
|
||
|
|
||
|
@MainActor
|
||
|
private func insertAttachments(at offset: Int, itemProviders: [NSItemProvider]) async {
|
||
|
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 }
|
||
|
self.draft.attachments.append(attachment)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func addImage() {
|
||
|
parent.config.presentAssetPicker?({ results in
|
||
|
Task {
|
||
|
await self.insertAttachments(at: self.draft.attachments.count, itemProviders: results.map(\.itemProvider))
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
private func addDrawing() {
|
||
|
parent.config.presentDrawing?(PKDrawing()) { drawing in
|
||
|
self.draft.attachments.append(DraftAttachment(data: .drawing(drawing)))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func togglePoll() {
|
||
|
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
|
||
|
|
||
|
withAnimation {
|
||
|
draft.poll = draft.poll == nil ? Draft.Poll() : 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 {
|
||
|
Group {
|
||
|
attachmentsList
|
||
|
|
||
|
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))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private var attachmentsList: some View {
|
||
|
ForEach(draft.attachments) { attachment in
|
||
|
ControllerView(controller: { AttachmentRowController(parent: controller.parent, attachment: attachment) })
|
||
|
.listRowInsets(EdgeInsets(top: cellPadding / 2, leading: cellPadding / 2, bottom: cellPadding / 2, trailing: cellPadding / 2))
|
||
|
.onDrag {
|
||
|
NSItemProvider(object: attachment)
|
||
|
}
|
||
|
}
|
||
|
.onMove(perform: controller.moveAttachments)
|
||
|
.onDelete(perform: controller.deleteAttachments)
|
||
|
.conditionally(controller.canAddAttachment) {
|
||
|
$0.onInsert(of: DraftAttachment.readableTypeIdentifiersForItemProvider, perform: { offset, providers in
|
||
|
Task {
|
||
|
await controller.insertAttachments(at: offset, itemProviders: providers)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
}
|