Native macOS app for posting to Mastodon, WordPress (self-hosted), and Micro.blog. Features: global hotkey (Ctrl+Option+Cmd+T), multiple accounts with selection, image attachments (up to 4, drag-and-drop), floating panel UI, Keychain storage.
149 lines
6.1 KiB
Swift
149 lines
6.1 KiB
Swift
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)
|
|
}
|
|
}
|