summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ClipboardButton.tsx22
-rw-r--r--src/components/ClipboardDisplay.tsx139
-rw-r--r--src/components/ConfigurationSection.tsx167
-rw-r--r--src/components/Content.tsx87
-rw-r--r--src/components/FlatpaksModal.tsx151
-rw-r--r--src/components/FpsMultiplierControl.tsx4
-rw-r--r--src/components/NerdStuffModal.tsx20
-rw-r--r--src/components/PluginUpdateChecker.tsx176
-rw-r--r--src/components/ProfileManagement.tsx11
-rw-r--r--src/components/WikiButton.tsx22
-rw-r--r--src/components/index.ts11
11 files changed, 281 insertions, 529 deletions
diff --git a/src/components/ClipboardButton.tsx b/src/components/ClipboardButton.tsx
deleted file mode 100644
index cac1863..0000000
--- a/src/components/ClipboardButton.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { PanelSectionRow, ButtonItem } from "@decky/ui";
-import { FaBook } from "react-icons/fa";
-
-export function ClipboardButton() {
- const handleClipboardClick = () => {
- window.open("https://github.com/xXJSONDeruloXx/decky-lossless-scaling-vk/wiki/Clipboard", "_blank");
- };
-
- return (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleClipboardClick}
- >
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaBook />
- <div>Plugin Wiki</div>
- </div>
- </ButtonItem>
- </PanelSectionRow>
- );
-}
diff --git a/src/components/ClipboardDisplay.tsx b/src/components/ClipboardDisplay.tsx
deleted file mode 100644
index 852a50f..0000000
--- a/src/components/ClipboardDisplay.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { useState, useEffect } from "react";
-import { PanelSectionRow } from "@decky/ui";
-import { FaClipboard, FaEye } from "react-icons/fa";
-
-export function ClipboardDisplay() {
- const [clipboardContent, setClipboardContent] = useState<string>("");
- const [isReading, setIsReading] = useState(false);
-
- const readClipboard = async () => {
- if (isReading) return; // Prevent concurrent reads
-
- setIsReading(true);
- try {
- console.log("ClipboardDisplay: Attempting to read clipboard...");
-
- if (!navigator.clipboard) {
- console.log("ClipboardDisplay: navigator.clipboard not available");
- setClipboardContent("Clipboard API not available");
- return;
- }
-
- if (!navigator.clipboard.readText) {
- console.log("ClipboardDisplay: navigator.clipboard.readText not available");
- setClipboardContent("Clipboard read not supported");
- return;
- }
-
- console.log("ClipboardDisplay: Calling navigator.clipboard.readText()...");
- const content = await navigator.clipboard.readText();
- console.log("ClipboardDisplay: Successfully read clipboard:", content.length, "characters");
- setClipboardContent(content);
- } catch (error) {
- // This is expected if user hasn't granted clipboard permissions
- // or if we're in a context where reading isn't allowed
- console.log("ClipboardDisplay: Error reading clipboard:", error);
- console.log("ClipboardDisplay: Error name:", (error as Error).name);
- console.log("ClipboardDisplay: Error message:", (error as Error).message);
-
- // More specific error messages based on error type
- if (error instanceof DOMException) {
- switch (error.name) {
- case 'NotAllowedError':
- setClipboardContent("Clipboard access denied - check permissions");
- break;
- case 'NotFoundError':
- setClipboardContent("No clipboard data found");
- break;
- case 'SecurityError':
- setClipboardContent("Clipboard access blocked by security policy");
- break;
- default:
- setClipboardContent(`Clipboard error: ${error.name}`);
- }
- } else {
- setClipboardContent("Unable to read clipboard");
- }
- } finally {
- setIsReading(false);
- }
- };
-
- // Read clipboard on mount and then every 3 seconds
- useEffect(() => {
- readClipboard();
-
- const interval = setInterval(() => {
- readClipboard();
- }, 3000);
-
- return () => clearInterval(interval);
- }, []);
-
- const truncateText = (text: string, maxLength: number = 60) => {
- if (text.length <= maxLength) return text;
- return text.substring(0, maxLength) + "...";
- };
-
- const displayText = truncateText(clipboardContent);
-
- return (
- <PanelSectionRow>
- <div style={{
- padding: "8px",
- backgroundColor: "rgba(255, 255, 255, 0.05)",
- borderRadius: "4px",
- border: "1px solid rgba(255, 255, 255, 0.1)",
- marginBottom: "8px"
- }}>
- <div style={{
- display: "flex",
- alignItems: "center",
- gap: "8px",
- marginBottom: "4px"
- }}>
- <FaClipboard style={{ color: "#888", fontSize: "12px" }} />
- <div style={{
- fontSize: "12px",
- fontWeight: "bold",
- color: "#888",
- textTransform: "uppercase"
- }}>
- Current Clipboard
- </div>
- {isReading && (
- <FaEye style={{
- color: "#888",
- fontSize: "10px",
- animation: "pulse 1s ease-in-out infinite"
- }} />
- )}
- </div>
- <div style={{
- fontSize: "11px",
- color: clipboardContent.includes("error") ||
- clipboardContent.includes("denied") ||
- clipboardContent.includes("not available") ||
- clipboardContent.includes("not supported") ||
- clipboardContent.includes("blocked") ||
- clipboardContent === "Unable to read clipboard"
- ? "#ff6b6b"
- : "#fff",
- fontFamily: "monospace",
- wordBreak: "break-word",
- lineHeight: "1.3",
- minHeight: "14px"
- }}>
- {displayText || "Reading clipboard..."}
- </div>
- </div>
- <style>{`
- @keyframes pulse {
- 0% { opacity: 0.5; }
- 50% { opacity: 1; }
- 100% { opacity: 0.5; }
- }
- `}</style>
- </PanelSectionRow>
- );
-}
diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx
index b098b32..0734297 100644
--- a/src/components/ConfigurationSection.tsx
+++ b/src/components/ConfigurationSection.tsx
@@ -2,7 +2,6 @@ import { PanelSectionRow, ToggleField, SliderField, ButtonItem } from "@decky/ui
import { useState, useEffect } from "react";
import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri";
import { ConfigurationData } from "../config/configSchema";
-import { FpsMultiplierControl } from "./FpsMultiplierControl";
import {
FLOW_SCALE, PERFORMANCE_MODE, HDR_MODE,
EXPERIMENTAL_PRESENT_MODE, DXVK_FRAME_RATE, DISABLE_STEAMDECK_MODE,
@@ -14,13 +13,23 @@ interface ConfigurationSectionProps {
onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string) => Promise<void>;
}
-const WORKAROUNDS_COLLAPSED_KEY = 'lsfg-workarounds-collapsed';
+const WORKAROUNDS_COLLAPSED_KEY = "lsfg-workarounds-collapsed";
+const CONFIG_COLLAPSED_KEY = "lsfg-config-collapsed";
export function ConfigurationSection({
config,
onConfigChange
}: ConfigurationSectionProps) {
// Initialize with localStorage value, fallback to true if not found
+ const [configCollapsed, setConfigCollapsed] = useState(() => {
+ try {
+ const saved = localStorage.getItem(CONFIG_COLLAPSED_KEY);
+ return saved !== null ? JSON.parse(saved) : false;
+ } catch {
+ return false;
+ }
+ });
+
const [workaroundsCollapsed, setWorkaroundsCollapsed] = useState(() => {
try {
const saved = localStorage.getItem(WORKAROUNDS_COLLAPSED_KEY);
@@ -33,9 +42,17 @@ export function ConfigurationSection({
// Persist workarounds collapse state to localStorage
useEffect(() => {
try {
+ localStorage.setItem(CONFIG_COLLAPSED_KEY, JSON.stringify(configCollapsed));
+ } catch (error) {
+ console.warn("Failed to save config collapse state:", error);
+ }
+ }, [configCollapsed]);
+
+ useEffect(() => {
+ try {
localStorage.setItem(WORKAROUNDS_COLLAPSED_KEY, JSON.stringify(workaroundsCollapsed));
} catch (error) {
- console.warn('Failed to save workarounds collapse state:', error);
+ console.warn("Failed to save workarounds collapse state:", error);
}
}, [workaroundsCollapsed]);
@@ -43,6 +60,8 @@ export function ConfigurationSection({
<>
<style>
{`
+ .LSFG_ConfigCollapseButton_Container > div > div > div > button,
+ .LSFG_ConfigCollapseButton_Container > div > div > div > div > button,
.LSFG_WorkaroundsCollapseButton_Container > div > div > div > button {
height: 10px !important;
}
@@ -52,59 +71,100 @@ export function ConfigurationSection({
`}
</style>
- {/* FPS Multiplier */}
- <FpsMultiplierControl config={config} onConfigChange={onConfigChange} />
-
+ {/* Config Section */}
<PanelSectionRow>
- <SliderField
- label={`Flow Scale (${Math.round(config.flow_scale * 100)}%)`}
- description="Lowers internal motion estimation resolution, improving performance slightly"
- value={config.flow_scale}
- min={0.25}
- max={1.0}
- step={0.01}
- onChange={(value) => onConfigChange(FLOW_SCALE, value)}
- />
+ <div
+ style={{
+ fontSize: "14px",
+ fontWeight: "bold",
+ marginTop: "8px",
+ marginBottom: "6px",
+ borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
+ paddingBottom: "3px",
+ color: "white"
+ }}
+ >
+ Config
+ </div>
</PanelSectionRow>
<PanelSectionRow>
- <SliderField
- label={`Base FPS Cap${config.dxvk_frame_rate > 0 ? ` (${config.dxvk_frame_rate} FPS)` : ' (Off)'}`}
- description="Base framerate cap for DirectX games, before frame multiplier. (Requires game restart to apply)"
- value={config.dxvk_frame_rate}
- min={0}
- max={60}
- step={1}
- onChange={(value) => onConfigChange(DXVK_FRAME_RATE, value)}
- />
+ <div
+ className="LSFG_ConfigCollapseButton_Container"
+ style={{ marginTop: "-2px", marginBottom: "4px" }}
+ >
+ <ButtonItem
+ layout="below"
+ bottomSeparator={configCollapsed ? "standard" : "none"}
+ onClick={() => setConfigCollapsed(!configCollapsed)}
+ >
+ {configCollapsed ? (
+ <RiArrowDownSFill
+ style={{ transform: "translate(0, -13px)", fontSize: "1.5em" }}
+ />
+ ) : (
+ <RiArrowUpSFill
+ style={{ transform: "translate(0, -12px)", fontSize: "1.5em" }}
+ />
+ )}
+ </ButtonItem>
+ </div>
</PanelSectionRow>
- <PanelSectionRow>
- <ToggleField
- label={`Present Mode (${(config.experimental_present_mode || "fifo") === "fifo" ? "FIFO - VSync" : "Mailbox"})`}
- description="Toggle between FIFO - VSync (default) and Mailbox presentation modes for better performance or compatibility"
- checked={(config.experimental_present_mode || "fifo") === "fifo"}
- onChange={(value) => onConfigChange(EXPERIMENTAL_PRESENT_MODE, value ? "fifo" : "mailbox")}
- />
- </PanelSectionRow>
+ {!configCollapsed && (
+ <>
+ <PanelSectionRow>
+ <SliderField
+ label={`Flow Scale (${Math.round(config.flow_scale * 100)}%)`}
+ description="Lowers internal motion estimation resolution, improving performance slightly"
+ value={config.flow_scale}
+ min={0.25}
+ max={1.0}
+ step={0.01}
+ onChange={(value) => onConfigChange(FLOW_SCALE, value)}
+ />
+ </PanelSectionRow>
- <PanelSectionRow>
- <ToggleField
- label="Performance Mode"
- description="Uses a lighter model for FG (Recommended for most games)"
- checked={config.performance_mode}
- onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)}
- />
- </PanelSectionRow>
+ <PanelSectionRow>
+ <SliderField
+ label={`Base FPS Cap${config.dxvk_frame_rate > 0 ? ` (${config.dxvk_frame_rate} FPS)` : " (Off)"}`}
+ description="Base framerate cap for DirectX games, before frame multiplier. (Requires game restart to apply)"
+ value={config.dxvk_frame_rate}
+ min={0}
+ max={60}
+ step={1}
+ onChange={(value) => onConfigChange(DXVK_FRAME_RATE, value)}
+ />
+ </PanelSectionRow>
- <PanelSectionRow>
- <ToggleField
- label="HDR Mode"
- description="Enables HDR mode (only for games that support HDR)"
- checked={config.hdr_mode}
- onChange={(value) => onConfigChange(HDR_MODE, value)}
- />
- </PanelSectionRow>
+ <PanelSectionRow>
+ <ToggleField
+ label={`Present Mode (${(config.experimental_present_mode || "fifo") === "fifo" ? "FIFO - VSync" : "Mailbox"})`}
+ description="Toggle between FIFO - VSync (default) and Mailbox presentation modes for better performance or compatibility"
+ checked={(config.experimental_present_mode || "fifo") === "fifo"}
+ onChange={(value) => onConfigChange(EXPERIMENTAL_PRESENT_MODE, value ? "fifo" : "mailbox")}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="Performance Mode"
+ description="Uses a lighter model for FG (Recommended for most games)"
+ checked={config.performance_mode}
+ onChange={(value) => onConfigChange(PERFORMANCE_MODE, value)}
+ />
+ </PanelSectionRow>
+
+ <PanelSectionRow>
+ <ToggleField
+ label="HDR Mode"
+ description="Enables HDR mode (only for games that support HDR)"
+ checked={config.hdr_mode}
+ onChange={(value) => onConfigChange(HDR_MODE, value)}
+ />
+ </PanelSectionRow>
+ </>
+ )}
{/* Workarounds Section */}
<PanelSectionRow>
@@ -112,10 +172,10 @@ export function ConfigurationSection({
style={{
fontSize: "14px",
fontWeight: "bold",
- marginTop: "16px",
- marginBottom: "8px",
+ marginTop: "8px",
+ marginBottom: "6px",
borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
- paddingBottom: "4px",
+ paddingBottom: "3px",
color: "white"
}}
>
@@ -124,7 +184,10 @@ export function ConfigurationSection({
</PanelSectionRow>
<PanelSectionRow>
- <div className="LSFG_WorkaroundsCollapseButton_Container">
+ <div
+ className="LSFG_WorkaroundsCollapseButton_Container"
+ style={{ marginTop: "-2px", marginBottom: "4px" }}
+ >
<ButtonItem
layout="below"
bottomSeparator={workaroundsCollapsed ? "standard" : "none"}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index fdb8672..28eefa7 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -8,14 +8,11 @@ import { InstallationButton } from "./InstallationButton";
import { ConfigurationSection } from "./ConfigurationSection";
import { ProfileManagement } from "./ProfileManagement";
import { UsageInstructions } from "./UsageInstructions";
-// import { WikiButton } from "./WikiButton";
-// import { ClipboardButton } from "./ClipboardButton";
import { SmartClipboardButton } from "./SmartClipboardButton";
import { FgmodClipboardButton } from "./FgmodClipboardButton";
-// import { ClipboardDisplay } from "./ClipboardDisplay";
-// import { PluginUpdateChecker } from "./PluginUpdateChecker";
+import { FpsMultiplierControl } from "./FpsMultiplierControl";
import { NerdStuffModal } from "./NerdStuffModal";
-import FlatpaksModal from "./FlatpaksModal";
+import { FlatpaksModal } from "./FlatpaksModal";
import { ConfigurationData } from "../config/configSchema";
export function Content() {
@@ -102,13 +99,30 @@ export function Content() {
/>
</>
)}
-
- {/* Clipboard buttons - only show if installed */}
+
+ {/* FPS multiplier controls stay above profile selection when installed */}
{isInstalled && (
<>
- {/* <ClipboardDisplay /> */}
- <SmartClipboardButton />
- <FgmodClipboardButton />
+ <PanelSectionRow>
+ <div
+ style={{
+ fontSize: "14px",
+ fontWeight: "bold",
+ marginTop: "8px",
+ marginBottom: "6px",
+ borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
+ paddingBottom: "3px",
+ color: "white"
+ }}
+ >
+ FPS Multiplier
+ </div>
+ </PanelSectionRow>
+
+ <FpsMultiplierControl
+ config={config}
+ onConfigChange={handleConfigChange}
+ />
</>
)}
@@ -131,36 +145,17 @@ export function Content() {
/>
)}
- {/* Usage instructions - always visible for user guidance */}
- <UsageInstructions config={config} />
-
- {/* Wiki and clipboard buttons - always available for documentation */}
- {/* <WikiButton /> */}
- {/* <ClipboardButton /> */}
-
- {/* Plugin Update Checker */}
- {/* <PluginUpdateChecker /> */}
-
- {/* Show installation components at bottom when fully installed */}
+ {/* Clipboard buttons sit beside usage info for quick access */}
{isInstalled && (
<>
- <InstallationButton
- isInstalled={isInstalled}
- isInstalling={isInstalling}
- isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
- />
-
- <StatusDisplay
- dllDetected={dllDetected}
- dllDetectionStatus={dllDetectionStatus}
- isInstalled={isInstalled}
- installationStatus={installationStatus}
- />
+ <SmartClipboardButton />
+ <FgmodClipboardButton />
</>
)}
+ {/* Usage instructions - always visible for user guidance */}
+ <UsageInstructions config={config} />
+
{/* Nerd Stuff Button */}
<PanelSectionRow>
<ButtonItem
@@ -177,9 +172,29 @@ export function Content() {
layout="below"
onClick={handleShowFlatpaks}
>
- Flatpaks
+ Flatpak Setup
</ButtonItem>
</PanelSectionRow>
+
+ {/* Status and uninstall sit at bottom when installed to match desired layout */}
+ {isInstalled && (
+ <>
+ <StatusDisplay
+ dllDetected={dllDetected}
+ dllDetectionStatus={dllDetectionStatus}
+ isInstalled={isInstalled}
+ installationStatus={installationStatus}
+ />
+
+ <InstallationButton
+ isInstalled={isInstalled}
+ isInstalling={isInstalling}
+ isUninstalling={isUninstalling}
+ onInstall={onInstall}
+ onUninstall={onUninstall}
+ />
+ </>
+ )}
</PanelSection>
);
}
diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx
index ae0c333..ca69ec6 100644
--- a/src/components/FlatpaksModal.tsx
+++ b/src/components/FlatpaksModal.tsx
@@ -1,4 +1,4 @@
-import { FC, useState, useEffect } from 'react';
+import { FC, useState, useEffect, CSSProperties } from 'react';
import {
ModalRoot,
DialogBody,
@@ -32,7 +32,7 @@ interface FlatpaksModalProps {
closeModal?: () => void;
}
-const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
+export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null);
const [flatpakApps, setFlatpakApps] = useState<FlatpakAppInfo | null>(null);
const [loading, setLoading] = useState(true);
@@ -126,6 +126,40 @@ const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
);
}
+ const instructionSteps = [
+ {
+ id: 'try-first',
+ title: 'Try first:',
+ command: '~/lsfg'
+ },
+ {
+ id: 'try-full-path',
+ title: "If that doesn't work, try full path:",
+ command: '/home/(username)/lsfg'
+ },
+ {
+ id: 'final-result',
+ title: 'Final result should look like:',
+ command: '~/lsfg "usr/bin/flatpak"'
+ }
+ ];
+
+ const focusableInstructionStyle: CSSProperties = {
+ padding: '10px',
+ background: 'rgba(0, 0, 0, 0.3)',
+ borderRadius: '6px',
+ marginBottom: '12px'
+ };
+
+ const commandStyle: CSSProperties = {
+ fontFamily: 'monospace',
+ fontSize: '0.85em',
+ background: 'rgba(0, 0, 0, 0.45)',
+ padding: '8px',
+ borderRadius: '4px',
+ marginTop: '6px'
+ };
+
return (
<ModalRoot closeModal={closeModal}>
<DialogHeader>Flatpak Extensions</DialogHeader>
@@ -327,80 +361,57 @@ const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
{/* Steam Configuration Instructions */}
<DialogControlsSection>
<DialogControlsSectionHeader>Steam Configuration</DialogControlsSectionHeader>
-
- <Focusable>
- <div style={{
- padding: '12px',
- background: 'rgba(255, 255, 255, 0.1)',
- borderRadius: '8px',
- margin: '8px 0'
- }}>
- <div style={{ fontWeight: 'bold', marginBottom: '8px', color: '#fff' }}>
- Configure Steam Flatpak Shortcuts
- </div>
- <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '8px' }}>
- In Steam, open your flatpak game and click the cog wheel."
- </div>
- <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '12px', color: '#ffa500' }}>
- <strong>IMPORTANT:</strong> Set this in TARGET (NOT LAUNCH OPTIONS)
- </div>
-
- <div style={{ fontWeight: 'bold', marginBottom: '6px' }}>
- Try first:
- </div>
- <div style={{
- fontFamily: 'monospace',
- fontSize: '0.85em',
- background: 'rgba(0, 0, 0, 0.3)',
- padding: '8px',
- borderRadius: '4px',
- marginBottom: '12px'
- }}>
- ~/lsfg
- </div>
-
- <div style={{ fontWeight: 'bold', marginBottom: '6px' }}>
- If that doesn't work, try full path:
- </div>
- <div style={{
- fontFamily: 'monospace',
- fontSize: '0.85em',
- background: 'rgba(0, 0, 0, 0.3)',
- padding: '8px',
- borderRadius: '4px',
- marginBottom: '12px'
- }}>
- /home/(username)/lsfg
- </div>
-
- <div style={{ fontWeight: 'bold', marginBottom: '6px' }}>
- Final result should look like:
- </div>
- <div style={{
- fontFamily: 'monospace',
- fontSize: '0.85em',
- background: 'rgba(0, 0, 0, 0.3)',
- padding: '8px',
- borderRadius: '4px'
- }}>
- ~/lsfg "usr/bin/flatpak"
- </div>
-
- {/* Visual example image */}
- <div style={{ marginTop: '16px', textAlign: 'center' }}>
- <img
- src={flatpakTargetImage}
+ <div
+ style={{
+ padding: '12px',
+ background: 'rgba(255, 255, 255, 0.1)',
+ borderRadius: '8px',
+ margin: '8px 0',
+ display: 'flex',
+ flexDirection: 'column'
+ }}
+ >
+ <div style={{ fontWeight: 'bold', marginBottom: '8px', color: '#fff' }}>
+ Configure Steam Flatpak Shortcuts
+ </div>
+ <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '8px' }}>
+ In Steam, open your flatpak game and click the cog wheel.
+ </div>
+ <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '12px', color: '#ffa500' }}>
+ <strong>IMPORTANT:</strong> Set this in TARGET (NOT LAUNCH OPTIONS)
+ </div>
+
+ {instructionSteps.map((step) => (
+ <Focusable
+ key={step.id}
+ focusWithinClassName="gpfocuswithin"
+ onActivate={() => {}}
+ style={focusableInstructionStyle}
+ >
+ <div style={{ fontWeight: 'bold' }}>{step.title}</div>
+ <div style={commandStyle}>{step.command}</div>
+ </Focusable>
+ ))}
+
+ <Focusable
+ focusWithinClassName="gpfocuswithin"
+ onActivate={() => {}}
+ style={{ marginTop: '4px' }}
+ >
+ <div style={{ textAlign: 'center' }}>
+ <img
+ src={flatpakTargetImage}
alt="Steam Properties Target Field Example"
- style={{
- maxWidth: '100%',
+ style={{
+ maxWidth: '100%',
height: 'auto',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '4px'
}}
/>
</div>
- </div>
- </Focusable>
+ </Focusable>
+ </div>
</DialogControlsSection>
{/* Close Button */}
@@ -419,5 +430,3 @@ const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
</ModalRoot>
);
};
-
-export default FlatpaksModal;
diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx
index 49d2cd9..5ac31bb 100644
--- a/src/components/FpsMultiplierControl.tsx
+++ b/src/components/FpsMultiplierControl.tsx
@@ -15,8 +15,8 @@ export function FpsMultiplierControl({
<PanelSectionRow>
<Focusable
style={{
- marginTop: "10px",
- marginBottom: "10px",
+ marginTop: "6px",
+ marginBottom: "6px",
display: "flex",
justifyContent: "center",
alignItems: "center"
diff --git a/src/components/NerdStuffModal.tsx b/src/components/NerdStuffModal.tsx
index b5689c9..b4a79a2 100644
--- a/src/components/NerdStuffModal.tsx
+++ b/src/components/NerdStuffModal.tsx
@@ -3,7 +3,9 @@ import {
ModalRoot,
Field,
Focusable,
- Button
+ DialogControlsSection,
+ PanelSectionRow,
+ ButtonItem
} from "@decky/ui";
import { getDllStats, DllStatsResult, getConfigFileContent, getLaunchScriptContent, FileContentResult } from "../api/lsfgApi";
@@ -86,7 +88,7 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
</Focusable>
</Field>
- <Field label="SHA256 Hash">
+ <Field label="DLL SHA256 Hash">
<Focusable
onClick={() => dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)}
onActivate={() => dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)}
@@ -166,9 +168,17 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
</Field>
)}
- <Button onClick={closeModal}>
- Close
- </Button>
+ {/* Close Button */}
+ <DialogControlsSection>
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={closeModal}
+ >
+ Close
+ </ButtonItem>
+ </PanelSectionRow>
+ </DialogControlsSection>
</>
)}
</ModalRoot>
diff --git a/src/components/PluginUpdateChecker.tsx b/src/components/PluginUpdateChecker.tsx
deleted file mode 100644
index 9fa2afb..0000000
--- a/src/components/PluginUpdateChecker.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- ButtonItem,
- PanelSection,
- PanelSectionRow,
- Field,
- Focusable
-} from '@decky/ui';
-import { checkForPluginUpdate, downloadPluginUpdate, UpdateCheckResult, UpdateDownloadResult } from '../api/lsfgApi';
-
-interface PluginUpdateCheckerProps {
- // Add any props if needed
-}
-
-interface UpdateInfo {
- updateAvailable: boolean;
- currentVersion: string;
- latestVersion: string;
- releaseNotes: string;
- releaseDate: string;
- downloadUrl: string;
-}
-
-export const PluginUpdateChecker: React.FC<PluginUpdateCheckerProps> = () => {
- const [checkingUpdate, setCheckingUpdate] = useState(false);
- const [downloadingUpdate, setDownloadingUpdate] = useState(false);
- const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
- const [updateError, setUpdateError] = useState<string | null>(null);
- const [downloadResult, setDownloadResult] = useState<UpdateDownloadResult | null>(null);
-
- // Auto-hide error messages after 5 seconds
- useEffect(() => {
- if (updateError) {
- const timer = setTimeout(() => {
- setUpdateError(null);
- }, 5000);
- return () => clearTimeout(timer);
- }
- return undefined;
- }, [updateError]);
-
- const handleCheckForUpdate = async () => {
- setCheckingUpdate(true);
- setUpdateError(null);
- setUpdateInfo(null);
- setDownloadResult(null); // Clear previous download result
-
- try {
- const result: UpdateCheckResult = await checkForPluginUpdate();
-
- if (result.success) {
- setUpdateInfo({
- updateAvailable: result.update_available,
- currentVersion: result.current_version,
- latestVersion: result.latest_version,
- releaseNotes: result.release_notes,
- releaseDate: result.release_date,
- downloadUrl: result.download_url
- });
-
- // Simple console log instead of toast since showToast may not be available
- if (result.update_available) {
- console.log("Update available!", `Version ${result.latest_version} is now available.`);
- } else {
- console.log("Up to date!", "You have the latest version installed.");
- }
- } else {
- setUpdateError(result.error || "Failed to check for updates");
- }
- } catch (error) {
- setUpdateError(`Error checking for updates: ${error}`);
- } finally {
- setCheckingUpdate(false);
- }
- };
-
- const handleDownloadUpdate = async () => {
- if (!updateInfo?.downloadUrl) return;
-
- setDownloadingUpdate(true);
- setUpdateError(null);
- setDownloadResult(null);
-
- try {
- const result: UpdateDownloadResult = await downloadPluginUpdate(updateInfo.downloadUrl);
-
- if (result.success) {
- setDownloadResult(result);
- console.log("✓ Download complete!", `Plugin downloaded to ${result.download_path}`);
- } else {
- setUpdateError(result.error || "Failed to download update");
- }
- } catch (error) {
- setUpdateError(`Error downloading update: ${error}`);
- } finally {
- setDownloadingUpdate(false);
- }
- };
-
- const getStatusMessage = () => {
- if (!updateInfo) return null;
-
- if (updateInfo.updateAvailable) {
- if (downloadResult?.success) {
- return "✓ v" + updateInfo.latestVersion + " downloaded - ready to install";
- } else {
- return "Update available: v" + updateInfo.latestVersion;
- }
- } else {
- return "Up to date (v" + updateInfo.currentVersion + ")";
- }
- };
-
- return (
- <PanelSection title="PLUGIN UPDATES">
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleCheckForUpdate}
- disabled={checkingUpdate}
- description={getStatusMessage()}
- >
- {checkingUpdate ? 'Checking for updates...' : 'Check for Updates'}
- </ButtonItem>
- </PanelSectionRow>
-
- {updateInfo && updateInfo.updateAvailable && !downloadResult?.success && (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleDownloadUpdate}
- disabled={downloadingUpdate}
- description={`Download version ${updateInfo.latestVersion}`}
- >
- {downloadingUpdate ? 'Downloading...' : 'Download Update'}
- </ButtonItem>
- </PanelSectionRow>
- )}
-
- {downloadResult?.success && (
- <>
- <PanelSectionRow>
- <Field label="Download Complete!">
- <Focusable>
- File saved to: {downloadResult.download_path}
- </Focusable>
- </Field>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <Field label="Installation Instructions:">
- <Focusable>
- 1. Go to Decky Loader settings
- <br />2. Click "Developer" tab
- <br />3. Click "Uninstall" next to "decky-lsfg-vk"
- <br />4. Click "Install from ZIP"
- <br />5. Select the downloaded file
- <br />6. Restart Steam or reload plugins
- </Focusable>
- </Field>
- </PanelSectionRow>
- </>
- )}
-
- {updateError && (
- <PanelSectionRow>
- <Field label="Error:">
- <Focusable>
- {updateError}
- </Focusable>
- </Field>
- </PanelSectionRow>
- )}
- </PanelSection>
- );
-};
diff --git a/src/components/ProfileManagement.tsx b/src/components/ProfileManagement.tsx
index d3d15a9..62160d9 100644
--- a/src/components/ProfileManagement.tsx
+++ b/src/components/ProfileManagement.tsx
@@ -367,10 +367,10 @@ export function ProfileManagement({ currentProfile, onProfileChange }: ProfileMa
style={{
fontSize: "14px",
fontWeight: "bold",
- marginTop: "16px",
- marginBottom: "8px",
+ marginTop: "8px",
+ marginBottom: "6px",
borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
- paddingBottom: "4px",
+ paddingBottom: "3px",
color: "white"
}}
>
@@ -379,7 +379,10 @@ export function ProfileManagement({ currentProfile, onProfileChange }: ProfileMa
</PanelSectionRow>
<PanelSectionRow>
- <div className="LSFG_ProfilesCollapseButton_Container">
+ <div
+ className="LSFG_ProfilesCollapseButton_Container"
+ style={{ marginTop: "-2px", marginBottom: "4px" }}
+ >
<ButtonItem
layout="below"
bottomSeparator={profilesCollapsed ? "standard" : "none"}
diff --git a/src/components/WikiButton.tsx b/src/components/WikiButton.tsx
deleted file mode 100644
index 065fb8e..0000000
--- a/src/components/WikiButton.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { PanelSectionRow, ButtonItem } from "@decky/ui";
-import { FaBook } from "react-icons/fa";
-
-export function WikiButton() {
- const handleWikiClick = () => {
- window.open("https://github.com/PancakeTAS/lsfg-vk/wiki", "_blank");
- };
-
- return (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleWikiClick}
- >
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- <FaBook />
- <div>LSFG-VK Wiki</div>
- </div>
- </ButtonItem>
- </PanelSectionRow>
- );
-}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..bec45ae
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,11 @@
+export { Content } from "./Content";
+export { StatusDisplay } from "./StatusDisplay";
+export { InstallationButton } from "./InstallationButton";
+export { ConfigurationSection } from "./ConfigurationSection";
+export { FpsMultiplierControl } from "./FpsMultiplierControl";
+export { UsageInstructions } from "./UsageInstructions";
+export { SmartClipboardButton } from "./SmartClipboardButton";
+export { FgmodClipboardButton } from "./FgmodClipboardButton";
+export { NerdStuffModal } from "./NerdStuffModal";
+export { FlatpaksModal } from "./FlatpaksModal";
+export { ProfileManagement } from "./ProfileManagement";