Add PWA, CLI daemon, deploy tooling, and full documentation
PWA (pwa/): - Ephemeral QR pairing and trusted device direct reconnect (no QR re-scan) - Known devices tab as default; pairRoomName() derives deterministic room - In-app QR scanner via native BarcodeDetector API - Multi-file send/receive with offer queue and Accept all - Auto-download on receipt, real-time send/receive progress bars - Trusted peer nicknames, rename, forget - Terminal aesthetic: #080808 bg, #00e87a accent, JetBrains Mono - manifest.json corrected (SVG icon, theme_color, share_target) - Apple PWA meta tags for home screen install CLI (cli/): - flit send / flit recv: ephemeral one-shot transfer with terminal QR - flit daemon: persistent receiver for trusted peers from ~/.flit/daemon.toml - TOML config: signal_url, turn_url, download_dir, [[peers]] - One goroutine per peer, exponential backoff reconnect (2s→30s cap) - transport.PairRoomName() and Session.OnDisconnected added - Anchor/TURN config via FLIT_SIGNAL_URL / FLIT_TURN_URL env vars (no hardcoded URLs) Deploy: - build-pwa.sh, deploy-pwa.sh / serve-pwa.sh templates in README (gitignored) - .gitea/workflows/build.yml: PWA build on v* tag, Gitea release artifact - pwa/public/config.js gitignored; config.js.example committed - .env.example for CLI env vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -222,14 +222,18 @@ function gatherComplete(pc: RTCPeerConnection): Promise<void> {
|
||||
export interface FileOfferEvent { peer: string; xid: string; name: string; size: number }
|
||||
export interface FileProgressEvent { peer: string; xid: string; name: string; received: number; total: number }
|
||||
export interface FileRecvEvent { peer: string; name: string; size: number; url: string }
|
||||
export interface FileSendProgressEvent { peer: string; xid: string; name: string; sent: number; total: number }
|
||||
export interface FileSentEvent { peer: string; xid: string; name: string }
|
||||
|
||||
type PeerEvents = {
|
||||
connected: (verified: boolean) => void
|
||||
connected: (verified: boolean, peerId: string) => void
|
||||
status: (state: RTCPeerConnectionState) => void
|
||||
fileOffer: (e: FileOfferEvent) => void
|
||||
fileProgress: (e: FileProgressEvent) => void
|
||||
fileRecv: (e: FileRecvEvent) => void
|
||||
fileCancelled: (xid: string) => void
|
||||
fileSendProgress: (e: FileSendProgressEvent) => void
|
||||
fileSent: (e: FileSentEvent) => void
|
||||
}
|
||||
|
||||
export class PeerConn {
|
||||
@@ -391,7 +395,7 @@ export class PeerConn {
|
||||
|
||||
if (m['type'] === 'hello') {
|
||||
this.verified = m['id'] === this.peerId && this.peerAuthed
|
||||
this.events.connected?.(this.verified)
|
||||
this.events.connected?.(this.verified, this.peerId)
|
||||
} else if (m['type'] === 'file-offer') {
|
||||
this.events.fileOffer?.({ peer: this.peerId, xid: m['xid'] as string, name: m['name'] as string, size: m['size'] as number })
|
||||
} else if (m['type'] === 'file-accept') {
|
||||
@@ -436,8 +440,14 @@ export class PeerConn {
|
||||
while (dc.bufferedAmount > 1024 * 1024) await new Promise(r => setTimeout(r, 10))
|
||||
dc.send(buf.slice(offset, offset + CHUNK))
|
||||
offset += CHUNK
|
||||
this.events.fileSendProgress?.({ peer: this.peerId, xid, name: file.name, sent: offset, total: buf.byteLength })
|
||||
// Yield to the event loop every chunk so the browser can paint
|
||||
// progress — without this, small files finish in one synchronous
|
||||
// tick and the UI never shows intermediate state.
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
}
|
||||
dc.close()
|
||||
this.events.fileSent?.({ peer: this.peerId, xid, name: file.name })
|
||||
}
|
||||
|
||||
private _dc(obj: object) {
|
||||
|
||||
Reference in New Issue
Block a user