mirror of
https://github.com/carey314/mio-plugin-airdrop.git
synced 2026-06-11 03:54:33 +00:00
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).
136 lines
5.0 KiB
Swift
136 lines
5.0 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|