diff --git a/NEXT_STEPS.md b/NEXT_STEPS.md index 217cd14..4bdd410 100644 --- a/NEXT_STEPS.md +++ b/NEXT_STEPS.md @@ -54,11 +54,12 @@ product-ready. The peer-cli harness now exposes `cancel-download` so cancellation scenarios exercise the same runtime path as the GUI. -7. **Clean product semantics** +7. **Done — Clean product semantics** - Decide how the UI labels this state. It is installed but not downloaded, so - “Local only” is technically correct, but users may need a clear affordance - like “Installed, not shareable”. + The UI now keeps streamed installs in the installed visual state while making + the sharing limitation explicit: cards show `Not shareable`, and the detail + modal status shows `Installed, not shareable`. Downloaded-and-installed games + keep the normal `Installed` label. My recommended next slice: make the provider abstraction final-ish, then implement a real one-pass provider. Everything else builds cleanly on that. diff --git a/crates/lanspread-tauri-deno-ts/src/components/StateChip.tsx b/crates/lanspread-tauri-deno-ts/src/components/StateChip.tsx index 9c49213..214b563 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/StateChip.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/StateChip.tsx @@ -1,13 +1,5 @@ import { Game } from '../lib/types'; -import { deriveState } from '../lib/gameState'; - -const LABELS: Record = { - installed: 'Installed', - local: 'Local', - downloading: 'Downloading', - busy: 'Working', - none: '', -}; +import { deriveState, stateChipLabel } from '../lib/gameState'; interface Props { game: Game; @@ -17,7 +9,7 @@ interface Props { export const StateChip = ({ game, showNone = false }: Props) => { const state = deriveState(game); - const label = LABELS[state] ?? ''; + const label = stateChipLabel(game); if (!label && !showNone) return null; return (
diff --git a/crates/lanspread-tauri-deno-ts/src/components/modals/GameDetailModal.tsx b/crates/lanspread-tauri-deno-ts/src/components/modals/GameDetailModal.tsx index b5d9693..117ba36 100644 --- a/crates/lanspread-tauri-deno-ts/src/components/modals/GameDetailModal.tsx +++ b/crates/lanspread-tauri-deno-ts/src/components/modals/GameDetailModal.tsx @@ -5,7 +5,7 @@ import { StateChip } from '../StateChip'; import { ActionButton } from '../ActionButton'; import { Game, InstallStatus } from '../../lib/types'; -import { canStreamInstall, deriveState, hasNewerLocalVersion, isInProgress } from '../../lib/gameState'; +import { canStreamInstall, gameStatusLabel, hasNewerLocalVersion, isInProgress } from '../../lib/gameState'; import { formatBytes, formatEtiVersion, formatPlayers } from '../../lib/format'; interface Props { @@ -29,16 +29,6 @@ const tagsFromGame = (game: Game): string[] => { return tags; }; -const statusLabelFor = (game: Game): string => { - switch (deriveState(game)) { - case 'installed': return 'Installed'; - case 'local': return 'Downloaded'; - case 'downloading': return 'Downloading'; - case 'busy': return 'Working…'; - case 'none': return 'Not downloaded'; - } -}; - export const GameDetailModal = ({ game, thumbnailUrl, @@ -115,7 +105,7 @@ export const GameDetailModal = ({
Status
-
{statusLabelFor(game)}
+
{gameStatusLabel(game)}
diff --git a/crates/lanspread-tauri-deno-ts/src/lib/gameState.ts b/crates/lanspread-tauri-deno-ts/src/lib/gameState.ts index a041f19..ec7a3a2 100644 --- a/crates/lanspread-tauri-deno-ts/src/lib/gameState.ts +++ b/crates/lanspread-tauri-deno-ts/src/lib/gameState.ts @@ -82,6 +82,35 @@ export const deriveState = (game: Game): DerivedState => { return 'none'; }; +export const isInstalledNotShareable = (game: Game): boolean => + game.installed && !game.downloaded; + +export const stateChipLabel = (game: Game): string => { + const state = deriveState(game); + if (state === 'installed' && isInstalledNotShareable(game)) return 'Not shareable'; + switch (state) { + case 'installed': return 'Installed'; + case 'local': return 'Local'; + case 'downloading': return 'Downloading'; + case 'busy': return 'Working'; + case 'none': return ''; + } +}; + +export const gameStatusLabel = (game: Game): string => { + const state = deriveState(game); + if (state === 'installed' && isInstalledNotShareable(game)) { + return 'Installed, not shareable'; + } + switch (state) { + case 'installed': return 'Installed'; + case 'local': return 'Downloaded'; + case 'downloading': return 'Downloading'; + case 'busy': return 'Working…'; + case 'none': return 'Not downloaded'; + } +}; + export const isUnavailable = (game: Game): boolean => !game.installed && !game.downloaded diff --git a/crates/lanspread-tauri-deno-ts/tests/gameState.test.ts b/crates/lanspread-tauri-deno-ts/tests/gameState.test.ts index 2cd40ad..333f23b 100644 --- a/crates/lanspread-tauri-deno-ts/tests/gameState.test.ts +++ b/crates/lanspread-tauri-deno-ts/tests/gameState.test.ts @@ -11,7 +11,9 @@ import { formatDownloadEta, formatDownloadSpeed, formatDownloadSpeedShort, + gameStatusLabel, mergeGameUpdate, + stateChipLabel, } from '../src/lib/gameState.ts'; import { ActiveOperationKind, @@ -243,3 +245,42 @@ Deno.test('stream install is available only for idle remote games', () => { 'busy games should not expose streamed install', ); }); + +Deno.test('streamed local installs are labeled installed but not shareable', () => { + const streamed = game({ + downloaded: false, + installed: true, + install_status: InstallStatus.Installed, + }); + const downloadedInstall = game({ + downloaded: true, + installed: true, + install_status: InstallStatus.Installed, + }); + + assertEquals( + deriveState(streamed), + 'installed', + 'streamed local installs should keep installed visual state', + ); + assertEquals( + stateChipLabel(streamed), + 'Not shareable', + 'card chip should make the non-shareable state visible', + ); + assertEquals( + gameStatusLabel(streamed), + 'Installed, not shareable', + 'detail status should spell out installed plus non-shareable', + ); + assertEquals( + stateChipLabel(downloadedInstall), + 'Installed', + 'normal downloaded installs should keep the installed chip label', + ); + assertEquals( + gameStatusLabel(downloadedInstall), + 'Installed', + 'normal downloaded installs should keep the installed detail label', + ); +});