ce55be4a-fb5f-4981-b507-0f4.../Sources/ui/ExpandedView.swift
徐翔宇 c4080b5cb9 v1.0.0: initial AirDrop plugin for MioIsland
Quick-access AirDrop from the notch panel. Tap the card body, pick
files via NSOpenPanel, hand off to NSSharingService(.sendViaAirDrop).
No private APIs, no entitlements, no network — just a thin wrapper
around the same AirDrop API Finder uses.

Design constraint: drag-and-drop is intentionally not implemented.
MioIsland's notch panel auto-collapses on click-outside, which
breaks the Cmd-Tab-to-Finder / grab / drag-back workflow before
the drop target reaches. Tap-to-choose is rock solid by comparison.

Requires MioIsland host v2.2.0+ (panel size clamp floor was
lowered to 120pt in that release).
2026-04-22 08:20:47 +08:00

136 lines
5.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ExpandedView.swift
// MioIsland AirDrop Plugin
//
// Main panel rendered when the user opens the AirDrop plugin in
// Mio Island. Card-in-container layout: top transparent strip for
// the notch + floating back chip, themed card below with the drop
// zone + "choose files" button.
//
import AppKit
import SwiftUI
struct ExpandedView: View {
@ObservedObject private var state: AirDropState = .shared
private static let ink = Color.white
private static let base = Color(red: 0x0A / 255.0, green: 0x0A / 255.0, blue: 0x0A / 255.0)
// MARK: - Body
var body: some View {
Group {
if HostVersionCheck.isOK() {
playingCard
} else {
hostUpgradeCard
}
}
.animation(.easeInOut(duration: 0.2), value: state.phase)
}
// MARK: - Main panel (when host version OK)
private var playingCard: some View {
// Layout (panel 440×280):
// 40 top transparent (notch + back chevron area)
// 20 margin
// 180 card (14 pad + ~135 content + 14 pad)
// 20 margin
// = 260pt used, 20pt buffer for SwiftUI rounding.
VStack(spacing: 0) {
Color.clear.frame(height: 40) // notch strip
Color.clear.frame(height: 20) // margin top
cardContent
.padding(EdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Self.base.opacity(0.88))
)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.white.opacity(0.12), lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
Color.clear.frame(height: 20) // margin bottom
}
.padding(.horizontal, 16)
}
private var cardContent: some View {
VStack(spacing: 10) {
// Header row just the title. The whole DropZoneView
// below is the tap target now; no redundant chip button.
HStack(alignment: .center, spacing: 6) {
Image(systemName: "airplayaudio")
.font(.system(size: 13, weight: .semibold))
.foregroundColor(Self.ink.opacity(0.9))
Text(L10n.title)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Self.ink.opacity(0.95))
Spacer()
}
// Big tap target for the whole card body.
DropZoneView()
}
}
// MARK: - Host upgrade card
private var hostUpgradeCard: some View {
VStack(spacing: 0) {
Color.clear.frame(height: 40)
Color.clear.frame(height: 20)
VStack(spacing: 12) {
HStack(spacing: 14) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 24, weight: .regular))
.foregroundColor(.orange)
.frame(width: 48, height: 48)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color.orange.opacity(0.12))
)
VStack(alignment: .leading, spacing: 4) {
Text(L10n.hostUpgradeTitle)
.font(.system(size: 14, weight: .semibold))
.foregroundColor(Self.ink.opacity(0.9))
Text(L10n.hostUpgradeHint)
.font(.system(size: 11))
.foregroundColor(Self.ink.opacity(0.55))
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Text("Host: v\(HostVersionCheck.hostVersion()) · required: v\(HostVersionCheck.minHost)+")
.font(.system(size: 10, design: .monospaced))
.foregroundColor(Self.ink.opacity(0.4))
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(EdgeInsets(top: 18, leading: 16, bottom: 18, trailing: 16))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Self.base.opacity(0.88))
)
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.white.opacity(0.12), lineWidth: 0.5)
)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
Color.clear.frame(height: 20)
}
.padding(.horizontal, 16)
}
}