summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--py_modules/lsfg_vk/flatpak_profile_service.py6
-rwxr-xr-xscripts/deploy-to-deck.sh30
-rw-r--r--src/components/CollapsibleItemGroup.tsx26
-rw-r--r--src/components/Content.tsx24
-rw-r--r--src/components/FlatpakNowPlayingTab.tsx27
-rw-r--r--src/components/FlatpakTab.tsx29
-rw-r--r--src/components/GameConfigurationSelector.tsx22
-rw-r--r--src/components/NowPlayingSummary.tsx16
-rw-r--r--src/components/NowPlayingTab.tsx12
-rw-r--r--src/hooks/useFlatpakConfiguration.ts4
-rw-r--r--src/styles.ts4
-rw-r--r--src/utils/nowPlaying.ts31
-rw-r--r--tests/nowPlaying.test.ts25
13 files changed, 157 insertions, 99 deletions
diff --git a/py_modules/lsfg_vk/flatpak_profile_service.py b/py_modules/lsfg_vk/flatpak_profile_service.py
index 9eaf9c7..9d2d104 100644
--- a/py_modules/lsfg_vk/flatpak_profile_service.py
+++ b/py_modules/lsfg_vk/flatpak_profile_service.py
@@ -378,12 +378,6 @@ class FlatpakProfileService:
fields[2].strip() if len(fields) > 2 else ""
),
})
- running.sort(key=lambda item: (
- not item["active"],
- -(item["start_time"] if isinstance(item["start_time"], int) else -1),
- -int(item["pid"]) if str(item["pid"]).isdigit() else 1,
- item["app_id"],
- ))
return {"success": True, "message": "", "error": None, "apps": running}
except Exception as error:
return {"success": False, "message": "", "error": str(error), "apps": []}
diff --git a/scripts/deploy-to-deck.sh b/scripts/deploy-to-deck.sh
index b5cb4f7..f27fe4e 100755
--- a/scripts/deploy-to-deck.sh
+++ b/scripts/deploy-to-deck.sh
@@ -5,6 +5,36 @@ deck_host="deck@192.168.0.241"
plugin_root="Decky LSFG-VK"
install_path="/home/deck/homebrew/plugins/Decky LSFG-VK"
+if [[ -z ${DEPLOY_EXPECT:-} && -f .env ]]; then
+ set -a
+ source .env
+ set +a
+fi
+
+if [[ -n ${DECK_PASSWORD:-} && -z ${DEPLOY_EXPECT:-} ]]; then
+ export DEPLOY_EXPECT=1
+ export DEPLOY_SCRIPT="$0"
+ exec expect <<'EXPECT'
+set timeout -1
+spawn bash $env(DEPLOY_SCRIPT)
+
+expect {
+ -re {(?i)yes/no} {
+ send -- "yes\r"
+ exp_continue
+ }
+ -re {(?i)(password|passphrase).*:} {
+ send -- "$env(DECK_PASSWORD)\r"
+ exp_continue
+ }
+ eof
+}
+
+catch wait result
+exit [lindex $result 3]
+EXPECT
+fi
+
package_dir="$(mktemp -d "${TMPDIR:-/tmp}/decky-plugin-package.XXXXXX")"
run_id="$(basename "$package_dir")"
remote_archive="/tmp/decky-lsfg-vk-${run_id}.zip"
diff --git a/src/components/CollapsibleItemGroup.tsx b/src/components/CollapsibleItemGroup.tsx
index a66a8ba..29fe945 100644
--- a/src/components/CollapsibleItemGroup.tsx
+++ b/src/components/CollapsibleItemGroup.tsx
@@ -1,5 +1,5 @@
import { ButtonItem, Field, PanelSectionRow } from "@decky/ui";
-import { type RefObject } from "react";
+import { useEffect, useState, type RefObject } from "react";
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
export interface CollapsibleItem {
@@ -9,6 +9,11 @@ export interface CollapsibleItem {
}
export const collapsibleItemGroupStyles = `
+ .LSFG_GameGroupCollapseButton_Container {
+ margin-top: -2px;
+ margin-bottom: 4px;
+ }
+
.LSFG_GameGroupCollapseButton_Container > div > div > div > button,
.LSFG_GameGroupCollapseButton_Container > div > div > div > div > button {
height: 24px !important;
@@ -25,6 +30,24 @@ export const collapsibleItemGroupStyles = `
}
`;
+export function usePersistentCollapsed(key: string) {
+ const [collapsed, setCollapsed] = useState(() => {
+ try {
+ return localStorage.getItem(key) !== "false";
+ } catch {
+ return true;
+ }
+ });
+
+ useEffect(() => {
+ try {
+ localStorage.setItem(key, String(collapsed));
+ } catch {}
+ }, [collapsed, key]);
+
+ return [collapsed, () => setCollapsed((value) => !value)] as const;
+}
+
interface Props {
title: string;
items: CollapsibleItem[];
@@ -53,7 +76,6 @@ export function CollapsibleItemGroup({
<div
ref={toggleRef}
className="LSFG_GameGroupCollapseButton_Container"
- style={{ marginTop: "-2px", marginBottom: "4px" }}
>
<ButtonItem
layout="below"
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index dc8c54f..32e9bf8 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,5 +1,5 @@
import { Tabs } from "@decky/ui";
-import { useEffect, useRef, useState, type FocusEvent } from "react";
+import { useEffect, useRef, useState, type FocusEvent, type ReactNode } from "react";
import { FaCube, FaFileAlt, FaGamepad, FaList, FaTools } from "react-icons/fa";
import { ConfigurationData } from "../config/configSchema";
import { useFlatpakConfiguration } from "../hooks/useFlatpakConfiguration";
@@ -86,7 +86,7 @@ export function Content() {
const hasNowPlaying = Boolean(nowPlayingTarget);
const runningWorkload = nowPlayingTarget
? nowPlayingTarget.kind === "flatpak"
- ? `flatpak:${nowPlayingTarget.app.app_id}`
+ ? `flatpak:${nowPlayingTarget.app.app_id}:${nowPlayingTarget.launcher?.appid ?? ""}`
: `steam:${nowPlayingTarget.game.appid}`
: null;
@@ -141,6 +141,10 @@ export function Content() {
/>
);
+ const tabContent = (content: ReactNode) => (
+ <div className="lsfg-vk-tab-content">{content}</div>
+ );
+
const nowPlaying = nowPlayingTarget?.kind === "steam" ? (
<NowPlayingTab
game={nowPlayingTarget.game}
@@ -159,11 +163,11 @@ export function Content() {
const tabs = setupComplete
? [
- ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: nowPlaying }] : []),
+ ...(nowPlaying ? [{ id: "NowPlaying", title: tabIcons.nowPlaying, content: tabContent(nowPlaying) }] : []),
{
id: "Games",
title: tabIcons.games,
- content: (
+ content: tabContent(
<ConfigurationTab
config={config}
targets={targets}
@@ -177,13 +181,13 @@ export function Content() {
onRepair={repair}
onReset={resetSelected}
onResetAll={resetAll}
- />
+ />,
),
},
{
id: "Flatpak",
title: tabIcons.flatpak,
- content: (
+ content: tabContent(
<FlatpakTab
apps={flatpak.apps}
runningApp={runningFlatpak}
@@ -194,13 +198,13 @@ export function Content() {
onRemove={flatpak.removeApp}
onConfigChange={flatpak.updateConfig}
onWorkaroundChange={flatpak.updateWorkarounds}
- />
+ />,
),
},
- ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> }] : []),
- { id: "Setup", title: tabIcons.setup, content: setup },
+ ...(showDebugTab ? [{ id: "ConfigFile", title: tabIcons.configFile, content: tabContent(<ConfigFileTab />) }] : []),
+ { id: "Setup", title: tabIcons.setup, content: tabContent(setup) },
]
- : [{ id: "Setup", title: tabIcons.setup, content: setup }];
+ : [{ id: "Setup", title: tabIcons.setup, content: tabContent(setup) }];
const availableTabIds = new Set(tabs.map(({ id }) => id));
const activeTab = availableTabIds.has(tab) ? tab : setupComplete ? "Games" : "Setup";
diff --git a/src/components/FlatpakNowPlayingTab.tsx b/src/components/FlatpakNowPlayingTab.tsx
index 9623b11..89f7f49 100644
--- a/src/components/FlatpakNowPlayingTab.tsx
+++ b/src/components/FlatpakNowPlayingTab.tsx
@@ -1,8 +1,9 @@
-import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
+import { Focusable } from "@decky/ui";
import type { FlatpakApp, LsfgConfig } from "../api/lsfgApi";
import type { GameTarget } from "../hooks/useGameConfiguration";
import { ConfigurationSection } from "./ConfigurationSection";
import { FpsMultiplierControl } from "./FpsMultiplierControl";
+import { NowPlayingSummary } from "./NowPlayingSummary";
interface Props {
app: FlatpakApp;
@@ -21,21 +22,15 @@ export function FlatpakNowPlayingTab({ app, launcher, onConfigChange }: Props) {
return (
<Focusable>
- <PanelSection title="Now Playing">
- <PanelSectionRow>
- <Field
- label={launcher?.name || app.app_name}
- description={launcher
- ? `${launcher.nonSteam ? "Steam shortcut" : "Steam"} · Running in ${app.app_name} · Flatpak`
- : `Flatpak · ${app.app_id}`}
- />
- </PanelSectionRow>
- {launcher && (
- <PanelSectionRow>
- <Field label="Controls" description={`${app.app_name} profile · ${app.app_id}`} />
- </PanelSectionRow>
- )}
- </PanelSection>
+ <NowPlayingSummary
+ title={launcher?.name || app.app_name}
+ details={[
+ launcher ? (launcher.nonSteam ? "Steam shortcut" : "Steam") : "Flatpak",
+ launcher && launcher.name !== app.app_name ? `Running in ${app.app_name}` : null,
+ launcher ? "Flatpak" : null,
+ `Controls: ${app.app_name} profile`,
+ ].filter((detail): detail is string => detail !== null)}
+ />
<FpsMultiplierControl config={app.config} onConfigChange={changeConfig} />
<ConfigurationSection config={app.config} onConfigChange={changeConfig} />
</Focusable>
diff --git a/src/components/FlatpakTab.tsx b/src/components/FlatpakTab.tsx
index 31bfc2d..0b20da0 100644
--- a/src/components/FlatpakTab.tsx
+++ b/src/components/FlatpakTab.tsx
@@ -1,8 +1,8 @@
import { ButtonItem, DialogButton, Field, Focusable, PanelSection, PanelSectionRow, gamepadDialogClasses } from "@decky/ui";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { FaArrowLeft } from "react-icons/fa";
import type { FlatpakApp, LsfgConfig, WorkaroundState } from "../api/lsfgApi";
-import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup";
+import { CollapsibleItemGroup, collapsibleItemGroupStyles, usePersistentCollapsed } from "./CollapsibleItemGroup";
import { ConfigurationSection } from "./ConfigurationSection";
import { FlatpakWorkaroundsSection } from "./FlatpakWorkaroundsSection";
import { FpsMultiplierControl } from "./FpsMultiplierControl";
@@ -20,23 +20,8 @@ interface Props {
onWorkaroundChange: (appId: string, state: WorkaroundState) => Promise<boolean>;
}
-function usePersistentCollapsed(key: string) {
- const [collapsed, setCollapsed] = useState(() => {
- try {
- return localStorage.getItem(key) !== "false";
- } catch {
- return true;
- }
- });
-
- useEffect(() => {
- try {
- localStorage.setItem(key, String(collapsed));
- } catch {}
- }, [collapsed, key]);
-
- return [collapsed, () => setCollapsed((value) => !value)] as const;
-}
+const ENABLED_COLLAPSED_KEY = "lsfg-flatpak-enabled-collapsed-v2";
+const AVAILABLE_COLLAPSED_KEY = "lsfg-flatpak-available-collapsed-v2";
export function FlatpakTab({
apps,
@@ -55,9 +40,8 @@ export function FlatpakTab({
[apps, selectedAppId],
);
const close = useCallback(() => setSelectedAppId(null), []);
- const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed("lsfg-flatpak-enabled-collapsed-v1");
- const [availableCollapsed, toggleAvailable] = usePersistentCollapsed("lsfg-flatpak-available-collapsed-v1");
- const enabledToggleRef = useRef<HTMLDivElement>(null);
+ const [enabledCollapsed, toggleEnabled] = usePersistentCollapsed(ENABLED_COLLAPSED_KEY);
+ const [availableCollapsed, toggleAvailable] = usePersistentCollapsed(AVAILABLE_COLLAPSED_KEY);
const enabledApps = useMemo(
() => apps.filter((app) => app.enabled).sort((a, b) => a.app_name.localeCompare(b.app_name)),
@@ -87,7 +71,6 @@ export function FlatpakTab({
collapsed={enabledCollapsed}
onToggle={toggleEnabled}
onSelect={setSelectedAppId}
- toggleRef={enabledToggleRef}
/>
<CollapsibleItemGroup
title="Available"
diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx
index ee87423..2c683ae 100644
--- a/src/components/GameConfigurationSelector.tsx
+++ b/src/components/GameConfigurationSelector.tsx
@@ -1,7 +1,7 @@
import { ButtonItem, ConfirmModal, Field, PanelSectionRow, showModal } from "@decky/ui";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef } from "react";
import { GameTarget } from "../hooks/useGameConfiguration";
-import { CollapsibleItemGroup, collapsibleItemGroupStyles } from "./CollapsibleItemGroup";
+import { CollapsibleItemGroup, collapsibleItemGroupStyles, usePersistentCollapsed } from "./CollapsibleItemGroup";
interface Props {
targets: GameTarget[];
@@ -16,24 +16,6 @@ interface Props {
const ENABLED_COLLAPSED_KEY = "lsfg-enabled-games-collapsed-v4";
const AVAILABLE_COLLAPSED_KEY = "lsfg-available-games-collapsed-v3";
-function usePersistentCollapsed(key: string) {
- const [collapsed, setCollapsed] = useState(() => {
- try {
- return localStorage.getItem(key) !== "false";
- } catch {
- return true;
- }
- });
-
- useEffect(() => {
- try {
- localStorage.setItem(key, String(collapsed));
- } catch {}
- }, [collapsed, key]);
-
- return [collapsed, () => setCollapsed((value) => !value)] as const;
-}
-
function targetDescription(game: GameTarget): string {
return game.nonSteam ? "Non-Steam" : "Steam";
}
diff --git a/src/components/NowPlayingSummary.tsx b/src/components/NowPlayingSummary.tsx
new file mode 100644
index 0000000..adc5d10
--- /dev/null
+++ b/src/components/NowPlayingSummary.tsx
@@ -0,0 +1,16 @@
+import { Field, PanelSection, PanelSectionRow } from "@decky/ui";
+
+interface Props {
+ title: string;
+ details: string[];
+}
+
+export function NowPlayingSummary({ title, details }: Props) {
+ return (
+ <PanelSection>
+ <PanelSectionRow>
+ <Field label={title} description={details.filter(Boolean).join(" | ")} />
+ </PanelSectionRow>
+ </PanelSection>
+ );
+}
diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx
index c067dc0..4c22fb7 100644
--- a/src/components/NowPlayingTab.tsx
+++ b/src/components/NowPlayingTab.tsx
@@ -1,7 +1,8 @@
-import { Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui";
+import { Focusable } from "@decky/ui";
import { ConfigurationData } from "../config/configSchema";
import { GameTarget } from "../hooks/useGameConfiguration";
import { GameConfigurationControls } from "./GameConfigurationControls";
+import { NowPlayingSummary } from "./NowPlayingSummary";
interface Props {
game: GameTarget;
@@ -23,11 +24,10 @@ export function NowPlayingTab({
}: Props) {
return (
<Focusable>
- <PanelSection title="Now Playing">
- <PanelSectionRow>
- <Field label={game.name} description={targetDescription(game)} />
- </PanelSectionRow>
- </PanelSection>
+ <NowPlayingSummary
+ title={game.name}
+ details={[targetDescription(game), `Controls: ${game.name} profile`]}
+ />
<GameConfigurationControls
config={config}
onConfigChange={onConfigChange}
diff --git a/src/hooks/useFlatpakConfiguration.ts b/src/hooks/useFlatpakConfiguration.ts
index ce881f0..6ff7a79 100644
--- a/src/hooks/useFlatpakConfiguration.ts
+++ b/src/hooks/useFlatpakConfiguration.ts
@@ -120,9 +120,7 @@ export function useFlatpakConfiguration(enabled: boolean) {
[operate],
);
- const runningApp = useMemo(() => {
- return selectMostRecentRunningFlatpak(apps, runningApps);
- }, [apps, runningApps]);
+ const runningApp = useMemo(() => selectMostRecentRunningFlatpak(apps, runningApps), [apps, runningApps]);
return {
apps,
diff --git a/src/styles.ts b/src/styles.ts
index d197451..58c6b01 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -10,6 +10,10 @@ export const tabStyles = `
padding-right: 8px !important;
}
+ .lsfg-vk-tabs .lsfg-vk-tab-content {
+ padding-bottom: 96px; // workaround for in-game bottom bar padding behaving differently than in launcher, remove later?
+ }
+
.lsfg-vk-tabs [role="tablist"] {
display: flex;
flex-wrap: nowrap;
diff --git a/src/utils/nowPlaying.ts b/src/utils/nowPlaying.ts
index a207321..2e0a876 100644
--- a/src/utils/nowPlaying.ts
+++ b/src/utils/nowPlaying.ts
@@ -29,25 +29,34 @@ export function selectMostRecentRunningFlatpak(
running,
app: apps.find((app) => app.app_id === running.app_id) || null,
}))
- .filter((candidate): candidate is { running: RunningFlatpakApp; app: FlatpakApp } => candidate.app !== null)
- .sort((a, b) => {
- if (a.running.active !== b.running.active) return a.running.active ? -1 : 1;
- const startDifference = numericValue(b.running.start_time) - numericValue(a.running.start_time);
- if (startDifference !== 0) return startDifference;
- const pidDifference = numericPid(b.running.pid) - numericPid(a.running.pid);
- if (pidDifference !== 0) return pidDifference;
- return a.running.app_id.localeCompare(b.running.app_id);
- });
+ .filter((candidate): candidate is { running: RunningFlatpakApp; app: FlatpakApp } => candidate.app !== null);
+ const activeCandidates = candidates.filter(({ running }) => running.active);
+ const eligibleCandidates = activeCandidates.length > 0
+ ? activeCandidates
+ : candidates.length === 1
+ ? candidates
+ : [];
- return candidates[0]?.app || null;
+ eligibleCandidates.sort((a, b) => {
+ const startDifference = numericValue(b.running.start_time) - numericValue(a.running.start_time);
+ if (startDifference !== 0) return startDifference;
+ const pidDifference = numericPid(b.running.pid) - numericPid(a.running.pid);
+ if (pidDifference !== 0) return pidDifference;
+ return a.running.app_id.localeCompare(b.running.app_id);
+ });
+
+ return eligibleCandidates[0]?.app || null;
}
export function resolveNowPlayingTarget(
runningGame: GameTarget | null,
runningFlatpak: FlatpakApp | null,
): NowPlayingTarget | null {
+ if (runningGame && !runningGame.nonSteam) {
+ return runningGame.configured ? { kind: "steam", game: runningGame } : null;
+ }
if (runningFlatpak) {
- return { kind: "flatpak", app: runningFlatpak, launcher: runningGame };
+ return { kind: "flatpak", app: runningFlatpak, launcher: runningGame?.nonSteam ? runningGame : null };
}
if (runningGame?.configured) return { kind: "steam", game: runningGame };
return null;
diff --git a/tests/nowPlaying.test.ts b/tests/nowPlaying.test.ts
index 3f0d891..8a52b67 100644
--- a/tests/nowPlaying.test.ts
+++ b/tests/nowPlaying.test.ts
@@ -54,8 +54,12 @@ test("Flatpak runtime wins while a Steam shortcut is running", () => {
assert.equal(target?.kind === "flatpak" ? target.launcher?.name : null, "1080 Snowboarding");
});
-test("Flatpak runtime wins over a native Steam game", () => {
- assert.equal(resolveNowPlayingTarget(game(false), flatpak("org.example.Game"))?.kind, "flatpak");
+test("native Steam game wins over an unrelated Flatpak", () => {
+ assert.equal(resolveNowPlayingTarget(game(false), flatpak("org.example.Game"))?.kind, "steam");
+});
+
+test("unconfigured native Steam game blocks unrelated Flatpak Now Playing", () => {
+ assert.equal(resolveNowPlayingTarget(game(false, false), flatpak("org.example.Game")), null);
});
test("direct Flatpak launch creates a Flatpak Now Playing target", () => {
@@ -65,6 +69,23 @@ test("direct Flatpak launch creates a Flatpak Now Playing target", () => {
assert.equal(target?.kind === "flatpak" ? target.launcher : null, null);
});
+test("multiple inactive Flatpaks do not create an arbitrary Now Playing target", () => {
+ const apps = [flatpak("org.example.one"), flatpak("org.example.two")];
+ const running = [
+ { app_id: "org.example.one", active: false, pid: "100", start_time: 500 },
+ { app_id: "org.example.two", active: false, pid: "200", start_time: 600 },
+ ];
+
+ assert.equal(selectMostRecentRunningFlatpak(apps, running), null);
+});
+
+test("one inactive Flatpak remains a usable fallback", () => {
+ const apps = [flatpak("org.example.one")];
+ const running = [{ app_id: "org.example.one", active: false, pid: "100", start_time: 500 }];
+
+ assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "org.example.one");
+});
+
test("configured Steam target remains the fallback", () => {
const target = resolveNowPlayingTarget(game(false), null);