mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
Approval modal: since the Share/QR sheet auto-opens after creating a transfer, it is always up when a receiver request arrives, and macOS silently drops a sheet presented while another is still dismissing — so the approval sheet never appeared. Drive the approval sheet from explicit state (not a constant binding) and, on macOS, defer its presentation one dismiss-beat after closing the Share/QR sheet so the hand-off is serialized. Still a non-dismissable sheet; iOS timing unchanged. Sandboxed file sharing: the macOS picker released its security scope immediately, so the core's later import failed with EPERM under the App Store sandbox (the non-sandboxed .dmg was unaffected). Capture a security-scoped bookmark at pick time and re-acquire access across shareFiles() — during which the core imports the bytes — mirroring the receive-folder scoped-access pattern.
79 lines
2.6 KiB
Swift
79 lines
2.6 KiB
Swift
import SwiftUI
|
|
import SFSafeSymbols
|
|
|
|
/// Non-dismissable receiver-approval modal, presented as a native sheet that can't
|
|
/// be swiped away. The endpoint id is the trusted identity; display names are
|
|
/// peer-provided.
|
|
struct ApprovalModalHost: View {
|
|
/// Driven by the host so presentation can be deferred until any competing sheet
|
|
/// (the Share/QR drawer) has finished dismissing — macOS silently drops a sheet
|
|
/// presented while another is still animating out.
|
|
@Binding var isPresented: Bool
|
|
let state: ApprovalState
|
|
let onAccept: (String) -> Void
|
|
let onRefuse: (String) -> Void
|
|
|
|
var body: some View {
|
|
Color.clear
|
|
.sheet(isPresented: $isPresented) {
|
|
if let request = state.current {
|
|
ApprovalSheet(state: state, request: request, onAccept: onAccept, onRefuse: onRefuse)
|
|
.interactiveDismissDisabled(true)
|
|
.modifier(ApprovalDetents())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ApprovalDetents: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
#if os(iOS)
|
|
content.presentationDetents([.medium])
|
|
#else
|
|
content.frame(minWidth: 420, minHeight: 320)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private struct ApprovalSheet: View {
|
|
let state: ApprovalState
|
|
let request: PendingApproval
|
|
let onAccept: (String) -> Void
|
|
let onRefuse: (String) -> Void
|
|
|
|
var body: some View {
|
|
let busy = state.respondingIds.contains(request.id)
|
|
let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice)
|
|
VStack(spacing: 16) {
|
|
Image(systemSymbol: .checkmarkShieldFill)
|
|
.font(.system(size: 44))
|
|
.foregroundStyle(.tint)
|
|
.padding(.top, 12)
|
|
Text(String(localized: L10n.Approval.connectionRequest))
|
|
.font(.title2).fontWeight(.semibold)
|
|
Text(L10n.Approval.requestBody(receiver: receiver, transferName: request.transferName))
|
|
.multilineTextAlignment(.center)
|
|
Text(L10n.Approval.endpointId(deviceId: request.remoteEndpointId))
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
if state.pending.count > 1 {
|
|
Text(L10n.Approval.pendingCount(count: state.pending.count))
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
Spacer(minLength: 0)
|
|
if busy { ProgressView() }
|
|
VStack(spacing: 10) {
|
|
Button(action: { onAccept(request.id) }) {
|
|
Text(String(localized: L10n.Button.approve)).frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.borderedProminent).controlSize(.large).disabled(busy)
|
|
Button(role: .destructive, action: { onRefuse(request.id) }) {
|
|
Text(String(localized: L10n.Button.refuse)).frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(.bordered).controlSize(.large).disabled(busy)
|
|
}
|
|
}
|
|
.padding(24)
|
|
}
|
|
}
|