ce55be4a-fb5f-4981-b507-0f4.../Sources/ui/DropZoneView.swift

130 lines
4.0 KiB
Swift
Raw Normal View History

//
// DropZoneView.swift
// MioIsland AirDrop Plugin
//
// Click-to-choose zone at the center of the panel. Tapping anywhere
// inside opens NSOpenPanel user picks files NSSharingService
// fires the AirDrop chooser sheet.
//
// Drag-and-drop was intentionally removed: macOS Dynamic Island
// panels auto-collapse when the user clicks outside them, which
// prevents the Cmd-Tab Finder grab file drag back workflow.
// Rather than fight the host's event routing, we commit to the
// click-to-choose path which is rock-solid.
//
import SwiftUI
struct DropZoneView: View {
@ObservedObject var state: AirDropState = .shared
private static let lime = Color(
red: 0xCA / 255.0,
green: 0xFF / 255.0,
blue: 0x00 / 255.0
)
private static let ink = Color.white
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(fillColor)
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(strokeColor, lineWidth: 1)
centerContent
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.onTapGesture {
guard case .idle = state.phase else { return }
state.chooseAndSend()
}
.animation(.easeInOut(duration: 0.25), value: state.phase)
}
// MARK: - Reactive styling
private var fillColor: Color {
switch state.phase {
case .sent: return Self.lime.opacity(0.14)
case .showingPicker: return Color.white.opacity(0.08)
case .error: return Color.red.opacity(0.08)
case .idle: return Color.white.opacity(0.04)
}
}
private var strokeColor: Color {
switch state.phase {
case .sent: return Self.lime.opacity(0.9)
case .showingPicker: return Self.lime.opacity(0.6)
case .error: return Color.red.opacity(0.7)
case .idle: return Color.white.opacity(0.18)
}
}
// MARK: - Center content (icon + label)
@ViewBuilder
private var centerContent: some View {
VStack(spacing: 8) {
Image(systemName: iconName)
.font(.system(size: 32, weight: .light))
.foregroundColor(iconColor)
Text(primaryText)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(primaryColor)
.multilineTextAlignment(.center)
if let sub = secondaryText {
Text(sub)
.font(.system(size: 11))
.foregroundColor(Self.ink.opacity(0.5))
}
}
.padding(.horizontal, 20)
}
private var iconName: String {
switch state.phase {
case .sent: return "checkmark.circle.fill"
case .error: return "exclamationmark.triangle.fill"
case .showingPicker: return "airplayaudio"
case .idle: return "airplayaudio"
}
}
private var iconColor: Color {
switch state.phase {
case .sent: return Self.lime
case .error: return Color.red.opacity(0.8)
case .showingPicker: return Self.ink.opacity(0.85)
case .idle: return Self.ink.opacity(0.7)
}
}
private var primaryText: String {
switch state.phase {
case .sent(let n): return L10n.sentCount(n)
case .error(let m): return m
case .showingPicker: return L10n.opening
case .idle: return L10n.chooseFiles
}
}
private var primaryColor: Color {
switch state.phase {
case .sent: return Self.lime
case .error: return Color.red.opacity(0.9)
default: return Self.ink.opacity(0.85)
}
}
private var secondaryText: String? {
guard case .idle = state.phase else { return nil }
return L10n.clickToChoose
}
}