summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/ConfigFileTab.tsx46
-rw-r--r--src/components/ConfigurationTab.tsx54
-rw-r--r--src/components/Content.tsx223
-rw-r--r--src/components/FlatpaksModal.tsx460
-rw-r--r--src/components/FlatpaksTab.tsx195
-rw-r--r--src/components/GameConfigurationSelector.tsx2
-rw-r--r--src/components/NerdStuffModal.tsx148
-rw-r--r--src/components/SetupTab.tsx47
-rw-r--r--src/components/SmartClipboardButton.tsx91
-rw-r--r--src/components/UsageInstructions.tsx69
-rw-r--r--src/components/index.ts8
11 files changed, 438 insertions, 905 deletions
diff --git a/src/components/ConfigFileTab.tsx b/src/components/ConfigFileTab.tsx
new file mode 100644
index 0000000..60c5c7e
--- /dev/null
+++ b/src/components/ConfigFileTab.tsx
@@ -0,0 +1,46 @@
+import { useEffect, useState } from "react";
+import { Field, Focusable, PanelSection, PanelSectionRow, Spinner } from "@decky/ui";
+import { getConfigFileContent, FileContentResult } from "../api/lsfgApi";
+import t from "../i18n/i18n";
+
+export function ConfigFileTab() {
+ const [result, setResult] = useState<FileContentResult | null>(null);
+
+ const copy = async (content: string) => {
+ try {
+ await navigator.clipboard.writeText(content);
+ } catch {
+ // Clipboard access is unavailable in some Deck UI contexts.
+ }
+ };
+
+ useEffect(() => {
+ getConfigFileContent().then(setResult).catch((error) => {
+ setResult({ success: false, error: String(error) });
+ });
+ }, []);
+
+ if (!result) {
+ return (
+ <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}>
+ <PanelSectionRow><Spinner /></PanelSectionRow>
+ </PanelSection>
+ );
+ }
+
+ return (
+ <PanelSection title={t("NERD_CONFIG_FILE", "Configuration File")}>
+ <PanelSectionRow>
+ <Field label={result.path || t("NERD_CONFIG_FILE", "Configuration File")} description={result.error || "Tap the file to copy it."}>
+ {result.success && result.content && (
+ <Focusable onActivate={() => void copy(result.content || "")}>
+ <pre style={{ maxHeight: "420px", overflow: "auto", whiteSpace: "pre-wrap", fontSize: "12px" }}>
+ {result.content}
+ </pre>
+ </Focusable>
+ )}
+ </Field>
+ </PanelSectionRow>
+ </PanelSection>
+ );
+}
diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx
new file mode 100644
index 0000000..6f3f474
--- /dev/null
+++ b/src/components/ConfigurationTab.tsx
@@ -0,0 +1,54 @@
+import { PanelSection } from "@decky/ui";
+import { ConfigurationData } from "../config/configSchema";
+import { GameTarget } from "../hooks/useGameConfiguration";
+import { ConfigurationSection } from "./ConfigurationSection";
+import { FgmodClipboardButton } from "./FgmodClipboardButton";
+import { FpsMultiplierControl } from "./FpsMultiplierControl";
+import { GameConfigurationSelector } from "./GameConfigurationSelector";
+import t from "../i18n/i18n";
+
+interface ConfigurationTabProps {
+ config: ConfigurationData;
+ targets: GameTarget[];
+ runningGame: GameTarget | null;
+ selectedAppId: string;
+ onSelect: (appid: string) => void;
+ onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise<void>;
+ onReset: () => Promise<void>;
+ onResetAll: () => Promise<void>;
+}
+
+export function ConfigurationTab({
+ config,
+ targets,
+ runningGame,
+ selectedAppId,
+ onSelect,
+ onConfigChange,
+ onReset,
+ onResetAll,
+}: ConfigurationTabProps) {
+ return (
+ <>
+ <PanelSection title={t("CONTENT_FPS_MULTIPLIER", "FPS Multiplier")}>
+ <FpsMultiplierControl config={config} onConfigChange={onConfigChange} />
+ </PanelSection>
+ <PanelSection title="Game Profile">
+ <GameConfigurationSelector
+ targets={targets}
+ runningGame={runningGame}
+ selectedAppId={selectedAppId}
+ onSelect={onSelect}
+ onReset={onReset}
+ onResetAll={onResetAll}
+ />
+ </PanelSection>
+ <PanelSection title="Options">
+ <ConfigurationSection config={config} onConfigChange={onConfigChange} />
+ </PanelSection>
+ <PanelSection>
+ <FgmodClipboardButton />
+ </PanelSection>
+ </>
+ );
+}
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index bdb3a04..de8f996 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,20 +1,22 @@
-import { useEffect } from "react";
-import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui";
-import { useInstallationStatus } from "../hooks/useLsfgHooks";
+import { Tabs } from "@decky/ui";
+import { useEffect, useState } from "react";
+import { FaFileAlt, FaGamepad, FaLayerGroup, FaTools } from "react-icons/fa";
+import { ConfigurationData } from "../config/configSchema";
+import { tabStyles } from "../styles";
import { useGameConfiguration } from "../hooks/useGameConfiguration";
import { useInstallationActions } from "../hooks/useInstallationActions";
-import { StatusDisplay } from "./StatusDisplay";
-import { InstallationButton } from "./InstallationButton";
-import { ConfigurationSection } from "./ConfigurationSection";
-import { GameConfigurationSelector } from "./GameConfigurationSelector";
-import { UsageInstructions } from "./UsageInstructions";
-import { SmartClipboardButton } from "./SmartClipboardButton";
-import { FgmodClipboardButton } from "./FgmodClipboardButton";
-import { FpsMultiplierControl } from "./FpsMultiplierControl";
-import { NerdStuffModal } from "./NerdStuffModal";
-import { FlatpaksModal } from "./FlatpaksModal";
-import { ConfigurationData } from "../config/configSchema";
-import t from '../i18n/i18n';
+import { useInstallationStatus } from "../hooks/useLsfgHooks";
+import { ConfigFileTab } from "./ConfigFileTab";
+import { ConfigurationTab } from "./ConfigurationTab";
+import { FlatpaksTab } from "./FlatpaksTab";
+import { SetupTab } from "./SetupTab";
+
+const tabIcons = {
+ configuration: <FaGamepad size={18} />,
+ flatpak: <FaLayerGroup size={18} />,
+ configFile: <FaFileAlt size={18} />,
+ setup: <FaTools size={18} />,
+};
export function Content() {
const {
@@ -25,141 +27,98 @@ export function Content() {
losslessScalingInstalled,
losslessScalingStatus,
steamBranchStatus,
- checkInstallation
+ checkInstallation,
} = useInstallationStatus();
-
- const { config, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload } = useGameConfiguration();
-
+ const {
+ config,
+ targets,
+ runningGame,
+ selectedAppId,
+ setSelectedAppId,
+ save,
+ resetSelected,
+ resetAll,
+ reload,
+ } = useGameConfiguration();
const { isInstalling, isUninstalling, handleInstall, handleUninstall } = useInstallationActions();
+ const [tab, setTab] = useState("Setup");
+ const setupComplete =
+ isInstalled &&
+ losslessScalingInstalled &&
+ steamBranchStatus?.success === true &&
+ steamBranchStatus.installed &&
+ !steamBranchStatus.needs_switch;
+
+ useEffect(() => {
+ setTab(setupComplete ? "Configuration" : "Setup");
+ }, [setupComplete]);
useEffect(() => {
if (isInstalled) void reload();
}, [isInstalled, reload]);
- const handleConfigChange = async (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => {
+ const handleConfigChange = async (
+ fieldName: keyof ConfigurationData,
+ value: boolean | number | string | string[],
+ ) => {
await save({ ...config, [fieldName]: value });
};
const onInstall = () => {
- handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation);
+ void handleInstall(setIsInstalled, setInstallationStatus, reload, checkInstallation);
};
const onUninstall = () => {
- handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation);
+ void handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation);
};
- const handleShowNerdStuff = () => {
- showModal(<NerdStuffModal />);
- };
+ const setupContent = (
+ <SetupTab
+ isInstalled={isInstalled}
+ installationStatus={installationStatus}
+ losslessScalingInstalled={losslessScalingInstalled}
+ losslessScalingStatus={losslessScalingStatus}
+ steamBranchStatus={steamBranchStatus}
+ isInstalling={isInstalling}
+ isUninstalling={isUninstalling}
+ onInstall={onInstall}
+ onUninstall={onUninstall}
+ />
+ );
- const handleShowFlatpaks = () => {
- showModal(<FlatpaksModal />);
- };
+ const tabs = setupComplete
+ ? [
+ {
+ id: "Configuration",
+ title: tabIcons.configuration,
+ content: (
+ <ConfigurationTab
+ config={config}
+ targets={targets}
+ runningGame={runningGame}
+ selectedAppId={selectedAppId}
+ onSelect={setSelectedAppId}
+ onConfigChange={handleConfigChange}
+ onReset={resetSelected}
+ onResetAll={resetAll}
+ />
+ ),
+ },
+ { id: "Flatpak", title: tabIcons.flatpak, content: <FlatpaksTab /> },
+ { id: "ConfigFile", title: tabIcons.configFile, content: <ConfigFileTab /> },
+ { id: "Setup", title: tabIcons.setup, content: setupContent },
+ ]
+ : [
+ { id: "Setup", title: tabIcons.setup, content: setupContent },
+ ];
return (
- <PanelSection>
- {!isInstalled && (
- <>
- <InstallationButton
- isInstalled={isInstalled}
- isInstalling={isInstalling}
- isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
- />
-
- <StatusDisplay
- isInstalled={isInstalled}
- installationStatus={installationStatus}
- losslessScalingInstalled={losslessScalingInstalled}
- losslessScalingStatus={losslessScalingStatus}
- steamBranchStatus={steamBranchStatus}
- />
- </>
- )}
-
- {isInstalled && (
- <>
- <PanelSectionRow>
- <div
- style={{
- fontSize: "14px",
- fontWeight: "bold",
- marginTop: "8px",
- marginBottom: "6px",
- borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
- paddingBottom: "3px",
- color: "white"
- }}
- >
- {t('CONTENT_FPS_MULTIPLIER', 'FPS Multiplier')}
- </div>
- </PanelSectionRow>
-
- <FpsMultiplierControl
- config={config}
- onConfigChange={handleConfigChange}
- />
- </>
- )}
-
- {isInstalled && (
- <GameConfigurationSelector targets={targets} runningGame={runningGame} selectedAppId={selectedAppId} onSelect={setSelectedAppId} onReset={resetSelected} onResetAll={resetAll} />
- )}
-
- {isInstalled && (
- <ConfigurationSection
- config={config}
- onConfigChange={handleConfigChange}
- />
- )}
-
- {isInstalled && (
- <>
- <SmartClipboardButton />
- <FgmodClipboardButton />
- </>
- )}
-
- <UsageInstructions />
-
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleShowNerdStuff}
- >
- {t('CONTENT_NERD_STUFF', 'Nerd Stuff')}
- </ButtonItem>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={handleShowFlatpaks}
- >
- {t('CONTENT_FLATPAK_SETUP', 'Flatpak Setup')}
- </ButtonItem>
- </PanelSectionRow>
-
- {isInstalled && (
- <>
- <StatusDisplay
- isInstalled={isInstalled}
- installationStatus={installationStatus}
- losslessScalingInstalled={losslessScalingInstalled}
- losslessScalingStatus={losslessScalingStatus}
- steamBranchStatus={steamBranchStatus}
- />
-
- <InstallationButton
- isInstalled={isInstalled}
- isInstalling={isInstalling}
- isUninstalling={isUninstalling}
- onInstall={onInstall}
- onUninstall={onUninstall}
- />
- </>
- )}
- </PanelSection>
+ <div
+ className="lsfg-vk-tabs"
+ style={{ height: "95%", width: "300px", position: "fixed", marginTop: "-12px", overflow: "hidden" }}
+ >
+ <style>{tabStyles}</style>
+ <Tabs activeTab={tab} onShowTab={setTab} tabs={tabs} />
+ </div>
);
}
diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx
deleted file mode 100644
index 8245160..0000000
--- a/src/components/FlatpaksModal.tsx
+++ /dev/null
@@ -1,460 +0,0 @@
-import { FC, useState, useEffect, CSSProperties } from 'react';
-import {
- ModalRoot,
- DialogBody,
- DialogHeader,
- DialogControlsSection,
- DialogControlsSectionHeader,
- ButtonItem,
- PanelSectionRow,
- Field,
- Toggle,
- Spinner,
- Focusable,
- showModal,
- ConfirmModal
-} from '@decky/ui';
-import { FaCheck, FaTimes, FaDownload, FaTrash, FaCog } from 'react-icons/fa';
-import flatpakTargetImage from '../../assets/flatpak-target.png';
-import {
- checkFlatpakExtensionStatus,
- installFlatpakExtension,
- uninstallFlatpakExtension,
- getFlatpakApps,
- setFlatpakAppOverride,
- removeFlatpakAppOverride,
- FlatpakExtensionStatus,
- FlatpakApp,
- FlatpakAppInfo
-} from '../api/lsfgApi';
-import t from '../i18n/i18n';
-import { showErrorToast } from '../utils/toastUtils';
-
-interface FlatpaksModalProps {
- closeModal?: () => void;
-}
-
-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);
- const [operationInProgress, setOperationInProgress] = useState<string | null>(null);
- const [operationError, setOperationError] = useState<string | null>(null);
-
- const loadData = async () => {
- setLoading(true);
- try {
- const [statusResult, appsResult] = await Promise.all([
- checkFlatpakExtensionStatus(),
- getFlatpakApps()
- ]);
-
- setExtensionStatus(statusResult);
- setFlatpakApps(appsResult);
- } catch (error) {
- console.error('Error loading Flatpak data:', error);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- loadData();
- }, []);
-
- const handleExtensionOperation = async (operation: 'install' | 'uninstall', version: string) => {
- const operationId = `${operation}-${version}`;
- setOperationInProgress(operationId);
- setOperationError(null);
-
- try {
- const result = operation === 'install'
- ? await installFlatpakExtension(version)
- : await uninstallFlatpakExtension(version);
-
- if (result.success) {
- // Reload status after operation
- const newStatus = await checkFlatpakExtensionStatus();
- setExtensionStatus(newStatus);
- } else {
- const message = result.error || result.message || 'Flatpak operation failed';
- setOperationError(message);
- showErrorToast('Flatpak operation failed', message);
- }
- } catch (error) {
- console.error(`Error ${operation}ing extension:`, error);
- const message = String(error);
- setOperationError(message);
- showErrorToast('Flatpak operation failed', message);
- } finally {
- setOperationInProgress(null);
- }
- };
-
- const handleAppOverrideToggle = async (app: FlatpakApp) => {
- const hasOverrides = app.has_filesystem_override && app.has_env_override;
- const operationId = `app-${app.app_id}`;
- setOperationInProgress(operationId);
- setOperationError(null);
-
- try {
- const result = hasOverrides
- ? await removeFlatpakAppOverride(app.app_id)
- : await setFlatpakAppOverride(app.app_id);
-
- if (result.success) {
- // Reload apps data after operation
- const newApps = await getFlatpakApps();
- setFlatpakApps(newApps);
- } else {
- const message = result.error || result.message || 'Flatpak override failed';
- setOperationError(message);
- showErrorToast('Flatpak override failed', message);
- }
- } catch (error) {
- console.error('Error toggling app override:', error);
- const message = String(error);
- setOperationError(message);
- showErrorToast('Flatpak override failed', message);
- } finally {
- setOperationInProgress(null);
- }
- };
-
- const confirmOperation = (operation: () => void, title: string, description: string) => {
- showModal(
- <ConfirmModal
- strTitle={title}
- strDescription={description}
- onOK={operation}
- onCancel={() => {}}
- />
- );
- };
-
- if (loading) {
- return (
- <ModalRoot closeModal={closeModal}>
- <DialogHeader>{t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')}</DialogHeader>
- <DialogBody>
- <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
- <Spinner />
- </div>
- </DialogBody>
- </ModalRoot>
- );
- }
-
- const instructionSteps = [
- {
- id: 'try-first',
- title: t('FLATPAK_STEP_TRY_FIRST', 'Try first:'),
- command: '~/lsfg'
- },
- {
- id: 'try-full-path',
- title: t('FLATPAK_STEP_TRY_FULL_PATH', "If that doesn't work, try full path:"),
- command: '/home/(username)/lsfg'
- },
- {
- id: 'final-result',
- title: t('FLATPAK_STEP_FINAL', '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>{t('FLATPAK_MODAL_TITLE', 'Flatpak Extensions')}</DialogHeader>
- <DialogBody>
- <Focusable>
- {/* Extension Status Section */}
- <DialogControlsSection>
- <DialogControlsSectionHeader>{t('FLATPAK_RUNTIME_INSTALLER', 'Runtime Extension Installer')}</DialogControlsSectionHeader>
-
- {operationError && (
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_OPERATION_ERROR', 'Operation failed')}
- description={operationError}
- icon={<FaTimes style={{color: 'red'}} />}
- />
- </PanelSectionRow>
- )}
-
- {extensionStatus && extensionStatus.success ? (
- <>
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_RUNTIME_23', 'Runtime 23.08')}
- description={extensionStatus.installed_23_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')}
- icon={extensionStatus.installed_23_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />}
- >
- <ButtonItem
- layout="below"
- onClick={() => {
- const operation = extensionStatus.installed_23_08 ? 'uninstall' : 'install';
- const action = () => handleExtensionOperation(operation, '23.08');
-
- if (operation === 'uninstall') {
- confirmOperation(
- action,
- t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'),
- `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 23.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}`
- );
- } else {
- action();
- }
- }}
- disabled={operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08'}
- >
- {operationInProgress === 'install-23.08' || operationInProgress === 'uninstall-23.08' ? (
- <Spinner />
- ) : extensionStatus.installed_23_08 ? (
- <>
- <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')}
- </>
- ) : (
- <>
- <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')}
- </>
- )}
- </ButtonItem>
- </Field>
- </PanelSectionRow>
-
- {/* 24.08 Runtime */}
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_RUNTIME_24', 'Runtime 24.08')}
- description={extensionStatus.installed_24_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')}
- icon={extensionStatus.installed_24_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />}
- >
- <ButtonItem
- layout="below"
- onClick={() => {
- const operation = extensionStatus.installed_24_08 ? 'uninstall' : 'install';
- const action = () => handleExtensionOperation(operation, '24.08');
-
- if (operation === 'uninstall') {
- confirmOperation(
- action,
- t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'),
- `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 24.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}`
- );
- } else {
- action();
- }
- }}
- disabled={operationInProgress === 'install-24.08' || operationInProgress === 'uninstall-24.08'}
- >
- {operationInProgress === 'install-24.08' || operationInProgress === 'uninstall-24.08' ? (
- <Spinner />
- ) : extensionStatus.installed_24_08 ? (
- <>
- <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')}
- </>
- ) : (
- <>
- <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')}
- </>
- )}
- </ButtonItem>
- </Field>
- </PanelSectionRow>
-
- {/* 25.08 Runtime */}
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_RUNTIME_25', 'Runtime 25.08')}
- description={extensionStatus.installed_25_08 ? t('FLATPAK_INSTALLED', 'Installed') : t('FLATPAK_NOT_INSTALLED', 'Not installed')}
- icon={extensionStatus.installed_25_08 ? <FaCheck style={{color: 'green'}} /> : <FaTimes style={{color: 'red'}} />}
- >
- <ButtonItem
- layout="below"
- onClick={() => {
- const operation = extensionStatus.installed_25_08 ? 'uninstall' : 'install';
- const action = () => handleExtensionOperation(operation, '25.08');
-
- if (operation === 'uninstall') {
- confirmOperation(
- action,
- t('FLATPAK_UNINSTALL_TITLE', 'Uninstall Runtime Extension'),
- `${t('FLATPAK_UNINSTALL_CONFIRM_PREFIX', 'Are you sure you want to uninstall the')} 25.08 ${t('FLATPAK_UNINSTALL_CONFIRM_SUFFIX', 'runtime extension?')}`
- );
- } else {
- action();
- }
- }}
- disabled={operationInProgress === 'install-25.08' || operationInProgress === 'uninstall-25.08'}
- >
- {operationInProgress === 'install-25.08' || operationInProgress === 'uninstall-25.08' ? (
- <Spinner />
- ) : extensionStatus.installed_25_08 ? (
- <>
- <FaTrash /> {t('FLATPAK_UNINSTALL_BTN', 'Uninstall')}
- </>
- ) : (
- <>
- <FaDownload /> {t('FLATPAK_INSTALL_BTN', 'Install')}
- </>
- )}
- </ButtonItem>
- </Field>
- </PanelSectionRow>
- </>
- ) : (
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_ERROR', 'Error')}
- description={extensionStatus?.error || t('FLATPAK_ERROR_STATUS', 'Failed to check extension status')}
- icon={<FaTimes style={{color: 'red'}} />}
- />
- </PanelSectionRow>
- )}
- </DialogControlsSection>
-
- {/* Flatpak Apps Section */}
- <DialogControlsSection>
- <DialogControlsSectionHeader>{t('FLATPAK_APPS_TITLE', 'Flatpak Applications')}</DialogControlsSectionHeader>
-
- {flatpakApps && flatpakApps.success ? (
- flatpakApps.apps.length > 0 ? (
- flatpakApps.apps.map((app) => {
- const hasOverrides = app.has_filesystem_override && app.has_env_override;
- const partialOverrides = app.has_filesystem_override || app.has_env_override;
-
- let statusColor = 'red';
- let statusText = t('FLATPAK_STATUS_NO_OVERRIDES', 'No overrides');
-
- if (hasOverrides) {
- statusColor = 'green';
- statusText = t('FLATPAK_STATUS_CONFIGURED', 'Configured');
- } else if (partialOverrides) {
- statusColor = 'orange';
- statusText = t('FLATPAK_STATUS_PARTIAL', 'Partial');
- }
-
- return (
- <PanelSectionRow key={app.app_id}>
- <Field
- label={app.app_name || app.app_id}
- description={`${app.app_id} - ${statusText}`}
- icon={<FaCog style={{color: statusColor}} />}
- >
- <Toggle
- value={hasOverrides}
- onChange={() => handleAppOverrideToggle(app)}
- disabled={operationInProgress === `app-${app.app_id}`}
- />
- </Field>
- </PanelSectionRow>
- );
- })
- ) : (
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_NO_APPS', 'No Flatpak Apps Found')}
- description={t('FLATPAK_NO_APPS_DESC', 'No Flatpak applications are currently installed')}
- />
- </PanelSectionRow>
- )
- ) : (
- <PanelSectionRow>
- <Field
- label={t('FLATPAK_ERROR', 'Error')}
- description={flatpakApps?.error || t('FLATPAK_ERROR_APPS', 'Failed to load Flatpak applications')}
- icon={<FaTimes style={{color: 'red'}} />}
- />
- </PanelSectionRow>
- )}
- </DialogControlsSection>
-
- {/* Steam Configuration Instructions */}
- <DialogControlsSection>
- <DialogControlsSectionHeader>{t('FLATPAK_STEAM_CONFIG_TITLE', 'Steam Configuration')}</DialogControlsSectionHeader>
- <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' }}>
- {t('FLATPAK_STEAM_CONFIG_HEADER', 'Configure Steam Flatpak Shortcuts')}
- </div>
- <div style={{ fontSize: '0.9em', lineHeight: '1.4', marginBottom: '8px' }}>
- {t('FLATPAK_STEAM_CONFIG_DESC', '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> {t('FLATPAK_STEAM_CONFIG_IMPORTANT', '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.replace(/ /g, '%20')}
- alt="Steam Properties Target Field Example"
- style={{
- maxWidth: '100%',
- height: 'auto',
- border: '1px solid rgba(255, 255, 255, 0.2)',
- borderRadius: '4px'
- }}
- />
- </div>
- </Focusable>
- </div>
- </DialogControlsSection>
-
- {/* Close Button */}
- <DialogControlsSection>
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={closeModal}
- >
- {t('FLATPAK_CLOSE', 'Close')}
- </ButtonItem>
- </PanelSectionRow>
- </DialogControlsSection>
- </Focusable>
- </DialogBody>
- </ModalRoot>
- );
-};
diff --git a/src/components/FlatpaksTab.tsx b/src/components/FlatpaksTab.tsx
new file mode 100644
index 0000000..dc73858
--- /dev/null
+++ b/src/components/FlatpaksTab.tsx
@@ -0,0 +1,195 @@
+import { useEffect, useState } from "react";
+import {
+ ButtonItem,
+ ConfirmModal,
+ Field,
+ PanelSection,
+ PanelSectionRow,
+ Spinner,
+ Toggle,
+ showModal,
+} from "@decky/ui";
+import { FaCheck, FaCog, FaDownload, FaTimes, FaTrash } from "react-icons/fa";
+import {
+ checkFlatpakExtensionStatus,
+ FlatpakApp,
+ FlatpakAppInfo,
+ FlatpakExtensionStatus,
+ getFlatpakApps,
+ installFlatpakExtension,
+ removeFlatpakAppOverride,
+ setFlatpakAppOverride,
+ uninstallFlatpakExtension,
+} from "../api/lsfgApi";
+import { showErrorToast } from "../utils/toastUtils";
+import t from "../i18n/i18n";
+
+const runtimeVersions = [
+ { version: "23.08", key: "installed_23_08" },
+ { version: "24.08", key: "installed_24_08" },
+ { version: "25.08", key: "installed_25_08" },
+] as const;
+
+interface RuntimeRowProps {
+ version: string;
+ installed: boolean;
+ busy: boolean;
+ onAction: () => void;
+}
+
+function RuntimeRow({ version, installed, busy, onAction }: RuntimeRowProps) {
+ return (
+ <PanelSectionRow>
+ <Field
+ label={`Runtime ${version}`}
+ description={installed ? t("FLATPAK_INSTALLED", "Installed") : t("FLATPAK_NOT_INSTALLED", "Not installed")}
+ icon={installed ? <FaCheck style={{ color: "green" }} /> : <FaTimes style={{ color: "red" }} />}
+ >
+ <ButtonItem layout="below" onClick={onAction} disabled={busy}>
+ {busy ? <Spinner /> : installed ? <><FaTrash /> {t("FLATPAK_UNINSTALL_BTN", "Uninstall")}</> : <><FaDownload /> {t("FLATPAK_INSTALL_BTN", "Install")}</>}
+ </ButtonItem>
+ </Field>
+ </PanelSectionRow>
+ );
+}
+
+interface AppRowProps {
+ app: FlatpakApp;
+ busy: boolean;
+ onToggle: () => void;
+}
+
+function AppRow({ app, busy, onToggle }: AppRowProps) {
+ const configured = app.has_filesystem_override && app.has_env_override;
+ const partial = app.has_filesystem_override || app.has_env_override;
+ const status = configured
+ ? t("FLATPAK_STATUS_CONFIGURED", "Configured")
+ : partial
+ ? t("FLATPAK_STATUS_PARTIAL", "Partial")
+ : t("FLATPAK_STATUS_NO_OVERRIDES", "No overrides");
+
+ return (
+ <PanelSectionRow>
+ <Field
+ label={app.app_name || app.app_id}
+ description={`${app.app_id} - ${status}`}
+ icon={<FaCog style={{ color: configured ? "green" : partial ? "orange" : "red" }} />}
+ >
+ <Toggle value={configured} onChange={onToggle} disabled={busy} />
+ </Field>
+ </PanelSectionRow>
+ );
+}
+
+export function FlatpaksTab() {
+ const [extensionStatus, setExtensionStatus] = useState<FlatpakExtensionStatus | null>(null);
+ const [apps, setApps] = useState<FlatpakAppInfo | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [operation, setOperation] = useState<string | null>(null);
+ const [error, setError] = useState<string | null>(null);
+
+ const load = async () => {
+ setLoading(true);
+ try {
+ const [nextStatus, nextApps] = await Promise.all([
+ checkFlatpakExtensionStatus(),
+ getFlatpakApps(),
+ ]);
+ setExtensionStatus(nextStatus);
+ setApps(nextApps);
+ } catch (loadError) {
+ setError(String(loadError));
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ void load();
+ }, []);
+
+ const runExtensionOperation = async (version: string, installed: boolean) => {
+ const action = installed ? "uninstall" : "install";
+ setOperation(`${action}-${version}`);
+ setError(null);
+ try {
+ const result = installed
+ ? await uninstallFlatpakExtension(version)
+ : await installFlatpakExtension(version);
+ if (!result.success) throw new Error(result.error || result.message);
+ setExtensionStatus(await checkFlatpakExtensionStatus());
+ } catch (operationError) {
+ const message = String(operationError);
+ setError(message);
+ showErrorToast("Flatpak operation failed", message);
+ } finally {
+ setOperation(null);
+ }
+ };
+
+ const confirmExtensionOperation = (version: string, installed: boolean) => {
+ if (!installed) {
+ void runExtensionOperation(version, false);
+ return;
+ }
+ showModal(
+ <ConfirmModal
+ strTitle={t("FLATPAK_UNINSTALL_TITLE", "Uninstall Runtime Extension")}
+ strDescription={`${t("FLATPAK_UNINSTALL_CONFIRM_PREFIX", "Are you sure you want to uninstall the")} ${version} ${t("FLATPAK_UNINSTALL_CONFIRM_SUFFIX", "runtime extension?")}`}
+ onOK={() => void runExtensionOperation(version, true)}
+ onCancel={() => {}}
+ />,
+ );
+ };
+
+ const toggleApp = async (app: FlatpakApp) => {
+ const configured = app.has_filesystem_override && app.has_env_override;
+ setOperation(`app-${app.app_id}`);
+ setError(null);
+ try {
+ const result = configured
+ ? await removeFlatpakAppOverride(app.app_id)
+ : await setFlatpakAppOverride(app.app_id);
+ if (!result.success) throw new Error(result.error || result.message);
+ setApps(await getFlatpakApps());
+ } catch (operationError) {
+ const message = String(operationError);
+ setError(message);
+ showErrorToast("Flatpak override failed", message);
+ } finally {
+ setOperation(null);
+ }
+ };
+
+ if (loading) {
+ return <PanelSection title={t("FLATPAK_MODAL_TITLE", "Flatpak Extensions")}><PanelSectionRow><Spinner /></PanelSectionRow></PanelSection>;
+ }
+
+ return (
+ <>
+ <PanelSection title={t("FLATPAK_RUNTIME_INSTALLER", "Runtime Extension Installer")}>
+ {error && <PanelSectionRow><Field label={t("FLATPAK_OPERATION_ERROR", "Operation failed")} description={error} icon={<FaTimes style={{ color: "red" }} />} /></PanelSectionRow>}
+ {extensionStatus?.success ? runtimeVersions.map(({ version, key }) => (
+ <RuntimeRow
+ key={version}
+ version={version}
+ installed={extensionStatus[key]}
+ busy={operation === `${extensionStatus[key] ? "uninstall" : "install"}-${version}`}
+ onAction={() => confirmExtensionOperation(version, extensionStatus[key])}
+ />
+ )) : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={extensionStatus?.error || error || t("FLATPAK_ERROR_STATUS", "Failed to check extension status")} /></PanelSectionRow>}
+ </PanelSection>
+
+ <PanelSection title={t("FLATPAK_APPS_TITLE", "Flatpak Applications")}>
+ {apps?.success ? apps.apps.length ? apps.apps.map((app) => (
+ <AppRow
+ key={app.app_id}
+ app={app}
+ busy={operation === `app-${app.app_id}`}
+ onToggle={() => void toggleApp(app)}
+ />
+ )) : <PanelSectionRow><Field label={t("FLATPAK_NO_APPS", "No Flatpak Apps Found")} description={t("FLATPAK_NO_APPS_DESC", "No Flatpak applications are currently installed")} /></PanelSectionRow> : <PanelSectionRow><Field label={t("FLATPAK_ERROR", "Error")} description={apps?.error || error || t("FLATPAK_ERROR_APPS", "Failed to load Flatpak applications")} /></PanelSectionRow>}
+ </PanelSection>
+ </>
+ );
+}
diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx
index 8a0df6c..a231477 100644
--- a/src/components/GameConfigurationSelector.tsx
+++ b/src/components/GameConfigurationSelector.tsx
@@ -13,7 +13,7 @@ interface Props {
export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) {
const options: DropdownOption[] = [
{ data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" },
- ...targets.map((target) => ({ data: target.appid, label: `${target.name} · ${target.appid}` })),
+ ...targets.map((target) => ({ data: target.appid, label: `${target.nonSteam ? "Non-Steam · " : ""}${target.name} · ${target.appid}` })),
];
return <>
<PanelSectionRow>
diff --git a/src/components/NerdStuffModal.tsx b/src/components/NerdStuffModal.tsx
deleted file mode 100644
index f075ccb..0000000
--- a/src/components/NerdStuffModal.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { useState, useEffect } from "react";
-import {
- ModalRoot,
- Field,
- Focusable,
- DialogControlsSection,
- PanelSectionRow,
- ButtonItem
-} from "@decky/ui";
-import {
- getConfigFileContent,
- getLaunchScriptContent,
- FileContentResult,
-} from "../api/lsfgApi";
-import t from '../i18n/i18n';
-
-interface NerdStuffModalProps {
- closeModal?: () => void;
-}
-
-export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
- const [configContent, setConfigContent] = useState<FileContentResult | null>(null);
- const [scriptContent, setScriptContent] = useState<FileContentResult | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
-
- useEffect(() => {
- const loadData = async () => {
- try {
- setLoading(true);
- setError(null);
-
- // Load all data in parallel
- const [configResult, scriptResult] = await Promise.all([
- getConfigFileContent(),
- getLaunchScriptContent(),
- ]);
-
- setConfigContent(configResult);
- setScriptContent(scriptResult);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to load data");
- } finally {
- setLoading(false);
- }
- };
-
- loadData();
- }, []);
-
- const copyToClipboard = async (text: string) => {
- try {
- await navigator.clipboard.writeText(text);
- // Could add a toast notification here if desired
- } catch (err) {
- console.error("Failed to copy to clipboard:", err);
- }
- };
-
- return (
- <ModalRoot onCancel={closeModal} onOK={closeModal}>
- {loading && (
- <div>{t('NERD_LOADING', 'Loading information...')}</div>
- )}
-
- {error && (
- <div>Error: {error}</div>
- )}
-
- {!loading && !error && (
- <>
- {/* Launch Script Section */}
- {scriptContent && (
- <Field label={t('NERD_LAUNCH_SCRIPT', 'Launch Script')}>
- {!scriptContent.success ? (
- <div>{t('NERD_SCRIPT_NOT_FOUND_PREFIX', 'Script not found:')} {scriptContent.error}</div>
- ) : (
- <div>
- <div style={{ marginBottom: "8px", fontSize: "0.9em", opacity: 0.8 }}>
- {t('NERD_PATH_PREFIX', 'Path:')} {scriptContent.path}
- </div>
- <Focusable
- onClick={() => scriptContent.content && copyToClipboard(scriptContent.content)}
- onActivate={() => scriptContent.content && copyToClipboard(scriptContent.content)}
- >
- <pre style={{
- background: "rgba(255, 255, 255, 0.1)",
- padding: "8px",
- borderRadius: "4px",
- fontSize: "0.8em",
- whiteSpace: "pre-wrap",
- overflow: "auto",
- maxHeight: "150px"
- }}>
- {scriptContent.content || t('NERD_NO_CONTENT', 'No content')}
- </pre>
- </Focusable>
- </div>
- )}
- </Field>
- )}
-
- {/* Config File Section */}
- {configContent && (
- <Field label={t('NERD_CONFIG_FILE', 'Configuration File')}>
- {!configContent.success ? (
- <div>{t('NERD_CONFIG_NOT_FOUND_PREFIX', 'Config not found:')} {configContent.error}</div>
- ) : (
- <div>
- <div style={{ marginBottom: "8px", fontSize: "0.9em", opacity: 0.8 }}>
- {t('NERD_PATH_PREFIX', 'Path:')} {configContent.path}
- </div>
- <Focusable
- onClick={() => configContent.content && copyToClipboard(configContent.content)}
- onActivate={() => configContent.content && copyToClipboard(configContent.content)}
- >
- <pre style={{
- background: "rgba(255, 255, 255, 0.1)",
- padding: "8px",
- borderRadius: "4px",
- fontSize: "0.8em",
- whiteSpace: "pre-wrap",
- overflow: "auto"
- }}>
- {configContent.content || t('NERD_NO_CONTENT', 'No content')}
- </pre>
- </Focusable>
- </div>
- )}
- </Field>
- )}
-
- {/* Close Button */}
- <DialogControlsSection>
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={closeModal}
- >
- {t('NERD_CLOSE', 'Close')}
- </ButtonItem>
- </PanelSectionRow>
- </DialogControlsSection>
- </>
- )}
- </ModalRoot>
- );
-}
diff --git a/src/components/SetupTab.tsx b/src/components/SetupTab.tsx
new file mode 100644
index 0000000..34106ed
--- /dev/null
+++ b/src/components/SetupTab.tsx
@@ -0,0 +1,47 @@
+import { PanelSection } from "@decky/ui";
+import type { SteamBranchStatus } from "../api/lsfgApi";
+import { InstallationButton } from "./InstallationButton";
+import { StatusDisplay } from "./StatusDisplay";
+
+interface SetupTabProps {
+ isInstalled: boolean;
+ installationStatus: string;
+ losslessScalingInstalled: boolean;
+ losslessScalingStatus: string;
+ steamBranchStatus: SteamBranchStatus | null;
+ isInstalling: boolean;
+ isUninstalling: boolean;
+ onInstall: () => void;
+ onUninstall: () => void;
+}
+
+export function SetupTab({
+ isInstalled,
+ installationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus,
+ steamBranchStatus,
+ isInstalling,
+ isUninstalling,
+ onInstall,
+ onUninstall,
+}: SetupTabProps) {
+ return (
+ <PanelSection title="Setup">
+ <StatusDisplay
+ isInstalled={isInstalled}
+ installationStatus={installationStatus}
+ losslessScalingInstalled={losslessScalingInstalled}
+ losslessScalingStatus={losslessScalingStatus}
+ steamBranchStatus={steamBranchStatus}
+ />
+ <InstallationButton
+ isInstalled={isInstalled}
+ isInstalling={isInstalling}
+ isUninstalling={isUninstalling}
+ onInstall={onInstall}
+ onUninstall={onUninstall}
+ />
+ </PanelSection>
+ );
+}
diff --git a/src/components/SmartClipboardButton.tsx b/src/components/SmartClipboardButton.tsx
deleted file mode 100644
index 8da239a..0000000
--- a/src/components/SmartClipboardButton.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { useState, useEffect } from "react";
-import { PanelSectionRow, ButtonItem } from "@decky/ui";
-import { FaClipboard, FaCheck } from "react-icons/fa";
-import { getLaunchOption } from "../api/lsfgApi";
-import { showClipboardErrorToast } from "../utils/toastUtils";
-import { copyWithVerification } from "../utils/clipboardUtils";
-import t from '../i18n/i18n';
-
-export function SmartClipboardButton() {
- const [isLoading, setIsLoading] = useState(false);
- const [showSuccess, setShowSuccess] = useState(false);
-
- useEffect(() => {
- if (showSuccess) {
- const timer = setTimeout(() => {
- setShowSuccess(false);
- }, 3000);
- return () => clearTimeout(timer);
- }
- return undefined;
- }, [showSuccess]);
-
- const getLaunchOptionText = async (): Promise<string> => {
- try {
- const result = await getLaunchOption();
- return result.launch_option || "~/lsfg %command%";
- } catch (error) {
- return "~/lsfg %command%";
- }
- };
-
- const copyToClipboard = async () => {
- if (isLoading || showSuccess) return;
-
- setIsLoading(true);
- try {
- const text = await getLaunchOptionText();
- const { success, verified } = await copyWithVerification(text);
-
- if (success) {
- setShowSuccess(true);
- if (!verified) {
- console.log('Copy verification failed but copy likely worked');
- }
- } else {
- showClipboardErrorToast();
- }
-
- } catch (error) {
- showClipboardErrorToast();
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
- <PanelSectionRow>
- <ButtonItem
- layout="below"
- onClick={copyToClipboard}
- disabled={isLoading || showSuccess}
- >
- <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
- {showSuccess ? (
- <FaCheck style={{ color: "#4CAF50" }} />
- ) : isLoading ? (
- <FaClipboard style={{
- animation: "pulse 1s ease-in-out infinite",
- opacity: 0.7
- }} />
- ) : (
- <FaClipboard />
- )}
- <div style={{
- color: showSuccess ? "#4CAF50" : "inherit",
- fontWeight: showSuccess ? "bold" : "normal"
- }}>
- {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_COPY_LAUNCH', 'Copy Launch Option')}
- </div>
- </div>
- </ButtonItem>
- <style>{`
- @keyframes pulse {
- 0% { opacity: 0.7; }
- 50% { opacity: 1; }
- 100% { opacity: 0.7; }
- }
- `}</style>
- </PanelSectionRow>
- );
-}
diff --git a/src/components/UsageInstructions.tsx b/src/components/UsageInstructions.tsx
deleted file mode 100644
index 36e31cf..0000000
--- a/src/components/UsageInstructions.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { PanelSectionRow } from "@decky/ui";
-import t from '../i18n/i18n';
-
-export function UsageInstructions() {
- return (
- <>
- <PanelSectionRow>
- <div
- style={{
- fontSize: "14px",
- fontWeight: "bold",
- marginTop: "16px",
- marginBottom: "8px",
- borderBottom: "1px solid rgba(255, 255, 255, 0.2)",
- paddingBottom: "4px",
- color: "white"
- }}
- >
- {t('USAGE_TITLE', 'Usage Instructions')}
- </div>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <div
- style={{
- fontSize: "12px",
- lineHeight: "1.4",
- opacity: "0.8",
- whiteSpace: "pre-wrap"
- }}
- >
- {t('USAGE_DESC', 'Click "Copy Launch Option" button, then paste it into your Steam game\'s launch options to enable frame generation.')}
- </div>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <div
- style={{
- fontSize: "12px",
- lineHeight: "1.4",
- opacity: "0.8",
- backgroundColor: "rgba(255, 255, 255, 0.1)",
- padding: "8px",
- borderRadius: "4px",
- fontFamily: "monospace",
- marginTop: "8px",
- marginBottom: "8px",
- textAlign: "center"
- }}
- >
- <strong>~/lsfg %command%</strong>
- </div>
- </PanelSectionRow>
-
- <PanelSectionRow>
- <div
- style={{
- fontSize: "11px",
- lineHeight: "1.3",
- opacity: "0.6",
- marginTop: "8px"
- }}
- >
- {t('USAGE_CONFIG_NOTE', 'The configuration is stored in ~/.config/lsfg-vk/conf.toml and hot-reloads while games are running.')}
- </div>
- </PanelSectionRow>
- </>
- );
-}
diff --git a/src/components/index.ts b/src/components/index.ts
index 4284aee..5089b6f 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -3,9 +3,9 @@ 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 { ConfigurationTab } from "./ConfigurationTab";
+export { SetupTab } from "./SetupTab";
+export { ConfigFileTab } from "./ConfigFileTab";
+export { FlatpaksTab } from "./FlatpaksTab";
export { GameConfigurationSelector } from "./GameConfigurationSelector";