mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-14 14:19:57 +02:00
Adds the native SwiftUI Saved Devices experience on top of the production saved-device core, as a top-level destination in the iOS tab bar and the macOS sidebar. Core seam: - App-facing saved-device domain models mirroring core/SavedDeviceModels.kt, with lifecycle helpers (canReceive/canResume/canCancel/canDelete) so views never hand-roll state checks. - 21 gateway methods through CoreGateway/CoreRepository with UniFFI mapping. cancelTargetedTransfer, forgetSavedDevice and blockDevice run off the serial lane: each must reach the core while a targeted receive is blocking it. - Payload-free pairingChanged/targetedTransferChanged signals, dispatched before the numeric-transferId guard since saved-device events identify their subject by peer endpoint or a string transfer id. Experience: - Screen lists saved devices and outstanding consent requests only; the global targeted-transfer history stays out, reachable per device. - Details as a sheet with detents on compact layouts and a native inspector on macOS, owning Send, label, forget/block and that device's transfers. - Label editing is transactional: the draft and editor survive a failed write, conflicting actions are refused while saving, and the editor closes only after the core confirms. - Pairing and targeted-offer consent hosted at the app root, answerable from any tab and suppressed while a transfer approval is up. Dismissing a pairing prompt suppresses locally without consuming the single-use eligibility; dismissing an offer declines it, since an unanswered offer holds a slot in the core's bounded per-sender queue. - Targeted send reuses the invitation composer's affordances with file, folder, rename, replace and cleanup parity. Picker copies are released on replace/remove/clear/cancel and after a successful create, but kept after a failure so retry does not require re-picking. - Notifications for pairing requests and offers (withdrawn once answered) and for terminal targeted transfers. Wording follows direction: on the sending device the peer finished receiving, not us. Localization: - Widens 52 saved-device keys from kmp-only to both platforms. - Five keys carried a literal %1$s with no declared args, which Compose renders positionally but the Apple generator emits as a plain constant, leaking the placeholder into the UI. They now use named args; Compose output is byte-identical. - Adds targeted_offer_title/body. Reusing the invitation approval copy stated the roles backwards, announcing the sender as the receiver. Also surfaces core startup failures: the startup overlay is drawn above the snackbar host, so a failed initialize() was indistinguishable from an app that never finished loading. AppModel now keeps the reason, logs it, and the overlay shows it with a retry, plus the technical detail in DEBUG builds. Send and receive between two devices is verified only partially; a missing endpoint-identity credential currently blocks startup on the test device.
76 lines
2.9 KiB
Swift
76 lines
2.9 KiB
Swift
import Foundation
|
|
import VnidropCore
|
|
|
|
/// A file/folder selected for sharing, ported from `PickedShareFile` in
|
|
/// `core/FilePicker.kt`.
|
|
struct PickedShareFile: Equatable, Identifiable, Sendable {
|
|
let value: String
|
|
let displayName: String
|
|
var sizeBytes: UInt64? = nil
|
|
var thumbnailData: Data? = nil
|
|
/// App-owned picker copy that may be deleted after import or abandonment.
|
|
var isTemporaryCopy: Bool = false
|
|
/// When true, `value` is a directory (path or security-scoped folder URL).
|
|
var isDirectory: Bool = false
|
|
/// macOS sandbox: a security-scoped bookmark captured at pick time so access to
|
|
/// `value` can be re-acquired when the core imports the file (the picker's own
|
|
/// scope ends immediately). Nil on iOS (which copies into the container instead).
|
|
var securityScopeBookmark: Data? = nil
|
|
|
|
var id: String { value }
|
|
}
|
|
|
|
/// Receive-destination and share-source platform bridge, ported from
|
|
/// `core/FileSystemService.kt` and its iOS/desktop actuals.
|
|
@MainActor
|
|
protocol FileSystemService {
|
|
var supportsCustomReceiveFolders: Bool { get }
|
|
|
|
func defaultReceiveFolder() -> ReceiveFolder
|
|
func effectiveReceiveFolder(_ configured: ReceiveFolder) -> ReceiveFolder
|
|
func validateReceiveFolder(_ folder: ReceiveFolder) async -> FolderAccessStatus
|
|
func canRevealReceiveFolder(_ folder: ReceiveFolder) -> Bool
|
|
func revealReceiveFolder(_ folder: ReceiveFolder) async -> Result<Void, Error>
|
|
/// Releases only app-owned picker copies; never deletes original user sources.
|
|
func discardPickedFiles(_ files: [PickedShareFile]) async
|
|
/// Imports a picked selection, either as an invitation or straight to a
|
|
/// remembered device. One entry point so the platform's security-scoped
|
|
/// access handling covers both.
|
|
func sharePickedFiles(
|
|
repository: CoreGateway,
|
|
files: [PickedShareFile],
|
|
transferName: String,
|
|
senderName: String,
|
|
destination: ShareDestination
|
|
) async -> Result<Share, Error>
|
|
/// Sends a picked selection straight to one saved device. Separate from
|
|
/// `sharePickedFiles` because a targeted transfer is its own domain with its
|
|
/// own result type — it is not an access mode on an invitation share.
|
|
func sendPickedFilesToSavedDevice(
|
|
repository: CoreGateway,
|
|
files: [PickedShareFile],
|
|
transferName: String,
|
|
receiverEndpointId: String
|
|
) async -> Result<TargetedTransferModel, Error>
|
|
}
|
|
|
|
extension FileSystemService {
|
|
var supportsCustomReceiveFolders: Bool { true }
|
|
|
|
func effectiveReceiveFolder(_ configured: ReceiveFolder) -> ReceiveFolder {
|
|
supportsCustomReceiveFolders ? configured : defaultReceiveFolder()
|
|
}
|
|
|
|
func canRevealReceiveFolder(_ folder: ReceiveFolder) -> Bool { false }
|
|
|
|
func revealReceiveFolder(_ folder: ReceiveFolder) async -> Result<Void, Error> {
|
|
.failure(InvitationError.unsupportedOperation)
|
|
}
|
|
|
|
func discardPickedFiles(_ files: [PickedShareFile]) async {}
|
|
}
|
|
|
|
extension ReceiveFolder {
|
|
var isFileSystemPath: Bool { kind == .fileSystemPath }
|
|
}
|