mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(apple): native SwiftUI app for iOS and macOS
Add a native SwiftUI VniDrop app (Send/Receive/Settings) talking to the Rust core via generated UniFFI Swift bindings, plus the uniffi-bindgen helper crate. iOS uses a TabView, macOS a NavigationSplitView sidebar.
This commit is contained in:
66
apple/VniDrop/Core/ExternalInvitationController.swift
Normal file
66
apple/VniDrop/Core/ExternalInvitationController.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
import Foundation
|
||||
|
||||
let vniDropInvitationMimeType = "application/vnd.vnidrop.transfer"
|
||||
let vniDropInvitationExtension = "vnd"
|
||||
let maxVniDropInvitationBytes = 64 * 1024
|
||||
|
||||
/// Buffered ingress for invitation documents opened by the OS, ported from
|
||||
/// `ExternalInvitationController.kt`. Hosts can submit before the UI is attached
|
||||
/// during a cold launch; each document is consumed exactly once.
|
||||
@MainActor
|
||||
final class ExternalInvitationController: ObservableObject {
|
||||
/// Emits validated (or failed) invitations. The app-level receive workflow
|
||||
/// consumes each exactly once.
|
||||
private var continuation: AsyncStream<Result<String, Error>>.Continuation?
|
||||
lazy var invitations: AsyncStream<Result<String, Error>> = {
|
||||
AsyncStream { continuation in
|
||||
self.continuation = continuation
|
||||
}
|
||||
}()
|
||||
|
||||
func openInvitation(raw: String) {
|
||||
continuation?.yield(validateInvitation(raw))
|
||||
}
|
||||
|
||||
func reportOpenFailure(message: String) {
|
||||
continuation?.yield(.failure(InvitationError.message(message)))
|
||||
}
|
||||
}
|
||||
|
||||
enum InvitationError: LocalizedError {
|
||||
case empty
|
||||
case tooLarge
|
||||
case invalidEncoding
|
||||
case message(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .empty: return "The invitation is empty"
|
||||
case .tooLarge: return "The invitation is too large"
|
||||
case .invalidEncoding: return "The invitation is not valid text"
|
||||
case .message(let m): return m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateInvitation(_ raw: String) -> Result<String, Error> {
|
||||
if raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return .failure(InvitationError.empty)
|
||||
}
|
||||
if raw.utf8.count > maxVniDropInvitationBytes {
|
||||
return .failure(InvitationError.tooLarge)
|
||||
}
|
||||
return .success(raw)
|
||||
}
|
||||
|
||||
/// Decode invitation document bytes as strict UTF-8, ported from
|
||||
/// `decodeInvitationBytes`. Rejects payloads that are not lossless UTF-8.
|
||||
func decodeInvitationBytes(_ bytes: Data) throws -> String {
|
||||
guard !bytes.isEmpty else { throw InvitationError.empty }
|
||||
guard bytes.count <= maxVniDropInvitationBytes else { throw InvitationError.tooLarge }
|
||||
guard let text = String(data: bytes, encoding: .utf8), Data(text.utf8) == bytes else {
|
||||
throw InvitationError.invalidEncoding
|
||||
}
|
||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw InvitationError.empty }
|
||||
return text
|
||||
}
|
||||
Reference in New Issue
Block a user