summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--py_modules/lsfg_vk/constants.py4
-rw-r--r--py_modules/lsfg_vk/flatpak_service.py400
-rw-r--r--py_modules/lsfg_vk/plugin.py63
-rw-r--r--src/api/lsfgApi.ts40
-rw-r--r--src/components/Content.tsx15
-rw-r--r--src/components/FlatpaksModal.tsx291
-rw-r--r--src/components/index.ts1
7 files changed, 814 insertions, 0 deletions
diff --git a/py_modules/lsfg_vk/constants.py b/py_modules/lsfg_vk/constants.py
index 69989c1..614e2fc 100644
--- a/py_modules/lsfg_vk/constants.py
+++ b/py_modules/lsfg_vk/constants.py
@@ -17,6 +17,10 @@ LIB_FILENAME = "liblsfg-vk.so"
JSON_FILENAME = "VkLayer_LS_frame_generation.json"
ZIP_FILENAME = "lsfg-vk_noui.zip"
+# Flatpak files
+FLATPAK_23_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak"
+FLATPAK_24_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak"
+
# File extensions
SO_EXT = ".so"
JSON_EXT = ".json"
diff --git a/py_modules/lsfg_vk/flatpak_service.py b/py_modules/lsfg_vk/flatpak_service.py
index e69de29..3fb621d 100644
--- a/py_modules/lsfg_vk/flatpak_service.py
+++ b/py_modules/lsfg_vk/flatpak_service.py
@@ -0,0 +1,400 @@
+"""
+Flatpak service for managing lsfg-vk Flatpak runtime extensions.
+"""
+
+import subprocess
+import os
+from pathlib import Path
+from typing import Dict, Any, List, Optional
+
+from .base_service import BaseService
+from .constants import (
+ FLATPAK_23_08_FILENAME, FLATPAK_24_08_FILENAME, BIN_DIR, CONFIG_DIR
+)
+from .types import BaseResponse
+
+
+class FlatpakExtensionStatus(BaseResponse):
+ """Response for Flatpak extension status"""
+ def __init__(self, success: bool = False, message: str = "", error: str = "",
+ installed_23_08: bool = False, installed_24_08: bool = False):
+ super().__init__(success, message, error)
+ self.installed_23_08 = installed_23_08
+ self.installed_24_08 = installed_24_08
+
+
+class FlatpakAppInfo(BaseResponse):
+ """Response for Flatpak app information"""
+ def __init__(self, success: bool = False, message: str = "", error: str = "",
+ apps: List[Dict[str, Any]] = None, total_apps: int = 0):
+ super().__init__(success, message, error)
+ self.apps = apps or []
+ self.total_apps = total_apps
+
+
+class FlatpakOverrideResponse(BaseResponse):
+ """Response for Flatpak override operations"""
+ def __init__(self, success: bool = False, message: str = "", error: str = "",
+ app_id: str = "", operation: str = ""):
+ super().__init__(success, message, error)
+ self.app_id = app_id
+ self.operation = operation
+
+
+class FlatpakService(BaseService):
+ """Service for handling Flatpak runtime extensions and app overrides"""
+
+ def __init__(self, logger=None):
+ super().__init__(logger)
+ self.extension_id_23_08 = "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/23.08"
+ self.extension_id_24_08 = "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/24.08"
+ self.flatpak_command = None # Will be set when flatpak is detected
+ def __init__(self, logger=None):
+ super().__init__(logger)
+ self.extension_id_23_08 = "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/23.08"
+ self.extension_id_24_08 = "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/24.08"
+ self.flatpak_command = None # Will be set when flatpak is detected
+
+ def _get_clean_env(self):
+ """Get a clean environment without PyInstaller's bundled libraries"""
+ import os
+
+ # Create a clean environment without PyInstaller's bundled libraries
+ env = os.environ.copy()
+
+ # Remove LD_LIBRARY_PATH that might point to PyInstaller's bundled libs
+ if 'LD_LIBRARY_PATH' in env:
+ del env['LD_LIBRARY_PATH']
+
+ # Ensure PATH includes standard binary locations
+ standard_paths = ['/usr/bin', '/usr/local/bin', '/bin']
+ current_path = env.get('PATH', '')
+
+ # Add standard paths if they're not already there
+ path_parts = current_path.split(':') if current_path else []
+ for std_path in standard_paths:
+ if std_path not in path_parts:
+ path_parts.insert(0, std_path)
+
+ env['PATH'] = ':'.join(path_parts)
+
+ return env
+
+ def _run_flatpak_command(self, args: List[str], **kwargs):
+ """Run flatpak command with clean environment to avoid library conflicts"""
+ if self.flatpak_command is None:
+ raise FileNotFoundError("Flatpak command not available")
+
+ env = self._get_clean_env()
+
+ # Log environment info for debugging
+ self.log.info(f"Running flatpak with PATH: {env.get('PATH')}")
+ self.log.info(f"LD_LIBRARY_PATH removed: {'LD_LIBRARY_PATH' not in env}")
+
+ # Run the command with the clean environment
+ return subprocess.run([self.flatpak_command] + args, env=env, **kwargs)
+
+ def check_flatpak_available(self) -> bool:
+ """Check if flatpak command is available and store the working command"""
+ import os
+
+ # Log environment info for debugging
+ self.log.info(f"PATH: {os.environ.get('PATH', 'Not set')}")
+ self.log.info(f"HOME: {os.environ.get('HOME', 'Not set')}")
+ self.log.info(f"USER: {os.environ.get('USER', 'Not set')}")
+
+ # Try common flatpak installation paths, starting with the standard command
+ flatpak_paths = [
+ "flatpak",
+ "/usr/bin/flatpak",
+ "/var/lib/flatpak/exports/bin/flatpak",
+ "/home/deck/.local/bin/flatpak"
+ ]
+
+ for flatpak_path in flatpak_paths:
+ try:
+ result = subprocess.run([flatpak_path, "--version"],
+ capture_output=True, check=True, text=True,
+ env=self._get_clean_env())
+ self.log.info(f"Flatpak found at {flatpak_path}: {result.stdout.strip()}")
+ self.flatpak_command = flatpak_path
+ return True
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ self.log.debug(f"Flatpak not found at {flatpak_path}")
+ continue
+
+ self.log.error("Flatpak command not found in any known locations")
+ self.flatpak_command = None
+ return False
+
+ def get_extension_status(self) -> FlatpakExtensionStatus:
+ """Check if lsfg-vk Flatpak extensions are installed"""
+ try:
+ if not self.check_flatpak_available():
+ error_msg = "Flatpak is not available on this system"
+ if self.flatpak_command is None:
+ error_msg += ". Command not found in PATH or common install locations."
+ self.log.error(error_msg)
+ return self._error_response(FlatpakExtensionStatus,
+ error_msg,
+ installed_23_08=False, installed_24_08=False)
+
+ # Get list of installed runtimes
+ result = self._run_flatpak_command(
+ ["list", "--runtime"],
+ capture_output=True, text=True, check=True
+ )
+
+ installed_runtimes = result.stdout
+
+ # Check for both versions by looking for the base extension name and version
+ base_extension_name = "org.freedesktop.Platform.VulkanLayer.lsfgvk"
+ installed_23_08 = False
+ installed_24_08 = False
+
+ for line in installed_runtimes.split('\n'):
+ if base_extension_name in line:
+ if "23.08" in line:
+ installed_23_08 = True
+ elif "24.08" in line:
+ installed_24_08 = True
+
+ status_msg = []
+ if installed_23_08:
+ status_msg.append("23.08 runtime extension installed")
+ if installed_24_08:
+ status_msg.append("24.08 runtime extension installed")
+
+ if not status_msg:
+ status_msg.append("No lsfg-vk runtime extensions installed")
+
+ return self._success_response(FlatpakExtensionStatus,
+ "; ".join(status_msg),
+ installed_23_08=installed_23_08,
+ installed_24_08=installed_24_08)
+
+ except subprocess.CalledProcessError as e:
+ error_msg = f"Error checking Flatpak extensions: {e.stderr if e.stderr else str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(FlatpakExtensionStatus, error_msg,
+ installed_23_08=False, installed_24_08=False)
+
+ def install_extension(self, version: str) -> BaseResponse:
+ """Install a specific version of the lsfg-vk Flatpak extension"""
+ try:
+ if version not in ["23.08", "24.08"]:
+ return self._error_response(BaseResponse, "Invalid version. Must be '23.08' or '24.08'")
+
+ if not self.check_flatpak_available():
+ return self._error_response(BaseResponse, "Flatpak is not available on this system")
+
+ # Get the path to the flatpak file
+ plugin_dir = Path(__file__).parent.parent.parent
+ filename = FLATPAK_23_08_FILENAME if version == "23.08" else FLATPAK_24_08_FILENAME
+ flatpak_path = plugin_dir / BIN_DIR / filename
+
+ if not flatpak_path.exists():
+ return self._error_response(BaseResponse, f"Flatpak file not found: {flatpak_path}")
+
+ # Install the extension
+ result = self._run_flatpak_command(
+ ["install", "--user", "--noninteractive", str(flatpak_path)],
+ capture_output=True, text=True
+ )
+
+ if result.returncode != 0:
+ error_msg = f"Failed to install Flatpak extension: {result.stderr}"
+ self.log.error(error_msg)
+ return self._error_response(BaseResponse, error_msg)
+
+ self.log.info(f"Successfully installed lsfg-vk Flatpak extension {version}")
+ return self._success_response(BaseResponse, f"lsfg-vk {version} runtime extension installed successfully")
+
+ except Exception as e:
+ error_msg = f"Error installing Flatpak extension {version}: {str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(BaseResponse, error_msg)
+
+ def uninstall_extension(self, version: str) -> BaseResponse:
+ """Uninstall a specific version of the lsfg-vk Flatpak extension"""
+ try:
+ if version not in ["23.08", "24.08"]:
+ return self._error_response(BaseResponse, "Invalid version. Must be '23.08' or '24.08'")
+
+ if not self.check_flatpak_available():
+ return self._error_response(BaseResponse, "Flatpak is not available on this system")
+
+ extension_id = self.extension_id_23_08 if version == "23.08" else self.extension_id_24_08
+
+ # Uninstall the extension
+ result = self._run_flatpak_command(
+ ["uninstall", "--user", "--noninteractive", extension_id],
+ capture_output=True, text=True
+ )
+
+ if result.returncode != 0:
+ error_msg = f"Failed to uninstall Flatpak extension: {result.stderr}"
+ self.log.error(error_msg)
+ return self._error_response(BaseResponse, error_msg)
+
+ self.log.info(f"Successfully uninstalled lsfg-vk Flatpak extension {version}")
+ return self._success_response(BaseResponse, f"lsfg-vk {version} runtime extension uninstalled successfully")
+
+ except Exception as e:
+ error_msg = f"Error uninstalling Flatpak extension {version}: {str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(BaseResponse, error_msg)
+
+ def get_flatpak_apps(self) -> FlatpakAppInfo:
+ """Get list of installed Flatpak apps and their lsfg-vk override status"""
+ try:
+ if not self.check_flatpak_available():
+ error_msg = "Flatpak is not available on this system"
+ if self.flatpak_command is None:
+ error_msg += ". Command not found in PATH or common install locations."
+ return self._error_response(FlatpakAppInfo,
+ error_msg,
+ apps=[], total_apps=0)
+
+ # Get list of installed apps
+ result = self._run_flatpak_command(
+ ["list", "--app"],
+ capture_output=True, text=True, check=True
+ )
+
+ apps = []
+ for line in result.stdout.strip().split('\n'):
+ if not line.strip():
+ continue
+
+ # Parse flatpak list output (Name\tApp ID\tVersion\tBranch\tInstallation)
+ parts = line.split('\t')
+ if len(parts) >= 2:
+ app_name = parts[0].strip()
+ app_id = parts[1].strip()
+
+ # Check override status
+ override_status = self._check_app_override_status(app_id)
+
+ apps.append({
+ "app_id": app_id,
+ "app_name": app_name,
+ "has_filesystem_override": override_status["filesystem"],
+ "has_env_override": override_status["env"]
+ })
+
+ return self._success_response(FlatpakAppInfo,
+ f"Found {len(apps)} Flatpak applications",
+ apps=apps, total_apps=len(apps))
+
+ except subprocess.CalledProcessError as e:
+ error_msg = f"Error getting Flatpak apps: {e.stderr if e.stderr else str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(FlatpakAppInfo, error_msg, apps=[], total_apps=0)
+
+ def _check_app_override_status(self, app_id: str) -> Dict[str, bool]:
+ """Check if an app has lsfg-vk overrides set"""
+ try:
+ result = self._run_flatpak_command(
+ ["override", "--user", "--show", app_id],
+ capture_output=True, text=True
+ )
+
+ if result.returncode != 0:
+ return {"filesystem": False, "env": False}
+
+ output = result.stdout
+ home_path = os.path.expanduser("~")
+ config_path = f"{home_path}/.config/lsfg-vk"
+
+ # Check for filesystem override in the [Context] section
+ # Format: filesystems=/home/kurt/.config/lsfg-vk;
+ filesystem_override = f"filesystems={config_path}" in output
+
+ # Check for environment override in the [Environment] section
+ # Format: LSFG_CONFIG=/home/kurt/.config/lsfg-vk/conf.toml
+ env_override = f"LSFG_CONFIG={config_path}/conf.toml" in output
+
+ return {"filesystem": filesystem_override, "env": env_override}
+
+ except Exception as e:
+ self.log.error(f"Error checking override status for {app_id}: {e}")
+ return {"filesystem": False, "env": False}
+
+ def set_app_override(self, app_id: str) -> FlatpakOverrideResponse:
+ """Set lsfg-vk overrides for a Flatpak app"""
+ try:
+ if not self.check_flatpak_available():
+ return self._error_response(FlatpakOverrideResponse,
+ "Flatpak is not available on this system",
+ app_id=app_id, operation="set")
+
+ home_path = os.path.expanduser("~")
+ config_path = f"{home_path}/.config/lsfg-vk"
+
+ # Set filesystem override
+ result1 = self._run_flatpak_command(
+ ["override", "--user", f"--filesystem={config_path}:rw", app_id],
+ capture_output=True, text=True
+ )
+
+ # Set environment override
+ result2 = self._run_flatpak_command(
+ ["override", "--user", f"--env=LSFG_CONFIG={config_path}/conf.toml", app_id],
+ capture_output=True, text=True
+ )
+
+ if result1.returncode != 0 or result2.returncode != 0:
+ error_msg = f"Failed to set overrides: {result1.stderr} {result2.stderr}"
+ return self._error_response(FlatpakOverrideResponse, error_msg,
+ app_id=app_id, operation="set")
+
+ self.log.info(f"Successfully set lsfg-vk overrides for {app_id}")
+ return self._success_response(FlatpakOverrideResponse,
+ f"lsfg-vk overrides set for {app_id}",
+ app_id=app_id, operation="set")
+
+ except Exception as e:
+ error_msg = f"Error setting overrides for {app_id}: {str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(FlatpakOverrideResponse, error_msg,
+ app_id=app_id, operation="set")
+
+ def remove_app_override(self, app_id: str) -> FlatpakOverrideResponse:
+ """Remove lsfg-vk overrides for a Flatpak app"""
+ try:
+ if not self.check_flatpak_available():
+ return self._error_response(FlatpakOverrideResponse,
+ "Flatpak is not available on this system",
+ app_id=app_id, operation="remove")
+
+ home_path = os.path.expanduser("~")
+ config_path = f"{home_path}/.config/lsfg-vk"
+
+ # Remove filesystem override
+ result1 = self._run_flatpak_command(
+ ["override", "--user", f"--nofilesystem={config_path}", app_id],
+ capture_output=True, text=True
+ )
+
+ # Remove environment override
+ result2 = self._run_flatpak_command(
+ ["override", "--user", "--unset-env=LSFG_CONFIG", app_id],
+ capture_output=True, text=True
+ )
+
+ if result1.returncode != 0 or result2.returncode != 0:
+ error_msg = f"Failed to remove overrides: {result1.stderr} {result2.stderr}"
+ return self._error_response(FlatpakOverrideResponse, error_msg,
+ app_id=app_id, operation="remove")
+
+ self.log.info(f"Successfully removed lsfg-vk overrides for {app_id}")
+ return self._success_response(FlatpakOverrideResponse,
+ f"lsfg-vk overrides removed for {app_id}",
+ app_id=app_id, operation="remove")
+
+ except Exception as e:
+ error_msg = f"Error removing overrides for {app_id}: {str(e)}"
+ self.log.error(error_msg)
+ return self._error_response(FlatpakOverrideResponse, error_msg,
+ app_id=app_id, operation="remove") \ No newline at end of file
diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py
index f08aa18..ccc9984 100644
--- a/py_modules/lsfg_vk/plugin.py
+++ b/py_modules/lsfg_vk/plugin.py
@@ -18,6 +18,7 @@ from .installation import InstallationService
from .dll_detection import DllDetectionService
from .configuration import ConfigurationService
from .config_schema import ConfigurationManager
+from .flatpak_service import FlatpakService
class Plugin:
@@ -35,6 +36,7 @@ class Plugin:
self.installation_service = InstallationService()
self.dll_detection_service = DllDetectionService()
self.configuration_service = ConfigurationService()
+ self.flatpak_service = FlatpakService()
# Installation methods
async def install_lsfg_vk(self) -> Dict[str, Any]:
@@ -612,6 +614,67 @@ class Plugin:
"exists": False,
"error": str(e)
}
+
+ # Flatpak management methods
+ async def check_flatpak_extension_status(self) -> Dict[str, Any]:
+ """Check status of lsfg-vk Flatpak runtime extensions
+
+ Returns:
+ FlatpakExtensionStatus dict with installation status for both runtime versions
+ """
+ return self.flatpak_service.get_extension_status()
+
+ async def install_flatpak_extension(self, version: str) -> Dict[str, Any]:
+ """Install lsfg-vk Flatpak runtime extension
+
+ Args:
+ version: Runtime version to install ("23.08" or "24.08")
+
+ Returns:
+ BaseResponse dict with success status and message/error
+ """
+ return self.flatpak_service.install_extension(version)
+
+ async def uninstall_flatpak_extension(self, version: str) -> Dict[str, Any]:
+ """Uninstall lsfg-vk Flatpak runtime extension
+
+ Args:
+ version: Runtime version to uninstall ("23.08" or "24.08")
+
+ Returns:
+ BaseResponse dict with success status and message/error
+ """
+ return self.flatpak_service.uninstall_extension(version)
+
+ async def get_flatpak_apps(self) -> Dict[str, Any]:
+ """Get list of installed Flatpak apps and their lsfg-vk override status
+
+ Returns:
+ FlatpakAppInfo dict with apps list and override status
+ """
+ return self.flatpak_service.get_flatpak_apps()
+
+ async def set_flatpak_app_override(self, app_id: str) -> Dict[str, Any]:
+ """Set lsfg-vk overrides for a Flatpak app
+
+ Args:
+ app_id: Flatpak application ID
+
+ Returns:
+ FlatpakOverrideResponse dict with operation result
+ """
+ return self.flatpak_service.set_app_override(app_id)
+
+ async def remove_flatpak_app_override(self, app_id: str) -> Dict[str, Any]:
+ """Remove lsfg-vk overrides for a Flatpak app
+
+ Args:
+ app_id: Flatpak application ID
+
+ Returns:
+ FlatpakOverrideResponse dict with operation result
+ """
+ return self.flatpak_service.remove_app_override(app_id)
# Decky Loader lifecycle methods
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index 8db0c82..d08cd42 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -96,6 +96,38 @@ export interface FgmodCheckResult {
error?: string;
}
+// Flatpak management interfaces
+export interface FlatpakExtensionStatus {
+ success: boolean;
+ message: string;
+ error?: string;
+ installed_23_08: boolean;
+ installed_24_08: boolean;
+}
+
+export interface FlatpakApp {
+ app_id: string;
+ app_name: string;
+ has_filesystem_override: boolean;
+ has_env_override: boolean;
+}
+
+export interface FlatpakAppInfo {
+ success: boolean;
+ message: string;
+ error?: string;
+ apps: FlatpakApp[];
+ total_apps: number;
+}
+
+export interface FlatpakOperationResult {
+ success: boolean;
+ message: string;
+ error?: string;
+ app_id?: string;
+ operation?: string;
+}
+
// Profile management interfaces
export interface ProfilesResult {
success: boolean;
@@ -125,6 +157,14 @@ export const getConfigFileContent = callable<[], FileContentResult>("get_config_
export const getLaunchScriptContent = callable<[], FileContentResult>("get_launch_script_content");
export const checkFgmodDirectory = callable<[], FgmodCheckResult>("check_fgmod_directory");
+// Flatpak management API functions
+export const checkFlatpakExtensionStatus = callable<[], FlatpakExtensionStatus>("check_flatpak_extension_status");
+export const installFlatpakExtension = callable<[string], FlatpakOperationResult>("install_flatpak_extension");
+export const uninstallFlatpakExtension = callable<[string], FlatpakOperationResult>("uninstall_flatpak_extension");
+export const getFlatpakApps = callable<[], FlatpakAppInfo>("get_flatpak_apps");
+export const setFlatpakAppOverride = callable<[string], FlatpakOperationResult>("set_flatpak_app_override");
+export const removeFlatpakAppOverride = callable<[string], FlatpakOperationResult>("remove_flatpak_app_override");
+
// Updated config function using object-based configuration (single source of truth)
export const updateLsfgConfig = callable<
[ConfigurationData],
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index a00a595..b76ff59 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -15,6 +15,7 @@ import { FgmodClipboardButton } from "./FgmodClipboardButton";
// import { ClipboardDisplay } from "./ClipboardDisplay";
import { PluginUpdateChecker } from "./PluginUpdateChecker";
import { NerdStuffModal } from "./NerdStuffModal";
+import { FlatpaksModal } from "./index";
import { ConfigurationData } from "../config/configSchema";
export function Content() {
@@ -76,6 +77,10 @@ export function Content() {
showModal(<NerdStuffModal />);
};
+ const handleShowFlatpaks = () => {
+ showModal(<FlatpaksModal />);
+ };
+
return (
<PanelSection>
{/* Show installation components at top when not fully installed */}
@@ -165,6 +170,16 @@ export function Content() {
Nerd Stuff
</ButtonItem>
</PanelSectionRow>
+
+ {/* Flatpaks Button */}
+ <PanelSectionRow>
+ <ButtonItem
+ layout="below"
+ onClick={handleShowFlatpaks}
+ >
+ Flatpaks
+ </ButtonItem>
+ </PanelSectionRow>
</PanelSection>
);
}
diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx
index e69de29..a57c683 100644
--- a/src/components/FlatpaksModal.tsx
+++ b/src/components/FlatpaksModal.tsx
@@ -0,0 +1,291 @@
+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 {
+ 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 Extensions</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>
+ </Focusable>
+ </DialogBody>
+ </ModalRoot>
+ );
+};
+
+export default FlatpaksModal;
diff --git a/src/components/index.ts b/src/components/index.ts
index 802cd3c..50480be 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -10,4 +10,5 @@ export { FgmodClipboardButton } from "./FgmodClipboardButton";
// export { ClipboardDisplay } from "./ClipboardDisplay";
export { PluginUpdateChecker } from "./PluginUpdateChecker";
export { NerdStuffModal } from "./NerdStuffModal";
+export { default as FlatpaksModal } from "./FlatpaksModal";
export { ProfileManagement } from "./ProfileManagement";