feat(app): expose local sharing and verified transfers

Add a durable, acknowledged Local network sharing switch with fail-closed
hydration, serialized mutation, and redacted ephemeral-identity diagnostics.
Keep local Call-to-Play state available while gating every network action on the
effective sharing generation.

Render revisioned verification, invalid-source retry, and sticky source
exhaustion states. Preserve opaque attempt IDs through progress delivery so
out-of-order webview events cannot attach stale bytes to a successor transfer,
and keep terminal exhaustion visible after the last source departs.

Own listeners, native invokes, persistence, dialogs, and companion-window
creation through webview close. Late creation is settled and cleaned before the
parent realm is destroyed.

Test Plan:
- `just frontend-test` -- passed (91/91)
- `just build` -- passed with TypeScript, Vite, and release Tauri compilation
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `git diff --cached --check` -- passed
This commit is contained in:
ddidderr committed 2026-08-10 13:59:40 +02:00
1 parent 60fd7ba0c2
commit 71dbf27d8b
42 files changed
+7457 -2101

No files matched your search

@@ -1,28 +1,41 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import {
createThumbnailRequestKey,
startOwnedThumbnailGeneration,
ThumbnailRequestGeneration,
thumbnailIdsFromRequestKey,
} from '../lib/thumbnailRequests';
import { windowAsyncScope } from '../lib/asyncOwnership';
/**
* Lazy, per-id cache for cover thumbnails. Returns `null` until the value is
* known; returns the empty string when the backend has nothing for the id, so
* callers can fall back to the placeholder cover art.
* Per-id cache for cover thumbnails belonging to the supplied game ids.
* Returns `null` until the value is known; returns the empty string when the
* backend has nothing for the id, so callers can use the placeholder cover.
*/
export const useThumbnails = () => {
export const useThumbnails = (ids: readonly string[]) => {
const [thumbnails, setThumbnails] = useState<Map<string, string>>(new Map());
const pending = useRef<Set<string>>(new Set());
const thumbnailsRef = useRef(thumbnails);
const requestKey = createThumbnailRequestKey(ids);
useEffect(() => {
const generation = new ThumbnailRequestGeneration(
id => invoke<string>('get_game_thumbnail', { gameId: id }),
(id, url) => {
const next = new Map(thumbnailsRef.current).set(id, url);
thumbnailsRef.current = next;
setThumbnails(next);
},
);
const missingIds = thumbnailIdsFromRequestKey(requestKey)
.filter(id => !thumbnailsRef.current.has(id));
return startOwnedThumbnailGeneration(windowAsyncScope, generation, missingIds);
}, [requestKey]);
const get = useCallback((id: string): string | null => {
if (thumbnails.has(id)) return thumbnails.get(id) ?? '';
if (pending.current.has(id)) return null;
pending.current.add(id);
invoke<string>('get_game_thumbnail', { gameId: id })
.then(url => {
pending.current.delete(id);
setThumbnails(prev => new Map(prev).set(id, url));
})
.catch(() => {
pending.current.delete(id);
setThumbnails(prev => new Map(prev).set(id, ''));
});
return null;
}, [thumbnails]);