Feature/streamed install prototype #27
+5
-4
@@ -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.
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { Game } from '../lib/types';
|
||||
import { deriveState } from '../lib/gameState';
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
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 (
|
||||
<div className="state-chip" data-state={state}>
|
||||
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
<div className="meta-cell">
|
||||
<div className="meta-label">Status</div>
|
||||
<div className="meta-value">{statusLabelFor(game)}</div>
|
||||
<div className="meta-value">{gameStatusLabel(game)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user