summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-07 16:02:26 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-07 16:02:26 -0400
commit22db1125238e54b29538102727c36284e122e703 (patch)
tree9fb5f2bc5a00256dc811e4dbbd1e963c5492c48c /src/components
parentf48ff4b541b1ef877905bda248459460a943dd6e (diff)
downloaddecky-lsfg-vk-22db1125238e54b29538102727c36284e122e703.tar.gz
decky-lsfg-vk-22db1125238e54b29538102727c36284e122e703.zip
feat: refine game discovery and profile UX
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConfigurationTab.tsx59
-rw-r--r--src/components/Content.tsx8
-rw-r--r--src/components/GameConfigurationSelector.tsx50
-rw-r--r--src/components/NowPlayingTab.tsx14
4 files changed, 88 insertions, 43 deletions
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx
index 0d8553c..83f46c1 100644
--- a/src/components/ConfigurationTab.tsx
+++ b/src/components/ConfigurationTab.tsx
@@ -12,6 +12,7 @@ interface ConfigurationTabProps {
onSelect: (appid: string) => void;
onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>;
onEnable: (appid: string) => Promise<boolean>;
+ onEnableAll: () => Promise<void>;
onReset: () => Promise<void>;
onResetAll: () => Promise<void>;
}
@@ -23,29 +24,32 @@ export function ConfigurationTab({
onSelect,
onConfigChange,
onEnable,
+ onEnableAll,
onReset,
onResetAll,
}: ConfigurationTabProps) {
const [detailAppId, setDetailAppId] = useState<string | null>(null);
const [focusFpsMultiplier, setFocusFpsMultiplier] = useState(false);
- const [focusBackToGames, setFocusBackToGames] = useState(false);
+ const [focusDetailAction, setFocusDetailAction] = useState<"enable" | "back" | null>(null);
const backToGamesRef = useRef<HTMLDivElement>(null);
+ const enableRef = useRef<HTMLDivElement>(null);
const promptedRunningAppId = useRef<string | null>(null);
const closeDetails = useCallback(() => {
setFocusFpsMultiplier(false);
- setFocusBackToGames(false);
+ setFocusDetailAction(null);
setDetailAppId(null);
}, []);
const clearFpsFocusRequest = useCallback(() => setFocusFpsMultiplier(false), []);
useEffect(() => {
- if (!focusBackToGames) return;
+ if (!focusDetailAction) return;
const frame = requestAnimationFrame(() => {
- backToGamesRef.current?.querySelector<HTMLElement>('[role="button"]')?.focus();
- setFocusBackToGames(false);
+ const ref = focusDetailAction === "enable" ? enableRef : backToGamesRef;
+ ref.current?.querySelector<HTMLElement>('[role="button"]')?.focus();
+ setFocusDetailAction(null);
});
return () => cancelAnimationFrame(frame);
- }, [focusBackToGames]);
+ }, [focusDetailAction]);
useEffect(() => {
if (!runningGame || runningGame.configured) {
@@ -54,6 +58,7 @@ export function ConfigurationTab({
}
if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) {
promptedRunningAppId.current = runningGame.appid;
+ setFocusDetailAction(runningGame.configured ? "back" : "enable");
setDetailAppId(runningGame.appid);
}
}, [detailAppId, runningGame?.appid, runningGame?.configured]);
@@ -67,10 +72,11 @@ export function ConfigurationTab({
targets={targets}
runningGame={runningGame}
onSelect={(appid) => {
- setFocusBackToGames(true);
+ setFocusDetailAction(targets.find((target) => target.appid === appid)?.configured ? "back" : "enable");
onSelect(appid);
setDetailAppId(appid);
}}
+ onEnableAll={onEnableAll}
onResetAll={onResetAll}
/>
</PanelSection>
@@ -79,8 +85,17 @@ export function ConfigurationTab({
const profileLabel = selectedTarget?.name || "Game profile";
const profileDescription = selectedTarget
- ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "Configured" : "Not configured"}`
+ ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? "LSFG-VK Enabled" : "LSFG-VK not enabled"}`
: "Game is no longer available";
+ const handleProfileAction = async () => {
+ if (selectedTarget?.configured) {
+ promptedRunningAppId.current = detailAppId;
+ await onReset();
+ closeDetails();
+ } else if (detailAppId && await onEnable(detailAppId)) {
+ setFocusFpsMultiplier(true);
+ }
+ };
return (
<Focusable onCancelButton={closeDetails}>
@@ -88,6 +103,13 @@ export function ConfigurationTab({
<PanelSectionRow>
<Field label={profileLabel} description={profileDescription} />
</PanelSectionRow>
+ {!selectedTarget?.configured && selectedTarget && (
+ <PanelSectionRow>
+ <Focusable ref={enableRef} noFocusRing>
+ <ButtonItem layout="below" onClick={handleProfileAction}>Enable for next launch</ButtonItem>
+ </Focusable>
+ </PanelSectionRow>
+ )}
<PanelSectionRow>
<Focusable ref={backToGamesRef} noFocusRing>
<ButtonItem layout="below" onClick={closeDetails}>Back to games</ButtonItem>
@@ -102,22 +124,11 @@ export function ConfigurationTab({
onFpsMultiplierFocused={clearFpsFocusRequest}
/>
)}
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={async () => {
- if (selectedTarget?.configured) {
- promptedRunningAppId.current = detailAppId;
- await onReset();
- closeDetails();
- } else if (detailAppId) {
- if (await onEnable(detailAppId)) setFocusFpsMultiplier(true);
- }
- }}
- >
- {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"}
- </ButtonItem>
- </PanelSectionRow>
+ {selectedTarget?.configured && (
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={handleProfileAction}>Remove profile</ButtonItem>
+ </PanelSectionRow>
+ )}
</Focusable>
);
}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index 1bc6e95..f1c8c11 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -6,7 +6,7 @@ import { tabStyles } from "../styles";
import { useGameConfiguration } from "../hooks/useGameConfiguration";
import { useInstallationActions } from "../hooks/useInstallationActions";
import { useInstallationStatus } from "../hooks/useLsfgHooks";
-// import { ConfigFileTab } from "./ConfigFileTab";
+import { ConfigFileTab } from "./ConfigFileTab";
import { ConfigurationTab } from "./ConfigurationTab";
import { FlatpaksTab } from "./FlatpaksTab";
import { NowPlayingTab } from "./NowPlayingTab";
@@ -38,6 +38,7 @@ export function Content() {
setSelectedAppId,
save,
enable,
+ enableAll,
resetSelected,
resetAll,
reload,
@@ -115,7 +116,6 @@ export function Content() {
game={runningGame}
config={config}
onConfigChange={handleConfigChange}
- onRemove={resetSelected}
/>
),
}] : []),
@@ -130,14 +130,14 @@ export function Content() {
onSelect={setSelectedAppId}
onConfigChange={handleConfigChange}
onEnable={enable}
+ onEnableAll={enableAll}
onReset={resetSelected}
onResetAll={resetAll}
/>
),
},
{ id: "Flatpak", title: tabIcons.flatpak, content: <FlatpaksTab /> },
- // Keep the configuration-file view available for future use without exposing it in the UI.
- // { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> },
+ { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }, // comment out for prod
{ id: "Setup", title: tabIcons.setup, content: setupContent },
]
: [
diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx
index 91fdc39..a046594 100644
--- a/src/components/GameConfigurationSelector.tsx
+++ b/src/components/GameConfigurationSelector.tsx
@@ -7,10 +7,11 @@ interface Props {
targets: GameTarget[];
runningGame: GameTarget | null;
onSelect: (appid: string) => void;
+ onEnableAll: () => Promise<void>;
onResetAll: () => Promise<void>;
}
-const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed";
+const CONFIGURED_COLLAPSED_KEY = "lsfg-configured-games-collapsed-v2";
const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed";
function usePersistentCollapsed(key: string) {
@@ -73,7 +74,7 @@ function GameGroup({
);
}
-export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) {
+export function GameConfigurationSelector({ targets, runningGame, onSelect, onEnableAll, onResetAll }: Props) {
const sortGames = (games: GameTarget[]) => [...games].sort((a, b) => {
if (a.appid === runningGame?.appid) return -1;
if (b.appid === runningGame?.appid) return 1;
@@ -81,8 +82,14 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe
});
const configuredGames = sortGames(targets.filter((game) => game.configured));
const availableGames = sortGames(targets.filter((game) => !game.configured));
+ const configuredSteamGames = configuredGames.filter((game) => !game.nonSteam);
+ const configuredNonSteamGames = configuredGames.filter((game) => game.nonSteam);
+ const availableSteamGames = availableGames.filter((game) => !game.nonSteam);
+ const availableNonSteamGames = availableGames.filter((game) => game.nonSteam);
const [configuredCollapsed, toggleConfigured] = usePersistentCollapsed(CONFIGURED_COLLAPSED_KEY);
+ const [configuredNonSteamCollapsed, toggleConfiguredNonSteam] = usePersistentCollapsed(`${CONFIGURED_COLLAPSED_KEY}-non-steam`);
const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY);
+ const [availableNonSteamCollapsed, toggleAvailableNonSteam] = usePersistentCollapsed(`${AVAILABLE_COLLAPSED_KEY}-non-steam`);
const confirmResetAll = () => {
showModal(
<ConfirmModal
@@ -94,6 +101,18 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe
/>,
);
};
+ const confirmEnableAll = () => {
+ showModal(
+ <ConfirmModal
+ strTitle="Enable all available games?"
+ strDescription="Create individual LSFG-VK profiles for every available game using the plugin defaults."
+ strOKButtonText="Enable all"
+ strCancelButtonText="Cancel"
+ onOK={() => void onEnableAll()}
+ onCancel={() => {}}
+ />,
+ );
+ };
return (
<>
@@ -102,20 +121,41 @@ export function GameConfigurationSelector({ targets, runningGame, onSelect, onRe
<Field label="No installed games" description="Steam has not reported any eligible games" />
</PanelSectionRow>
)}
+ {availableGames.length > 0 && (
+ <PanelSectionRow>
+ <ButtonItem layout="below" onClick={confirmEnableAll}>
+ Enable all available games
+ </ButtonItem>
+ </PanelSectionRow>
+ )}
<GameGroup
- title="Configured"
- games={configuredGames}
+ title="LSFG-VK Enabled"
+ games={configuredSteamGames}
collapsed={configuredCollapsed}
onToggle={toggleConfigured}
onSelect={onSelect}
/>
<GameGroup
+ title="LSFG-VK Enabled (Non-Steam)"
+ games={configuredNonSteamGames}
+ collapsed={configuredNonSteamCollapsed}
+ onToggle={toggleConfiguredNonSteam}
+ onSelect={onSelect}
+ />
+ <GameGroup
title="Available games"
- games={availableGames}
+ games={availableSteamGames}
collapsed={availableCollapsed}
onToggle={toggleAvailable}
onSelect={onSelect}
/>
+ <GameGroup
+ title="Available games (Non-Steam)"
+ games={availableNonSteamGames}
+ collapsed={availableNonSteamCollapsed}
+ onToggle={toggleAvailableNonSteam}
+ onSelect={onSelect}
+ />
<PanelSectionRow>
<ButtonItem
layout="below"
diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx
index b4f0557..8d2d779 100644
--- a/src/components/NowPlayingTab.tsx
+++ b/src/components/NowPlayingTab.tsx
@@ -1,4 +1,4 @@
-import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
+import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
import { ConfigurationData } from "../config/configSchema";
import { GameTarget } from "../hooks/useGameConfiguration";
import { GameConfigurationControls } from "./GameConfigurationControls";
@@ -7,26 +7,20 @@ interface Props {
game: GameTarget;
config: ConfigurationData;
onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>;
- onRemove: () => Promise<void>;
}
-export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) {
+export function NowPlayingTab({ game, config, onConfigChange }: Props) {
return (
<Focusable>
- <PanelSection title="Now Playing">
+ <PanelSection>
<PanelSectionRow>
<Field
label={game.name}
- description={`${game.nonSteam ? "Non-Steam" : "Steam"} · App ID ${game.appid} · Configured`}
+ description={`${game.nonSteam ? "Non-Steam" : "Steam"} | App ID ${game.appid}`}
/>
</PanelSectionRow>
</PanelSection>
<GameConfigurationControls config={config} onConfigChange={onConfigChange} />
- <PanelSectionRow>
- <ButtonItem layout="below" onClick={() => void onRemove()}>
- Remove profile
- </ButtonItem>
- </PanelSectionRow>
</Focusable>
);
}