summaryrefslogtreecommitdiff
path: root/src/components/FlatpaksModal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/FlatpaksModal.tsx')
-rw-r--r--src/components/FlatpaksModal.tsx383
1 files changed, 383 insertions, 0 deletions
diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx
index e69de29..bd81013 100644
--- a/src/components/FlatpaksModal.tsx
+++ b/src/components/FlatpaksModal.tsx
@@ -0,0 +1,383 @@
+import { FC, useState, useEffect } 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';
+
+interface FlatpaksModalProps {
+ closeModal?: () => void;
+}
+
+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 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);
+
+ try {
+ const result = operation === 'install'
+ ? await installFlatpakExtension(version)
+ : await uninstallFlatpakExtension(version);
+
+ if (result.success) {
+ // Reload status after operation
+ const newStatus = await checkFlatpakExtensionStatus();
+ setExtensionStatus(newStatus);
+ }
+ } catch (error) {
+ console.error(`Error ${operation}ing extension:`, error);
+ } 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);
+
+ 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);
+ }
+ } catch (error) {
+ console.error('Error toggling app override:', error);
+ } 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>Flatpak Extensions</DialogHeader>
+ <DialogBody>
+ <div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
+ <Spinner />
+ </div>
+ </DialogBody>
+ </ModalRoot>
+ );
+ }
+
+ return (
+ <ModalRoot closeModal={closeModal}>
+ <DialogHeader>Flatpak Extensions</DialogHeader>
+ <DialogBody>
+ <Focusable>
+ {/* Extension Status Section */}
+ <DialogControlsSection>
+ <DialogControlsSectionHeader>Runtime Extension Installer</DialogControlsSectionHeader>
+
+ {extensionStatus && extensionStatus.success ? (
+ <>
+ {/* 23.08 Runtime */}
+ <PanelSectionRow>
+ <Field
+ label="Runtime 23.08"
+ description={extensionStatus.installed_23_08 ? "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,
+ 'Uninstall Runtime Extension',
+ 'Are you sure you want to uninstall the 23.08 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 /> Uninstall
+ </>
+ ) : (
+ <>
+ <FaDownload /> Install
+ </>
+ )}
+ </ButtonItem>
+ </Field>
+ </PanelSectionRow>
+
+ {/* 24.08 Runtime */}
+ <PanelSectionRow>
+ <Field
+ label="Runtime 24.08"
+ description={extensionStatus.installed_24_08 ? "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,
+ 'Uninstall Runtime Extension',
+ 'Are you sure you want to uninstall the 24.08 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 /> Uninstall
+ </>
+ ) : (
+ <>
+ <FaDownload /> Install
+ </>
+ )}
+ </ButtonItem>
+ </Field>
+ </PanelSectionRow>
+ </>
+ ) : (
+ <PanelSectionRow>
+ <Field
+ label="Error"
+ description={extensionStatus?.error || 'Failed to check extension status'}
+ icon={<FaTimes style={{color: 'red'}} />}
+ />
+ </PanelSectionRow>
+ )}
+ </DialogControlsSection>
+
+ {/* Flatpak Apps Section */}
+ <DialogControlsSection>
+ <DialogControlsSectionHeader>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 = 'No overrides';
+
+ if (hasOverrides) {
+ statusColor = 'green';
+ statusText = 'Configured';
+ } else if (partialOverrides) {
+ statusColor = 'orange';
+ statusText = '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="No Flatpak Apps Found"
+ description="No Flatpak applications are currently installed"
+ />
+ </PanelSectionRow>
+ )
+ ) : (
+ <PanelSectionRow>
+ <Field
+ label="Error"
+ description={flatpakApps?.error || 'Failed to load Flatpak applications'}
+ icon={<FaTimes style={{color: 'red'}} />}
+ />
+ </PanelSectionRow>
+ )}
+ </DialogControlsSection>
+
+ {/* 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}
+ alt="Steam Properties Target Field Example"
+ style={{
+ maxWidth: '100%',
+ height: 'auto',
+ border: '1px solid rgba(255, 255, 255, 0.2)',
+ borderRadius: '4px'
+ }}
+ />
+ </div>
+ </div>
+ </Focusable>
+ </DialogControlsSection>
+
+ {/* Close Button */}
+ <DialogControlsSection>
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={closeModal}
+ >
+ Close
+ </ButtonItem>
+ </PanelSectionRow>
+ </DialogControlsSection>
+ </Focusable>
+ </DialogBody>
+ </ModalRoot>
+ );
+};
+
+export default FlatpaksModal;