import SwiftUI import UniformTypeIdentifiers struct ImageAttachmentArea: View { @Binding var attachedImages: [ImageAttachment] @State private var showFilePicker = false @State private var isDropTargeted = false var body: some View { VStack(spacing: 8) { // Thumbnail grid if !attachedImages.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(attachedImages) { attachment in ZStack(alignment: .topTrailing) { Image(nsImage: attachment.image) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 72, height: 72) .clipShape(RoundedRectangle(cornerRadius: 6)) Button { attachedImages.removeAll { $0.id == attachment.id } } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 16)) .foregroundStyle(.white) .shadow(color: .black.opacity(0.5), radius: 2) } .buttonStyle(.plain) .offset(x: 4, y: -4) } } } } } // Drop zone if attachedImages.count < AppState.maxImages { ZStack { RoundedRectangle(cornerRadius: 8) .strokeBorder( isDropTargeted ? Color.accentColor : Color.secondary.opacity(0.3), style: StrokeStyle(lineWidth: 1.5, dash: [6]) ) .frame(height: 44) .background( (isDropTargeted ? Color.accentColor.opacity(0.08) : Color.clear) .clipShape(RoundedRectangle(cornerRadius: 8)) ) HStack(spacing: 4) { Image(systemName: "photo.on.rectangle.angled") .font(.caption) Text("Drop images or") .font(.caption) Button("browse") { showFilePicker = true } .font(.caption) .buttonStyle(.link) Text("(\(attachedImages.count)/\(AppState.maxImages))") .font(.caption) .foregroundStyle(.secondary) } } .onDrop(of: [.image, .fileURL], isTargeted: $isDropTargeted) { providers in handleDrop(providers) } } } .fileImporter( isPresented: $showFilePicker, allowedContentTypes: [.png, .jpeg, .gif, .webP], allowsMultipleSelection: true ) { result in handleFileImport(result) } } private func handleDrop(_ providers: [NSItemProvider]) -> Bool { let remaining = AppState.maxImages - attachedImages.count for provider in providers.prefix(remaining) { if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier) { data, _ in guard let data = data as? Data, let url = URL(dataRepresentation: data, relativeTo: nil), let image = NSImage(contentsOf: url), let imageData = imageDataFrom(url: url) else { return } DispatchQueue.main.async { if attachedImages.count < AppState.maxImages { attachedImages.append( ImageAttachment( image: image, data: imageData, filename: url.lastPathComponent ) ) } } } } else if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { provider.loadItem(forTypeIdentifier: UTType.image.identifier) { data, _ in guard let data = data as? Data, let image = NSImage(data: data) else { return } DispatchQueue.main.async { if attachedImages.count < AppState.maxImages { attachedImages.append( ImageAttachment( image: image, data: data, filename: "image.jpg" ) ) } } } } } return true } private func handleFileImport(_ result: Result<[URL], Error>) { guard case .success(let urls) = result else { return } let remaining = AppState.maxImages - attachedImages.count for url in urls.prefix(remaining) { guard url.startAccessingSecurityScopedResource() else { continue } defer { url.stopAccessingSecurityScopedResource() } guard let image = NSImage(contentsOf: url), let data = imageDataFrom(url: url) else { continue } attachedImages.append( ImageAttachment( image: image, data: data, filename: url.lastPathComponent ) ) } } private func imageDataFrom(url: URL) -> Data? { try? Data(contentsOf: url) } }