feat(tauri): add low-disk streamed install action
NEXT_STEPS item 1 called out that streamed install was still CLI-only because the Tauri app started the peer with no stream provider. Users can now choose an explicit "Low disk install" action from the game detail modal for remote-only games instead of taking the default archive-preserving download path. The GUI command queues a normal peer detail fetch first so the peer database has the file metadata needed for source validation. A small pending handoff in Tauri routes the resulting GotGameFiles event into StreamInstallGame instead of DownloadGameFiles, and clears that pending state on no-peer or download failure events. This keeps the existing download continuation untouched for the default action. The external unrar stream provider moved from the CLI harness into lanspread-peer so CLI and Tauri use the same implementation. Tauri resolves the bundled unrar sidecar path and injects that provider at peer startup; falling back to the noop provider keeps peer startup alive if the sidecar cannot be resolved, while the streamed install operation still fails safely. Test Plan: - just fmt - just test - just frontend-test - just clippy - just build - git diff --check Refs: NEXT_STEPS.md item 1
This commit is contained in:
@@ -5,7 +5,7 @@ import { StateChip } from '../StateChip';
|
||||
import { ActionButton } from '../ActionButton';
|
||||
|
||||
import { Game, InstallStatus } from '../../lib/types';
|
||||
import { deriveState, hasNewerLocalVersion, isInProgress } from '../../lib/gameState';
|
||||
import { canStreamInstall, deriveState, hasNewerLocalVersion, isInProgress } from '../../lib/gameState';
|
||||
import { formatBytes, formatEtiVersion, formatPlayers } from '../../lib/format';
|
||||
|
||||
interface Props {
|
||||
@@ -13,6 +13,7 @@ interface Props {
|
||||
thumbnailUrl: string | null;
|
||||
onClose: () => void;
|
||||
onPrimary: (game: Game) => void;
|
||||
onStreamInstall: (game: Game) => void;
|
||||
onUninstall: (game: Game) => void;
|
||||
onRemoveDownload: (game: Game) => void;
|
||||
onCancelDownload: (game: Game) => void;
|
||||
@@ -43,6 +44,7 @@ export const GameDetailModal = ({
|
||||
thumbnailUrl,
|
||||
onClose,
|
||||
onPrimary,
|
||||
onStreamInstall,
|
||||
onUninstall,
|
||||
onRemoveDownload,
|
||||
onCancelDownload,
|
||||
@@ -55,6 +57,7 @@ export const GameDetailModal = ({
|
||||
const canRemoveDownload = game.downloaded
|
||||
&& !game.installed
|
||||
&& !isInProgress(game.install_status);
|
||||
const showStreamInstall = canStreamInstall(game);
|
||||
const canViewFiles = game.downloaded
|
||||
|| game.installed
|
||||
|| game.install_status === InstallStatus.Downloading
|
||||
@@ -133,6 +136,17 @@ export const GameDetailModal = ({
|
||||
onClick={() => onPrimary(game)}
|
||||
onCancelDownload={onCancelDownload}
|
||||
/>
|
||||
{showStreamInstall && (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
title="Install without keeping archive files"
|
||||
onClick={() => onStreamInstall(game)}
|
||||
>
|
||||
<Icon.install />
|
||||
<span>Low disk install</span>
|
||||
</button>
|
||||
)}
|
||||
{game.installed && game.can_host_server === true && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface GameActions {
|
||||
play: (id: string) => Promise<void>;
|
||||
startServer: (id: string) => Promise<void>;
|
||||
install: (id: string) => Promise<void>;
|
||||
streamInstall: (id: string) => Promise<void>;
|
||||
update: (id: string) => Promise<void>;
|
||||
uninstall: (id: string) => Promise<void>;
|
||||
removeDownload: (id: string) => Promise<void>;
|
||||
@@ -68,6 +69,15 @@ export const useGameActions = (
|
||||
}
|
||||
}, [games, settings.language, settings.username]);
|
||||
|
||||
const streamInstall = useCallback(async (id: string) => {
|
||||
try {
|
||||
const success = await invoke<boolean>('stream_install_game', { id });
|
||||
if (success) games.markChecking(id);
|
||||
} catch (err) {
|
||||
console.error('stream_install_game failed:', err);
|
||||
}
|
||||
}, [games]);
|
||||
|
||||
const update = useCallback(async (id: string) => {
|
||||
try {
|
||||
const game = games.games.find(item => item.id === id);
|
||||
@@ -129,5 +139,15 @@ export const useGameActions = (
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { play, startServer, install, update, uninstall, removeDownload, cancelDownload, viewFiles };
|
||||
return {
|
||||
play,
|
||||
startServer,
|
||||
install,
|
||||
streamInstall,
|
||||
update,
|
||||
uninstall,
|
||||
removeDownload,
|
||||
cancelDownload,
|
||||
viewFiles,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -114,6 +114,12 @@ export const needsUpdate = (game: Game): boolean => {
|
||||
return (compareVersionStamps(game.eti_game_version, game.local_version) ?? 0) > 0;
|
||||
};
|
||||
|
||||
export const canStreamInstall = (game: Game): boolean =>
|
||||
!game.downloaded
|
||||
&& !game.installed
|
||||
&& game.peer_count > 0
|
||||
&& !isInProgress(game.install_status);
|
||||
|
||||
/** What pressing the card's main action button should do, given the state. */
|
||||
export type PrimaryAction = 'play' | 'install' | 'update' | 'download' | 'busy' | 'disabled';
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@ export const MainWindow = () => {
|
||||
thumbnailUrl={thumbnails.get(openGame.id)}
|
||||
onClose={() => setOpenGameId(null)}
|
||||
onPrimary={handlePrimary}
|
||||
onStreamInstall={(g) => actions.streamInstall(g.id)}
|
||||
onUninstall={handleUninstall}
|
||||
onRemoveDownload={handleRemoveDownload}
|
||||
onCancelDownload={(g) => actions.cancelDownload(g.id)}
|
||||
|
||||
Reference in New Issue
Block a user