mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
The approval modal never appeared for a macOS sender: the receiver request reached the core and even fired its notification, but the modal stayed hidden. Root cause was observation, not presentation. `RootView` derived `approvals` and `messages` as `@ObservedObject` in `init` from a freshly built `AppGraph`. `init` runs on every view re-creation and each run makes a throwaway graph, so those observed objects were repointed to a dead `ApprovalCoordinator` that never receives core events — while the persisted `@StateObject graph` (and the models wired to it) kept the live one. Debug happened not to re-init the view, so it stayed on the live instance; release re-inits it, exposing the bug. Move the snackbar + approval modal into an `OverlayLayer` child view that takes the coordinator/messages as `@ObservedObject` and is constructed in `body` from the persisted `graph`, so the subscription is always against the live instances. While here: - Present the approval only after any open share/QR sheet has actually finished dismissing (macOS can't stack sheets), driven off the sheet's real `onDismiss` completion via a new `AdaptiveDrawer.onDismissed` hook and `SendModel.shareSheetsDismissed` — no wall-clock delay. - Move the list-level share-sheet state (`shareTargetId`) into `SendModel` so the approval flow can dismiss every share surface centrally. - Add a fallback: pending receiver rows in the Receivers panel now offer an Approve action (`SendModel.acceptReceiver`) alongside Refuse, for the case the modal didn't surface.
73 lines
2.1 KiB
Swift
73 lines
2.1 KiB
Swift
import SwiftUI
|
|
|
|
/// Presents modal content in a native sheet. On phones it uses medium/large
|
|
/// detents with a grabber; on wider layouts the sheet is form-sized. Content is
|
|
/// wrapped in a `NavigationStack` so it gets a native title bar + Close button.
|
|
struct AdaptiveDrawer<DrawerContent: View>: ViewModifier {
|
|
@Binding var isPresented: Bool
|
|
let windowClass: WindowClass
|
|
let onDismiss: () -> Void
|
|
/// Fired after the sheet's dismissal animation completes (as opposed to
|
|
/// `onDismiss`, which requests the close). Lets callers serialize a follow-up
|
|
/// sheet against this one's actual teardown instead of guessing a delay.
|
|
let onDismissed: (() -> Void)?
|
|
@ViewBuilder let drawerContent: () -> DrawerContent
|
|
|
|
func body(content: Content) -> some View {
|
|
content.sheet(
|
|
isPresented: Binding(get: { isPresented }, set: { if !$0 { onDismiss() } }),
|
|
onDismiss: onDismissed
|
|
) {
|
|
SheetChrome(onClose: onDismiss) { drawerContent() }
|
|
.modifier(PhoneDetents(enabled: windowClass == .phone))
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct PhoneDetents: ViewModifier {
|
|
let enabled: Bool
|
|
func body(content: Content) -> some View {
|
|
if enabled {
|
|
content
|
|
.presentationDetents([.medium, .large])
|
|
.presentationDragIndicator(.visible)
|
|
} else {
|
|
content.frame(minWidth: 460, minHeight: 480)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SheetChrome<Content: View>: View {
|
|
let onClose: () -> Void
|
|
@ViewBuilder let content: () -> Content
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView { content().padding(.top, 4) }
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button(String(localized: L10n.Button.close), action: onClose)
|
|
}
|
|
}
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func adaptiveDrawer<DrawerContent: View>(
|
|
isPresented: Binding<Bool>,
|
|
windowClass: WindowClass,
|
|
onDismiss: @escaping () -> Void,
|
|
onDismissed: (() -> Void)? = nil,
|
|
@ViewBuilder content: @escaping () -> DrawerContent
|
|
) -> some View {
|
|
modifier(AdaptiveDrawer(
|
|
isPresented: isPresented, windowClass: windowClass,
|
|
onDismiss: onDismiss, onDismissed: onDismissed, drawerContent: content
|
|
))
|
|
}
|
|
}
|