mirror of
https://github.com/carey314/mio-plugin-airdrop.git
synced 2026-06-11 03:54:33 +00:00
84 lines
2.8 KiB
Swift
84 lines
2.8 KiB
Swift
|
|
//
|
||
|
|
// AirDropState.swift
|
||
|
|
// MioIsland AirDrop Plugin
|
||
|
|
//
|
||
|
|
// Shared state + send coordinator. Single source of truth for what
|
||
|
|
// the panel should render. Not strictly "ObservableObject" heavy —
|
||
|
|
// we have only three phases, but using @Published keeps the door
|
||
|
|
// open for future "sending progress" UI.
|
||
|
|
//
|
||
|
|
|
||
|
|
import AppKit
|
||
|
|
import Combine
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class AirDropState: ObservableObject {
|
||
|
|
static let shared = AirDropState()
|
||
|
|
|
||
|
|
enum Phase: Equatable {
|
||
|
|
case idle
|
||
|
|
case showingPicker
|
||
|
|
case sent(count: Int)
|
||
|
|
case error(message: String)
|
||
|
|
}
|
||
|
|
|
||
|
|
@Published var phase: Phase = .idle
|
||
|
|
|
||
|
|
/// Last completed send — used to animate a brief "✓ 已发送" toast.
|
||
|
|
@Published private(set) var lastSendAt: Date?
|
||
|
|
|
||
|
|
/// Called by UI when user drops files or picks from NSOpenPanel.
|
||
|
|
/// Kicks off NSSharingService (opens the AirDrop chooser window)
|
||
|
|
/// and resets phase when the user dismisses the chooser.
|
||
|
|
func send(files: [URL]) {
|
||
|
|
let urls = files.filter { $0.isFileURL }
|
||
|
|
guard !urls.isEmpty else { return }
|
||
|
|
|
||
|
|
phase = .showingPicker
|
||
|
|
|
||
|
|
AirDropService.perform(files: urls) { [weak self] result in
|
||
|
|
guard let self else { return }
|
||
|
|
Task { @MainActor in
|
||
|
|
switch result {
|
||
|
|
case .success:
|
||
|
|
self.phase = .sent(count: urls.count)
|
||
|
|
self.lastSendAt = Date()
|
||
|
|
// Drop back to idle after 2.5s so the drop zone is
|
||
|
|
// usable for the next file without a click.
|
||
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in
|
||
|
|
guard let self else { return }
|
||
|
|
if case .sent = self.phase {
|
||
|
|
self.phase = .idle
|
||
|
|
}
|
||
|
|
}
|
||
|
|
case .failure(let err):
|
||
|
|
self.phase = .error(message: err.localizedDescription)
|
||
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||
|
|
guard let self else { return }
|
||
|
|
if case .error = self.phase {
|
||
|
|
self.phase = .idle
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Open NSOpenPanel to let the user pick files manually, then send.
|
||
|
|
func chooseAndSend() {
|
||
|
|
let panel = NSOpenPanel()
|
||
|
|
panel.allowsMultipleSelection = true
|
||
|
|
panel.canChooseDirectories = true
|
||
|
|
panel.canChooseFiles = true
|
||
|
|
panel.message = L10n.chooseFilesTitle
|
||
|
|
panel.prompt = L10n.choose
|
||
|
|
|
||
|
|
// Run modal on the main thread so it doesn't race the drop zone.
|
||
|
|
panel.begin { [weak self] response in
|
||
|
|
guard response == .OK else { return }
|
||
|
|
self?.send(files: panel.urls)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|