142 lines
5.1 KiB
Swift
142 lines
5.1 KiB
Swift
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
public struct DropZoneView: View {
|
|
@State private var isDragging = false
|
|
@State private var uploadResult: UploadResult?
|
|
@State private var error: String?
|
|
@State private var isUploading = false
|
|
@State private var uploadProgress: Double = 0
|
|
|
|
public let uploadManager: UploadManager
|
|
|
|
public init(uploadManager: UploadManager) {
|
|
self.uploadManager = uploadManager
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(spacing: 20) {
|
|
Image(systemName: "arrow.up.doc.fill")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(isDragging ? .blue : .secondary)
|
|
|
|
Text("Drop files here")
|
|
.font(.title2)
|
|
|
|
if isUploading {
|
|
ProgressView(value: uploadProgress)
|
|
.frame(width: 180)
|
|
Text("\(Int(uploadProgress * 100))%")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} else if let result = uploadResult {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(.green)
|
|
|
|
Text("Uploaded \(result.filename)")
|
|
.font(.headline)
|
|
|
|
Text("URL copied to clipboard")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(result.url.absoluteString)
|
|
.font(.caption2)
|
|
.foregroundColor(.blue)
|
|
.lineLimit(1)
|
|
.truncationMode(.middle)
|
|
.padding(.horizontal)
|
|
}
|
|
} else if let error = error {
|
|
VStack(spacing: 8) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(.red)
|
|
|
|
Text("Upload failed")
|
|
.font(.headline)
|
|
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.frame(width: 300, height: 250)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(isDragging ? Color.blue.opacity(0.1) : Color.secondary.opacity(0.1))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.strokeBorder(isDragging ? Color.blue : Color.secondary.opacity(0.3), style: StrokeStyle(lineWidth: 2, dash: [8]))
|
|
)
|
|
)
|
|
.onDrop(of: [.fileURL], isTargeted: $isDragging) { providers in
|
|
guard let provider = providers.first else { return false }
|
|
|
|
_ = provider.loadObject(ofClass: URL.self) { url, error in
|
|
guard let url = url else { return }
|
|
|
|
Task {
|
|
await MainActor.run {
|
|
isUploading = true
|
|
uploadProgress = 0
|
|
self.error = nil
|
|
self.uploadResult = nil
|
|
}
|
|
|
|
do {
|
|
let result = try await uploadFileResolvingDuplicates(url, onProgress: { progress in
|
|
Task { @MainActor in
|
|
uploadProgress = min(max(progress, 0), 1)
|
|
}
|
|
})
|
|
await MainActor.run {
|
|
uploadResult = result
|
|
isUploading = false
|
|
uploadProgress = 0
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
self.error = error.localizedDescription
|
|
isUploading = false
|
|
uploadProgress = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
private func uploadFileResolvingDuplicates(
|
|
_ url: URL,
|
|
onProgress: UploadProgressHandler?
|
|
) async throws -> UploadResult {
|
|
var duplicateAction: UploadDuplicateAction?
|
|
|
|
while true {
|
|
do {
|
|
return try await uploadManager.uploadFile(
|
|
url,
|
|
duplicateAction: duplicateAction,
|
|
onProgress: onProgress
|
|
)
|
|
} catch let error as APIError where error.isUploadConflict {
|
|
guard let action = promptForUploadDuplicate(error, filename: url.lastPathComponent) else {
|
|
throw UploadCancelledError()
|
|
}
|
|
duplicateAction = action
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
// This preview won't work without a real UploadManager
|
|
Text("DropZoneView Preview")
|
|
}
|