summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-05 23:44:37 -0400
committerxXJSONDeruloXx <danielhimebauch@gmail.com>2026-09-05 23:44:37 -0400
commite8e469f99078858dc953663cba6f3428e80b1c5d (patch)
tree6d0e6730bf439ec2cb5b3564c636ce8342466df1
parent77656ad88655effe0411808baf2fb90c629a24f8 (diff)
downloaddecky-lsfg-vk-e8e469f99078858dc953663cba6f3428e80b1c5d.tar.gz
decky-lsfg-vk-e8e469f99078858dc953663cba6f3428e80b1c5d.zip
refactor: delegate runtime checks to lsfg-vk
-rw-r--r--README.md4
-rw-r--r--package.json17
-rw-r--r--py_modules/lsfg_vk/config_schema.py9
-rw-r--r--py_modules/lsfg_vk/configuration.py13
-rw-r--r--py_modules/lsfg_vk/constants.py16
-rw-r--r--py_modules/lsfg_vk/dll_detection.py223
-rw-r--r--py_modules/lsfg_vk/flatpak_service.py80
-rw-r--r--py_modules/lsfg_vk/installation.py70
-rw-r--r--py_modules/lsfg_vk/plugin.py93
-rw-r--r--py_modules/lsfg_vk/runtime_service.py120
-rw-r--r--py_modules/lsfg_vk/types.py17
-rw-r--r--src/api/lsfgApi.ts27
-rw-r--r--src/components/Content.tsx21
-rw-r--r--src/components/FlatpaksModal.tsx67
-rw-r--r--src/components/NerdStuffModal.tsx55
-rw-r--r--src/components/StatusDisplay.tsx21
-rw-r--r--src/hooks/useInstallationActions.ts12
-rw-r--r--src/hooks/useLsfgHooks.ts37
-rw-r--r--src/i18n/languages.json12
19 files changed, 383 insertions, 531 deletions
diff --git a/README.md b/README.md
index 66c11ff..3d9ee7e 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ For per-game feedback and community support, please join the [decky-lsfg-vk Disc
**Frame generation not working?**
- Ensure you've added `~/lsfg %command%` to your game's launch options
-- Check that the Lossless Scaling DLL was detected correctly in the plugin
+- Ensure the Lossless Scaling installation is visible to the upstream lsfg-vk search paths
- Try enabling Performance Mode if you're experiencing crashes
- Make sure your game is running in fullscreen mode for best results
@@ -73,7 +73,7 @@ The plugin:
- Installs the pinned lsfg-vk 2.0.0 x86_64 and x86 Vulkan layers from the official upstream build
- Configures the Vulkan layer in `~/.local/share/vulkan/implicit_layer.d/`
- Creates and migrates the v2 TOML configuration in `~/.config/lsfg-vk/conf.toml`, preserving a one-time v1 backup during upgrade
-- Automatically detects your Lossless Scaling DLL installation
+- Delegates Lossless Scaling DLL discovery to lsfg-vk's upstream runtime search
- Provides an easy-to-use interface to configure frame generation settings:
- **FPS Multiplier**: Use OFF/1x bypass or choose 2x, 3x, or 4x frame generation
- **Flow Scale**: Adjust motion estimation quality vs performance
diff --git a/package.json b/package.json
index 365ad02..6cd5ebc 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,22 @@
{
"name": "lsfg-vk-2.0.0.tar.xz",
"url": "https://builds.lsfg-vk.dev/lsfg-vk-2.0.0.tar.xz",
- "sha256hash": "08bdbdf373a111022df87dac7aa87e3b564bb841f961552e3ca85fea12b5aa74"
+ "sha256hash": "d8378b45d378150ea9aba803a0ba855d8ce91ad9b3366ee0eb2036b06b08380c"
+ },
+ {
+ "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak",
+ "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_23.08.flatpak",
+ "sha256hash": "7eff81f96b278fde6fe395c88461c55e611547bf5991d179b9145ec6f5a778b1"
+ },
+ {
+ "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak",
+ "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_24.08.flatpak",
+ "sha256hash": "cbd663b9021c355dddec61ec51fd4388c12705f16637cadd4b45aead0e5dc427"
+ },
+ {
+ "name": "org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak",
+ "url": "https://github.com/xXJSONDeruloXx/lsfg-vk/releases/download/v2.0.0-decky.2/org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak",
+ "sha256hash": "615e87c03b18368ed1cca97e036f1bf54fff95fc6126aac697bb368f33281cbf"
}
],
"pnpm": {
diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py
index 92a74a3..e39be54 100644
--- a/py_modules/lsfg_vk/config_schema.py
+++ b/py_modules/lsfg_vk/config_schema.py
@@ -60,15 +60,6 @@ class ConfigurationManager:
return cast(ConfigurationData, dict(get_defaults()))
@staticmethod
- def get_defaults_with_dll_detection(dll_detection_service=None) -> ConfigurationData:
- defaults = ConfigurationManager.get_defaults()
- if dll_detection_service is not None:
- result = dll_detection_service.check_lossless_scaling_dll()
- if result.get("detected") and result.get("path"):
- defaults["dll"] = result["path"]
- return defaults
-
- @staticmethod
def get_field_names() -> list[str]:
return list(CONFIG_SCHEMA)
diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py
index 9b4d536..12710cc 100644
--- a/py_modules/lsfg_vk/configuration.py
+++ b/py_modules/lsfg_vk/configuration.py
@@ -3,10 +3,15 @@ import shlex
from .base_service import BaseService
from .config_schema import ConfigurationManager, DEFAULT_PROFILE_NAME, ProfileData
from .config_schema_generated import ConfigurationData, get_script_generation_logic
+from .runtime_service import RuntimeService
from .types import ConfigurationResponse, ProfileResponse, ProfilesResponse
class ConfigurationService(BaseService):
+ def __init__(self, logger=None, runtime_service: RuntimeService = None):
+ super().__init__(logger)
+ self.runtime_service = runtime_service or RuntimeService(logger=self.log)
+
def get_config(self) -> ConfigurationResponse:
try:
profile_data = self._get_profile_data()
@@ -69,9 +74,7 @@ class ConfigurationService(BaseService):
def _get_profile_data(self) -> ProfileData:
if not self.config_file_path.exists():
- from .dll_detection import DllDetectionService
-
- default = ConfigurationManager.get_defaults_with_dll_detection(DllDetectionService(self.log))
+ default = ConfigurationManager.get_defaults()
return ProfileData(
current_profile=DEFAULT_PROFILE_NAME,
profiles={DEFAULT_PROFILE_NAME: dict(default)},
@@ -97,9 +100,11 @@ class ConfigurationService(BaseService):
return profile_data
def _save_profile_data(self, profile_data: ProfileData) -> None:
+ content = ConfigurationManager.generate_toml_content_multi_profile(profile_data)
+ self.runtime_service.validate_config_content(content)
self._write_file(
self.config_file_path,
- ConfigurationManager.generate_toml_content_multi_profile(profile_data),
+ content,
0o644,
)
diff --git a/py_modules/lsfg_vk/constants.py b/py_modules/lsfg_vk/constants.py
index 7bcd0dc..1f78a59 100644
--- a/py_modules/lsfg_vk/constants.py
+++ b/py_modules/lsfg_vk/constants.py
@@ -1,6 +1,5 @@
-from pathlib import Path
-
LOCAL_BIN = ".local/bin"
+LOCAL_SHARE = ".local/share"
LOCAL_LIB = ".local/lib"
VULKAN_LAYER_DIR = ".local/share/vulkan/implicit_layer.d"
CONFIG_DIR = ".config/lsfg-vk"
@@ -13,15 +12,14 @@ LIB_X86_FILENAME = "liblsfg-vk-layer.x86.so"
JSON_FILENAME = "VkLayer_LSFGVK_frame_generation.json"
JSON_X86_FILENAME = "VkLayer_LSFGVK_frame_generation.x86.json"
CLI_FILENAME = "lsfg-vk-cli"
+UI_FILENAME = "lsfg-vk-ui"
+UI_DESKTOP_FILENAME = "gay.pancake.lsfg-vk-ui.desktop"
+UI_ICON_FILENAME = "gay.pancake.lsfg-vk-ui.png"
+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"
+FLATPAK_25_08_FILENAME = "org.freedesktop.Platform.VulkanLayer.lsfg_vk_25.08.flatpak"
LEGACY_LIB_FILENAME = "liblsfg-vk.so"
LEGACY_JSON_FILENAME = "VkLayer_LS_frame_generation.json"
BIN_DIR = "bin"
-
-STEAM_COMMON_PATH = Path("steamapps/common/Lossless Scaling")
-LOSSLESS_DLL_NAME = "lsfg-vk.dll"
-
-ENV_LSFG_DLL_PATH = "LSFGVK_DLL_PATH"
-ENV_XDG_DATA_HOME = "XDG_DATA_HOME"
-ENV_HOME = "HOME"
diff --git a/py_modules/lsfg_vk/dll_detection.py b/py_modules/lsfg_vk/dll_detection.py
deleted file mode 100644
index f7ba444..0000000
--- a/py_modules/lsfg_vk/dll_detection.py
+++ /dev/null
@@ -1,223 +0,0 @@
-"""
-DLL detection service for Lossless Scaling.
-"""
-
-import os
-import re
-from pathlib import Path
-from typing import Dict, Any, List
-
-from .base_service import BaseService
-from .constants import (
- ENV_LSFG_DLL_PATH, ENV_XDG_DATA_HOME, ENV_HOME,
- STEAM_COMMON_PATH, LOSSLESS_DLL_NAME
-)
-from .types import DllDetectionResponse
-
-
-class DllDetectionService(BaseService):
- """Service for detecting Lossless Scaling DLL"""
-
- def check_lossless_scaling_dll(self) -> DllDetectionResponse:
- """Check if Lossless Scaling DLL is available at the expected paths
-
- Search order:
- 1. LSFG_DLL_PATH environment variable
- 2. XDG_DATA_HOME Steam directory
- 3. HOME/.local/share Steam directory
- 4. All Steam library folders (including SD cards)
-
- Returns:
- DllDetectionResponse with detection status and path information
- """
- try:
- dll_path = self._check_env_dll_path()
- if dll_path:
- return dll_path
-
- xdg_path = self._check_xdg_data_home()
- if xdg_path:
- return xdg_path
-
- home_path = self._check_home_local_share()
- if home_path:
- return home_path
-
- steam_libraries_path = self._check_steam_library_folders()
- if steam_libraries_path:
- return steam_libraries_path
-
- return {
- "detected": False,
- "path": None,
- "source": None,
- "message": "Lossless Scaling DLL not found in expected locations",
- "error": None
- }
-
- except Exception as e:
- error_msg = f"Error checking Lossless Scaling DLL: {str(e)}"
- self.log.error(error_msg)
- return {
- "detected": False,
- "path": None,
- "source": None,
- "message": None,
- "error": str(e)
- }
-
- def _check_env_dll_path(self) -> DllDetectionResponse | None:
- """Check LSFG_DLL_PATH environment variable
-
- Returns:
- DllDetectionResponse if found, None otherwise
- """
- dll_path = os.getenv(ENV_LSFG_DLL_PATH)
- if dll_path and dll_path.strip():
- dll_path_obj = Path(dll_path.strip())
- if dll_path_obj.exists():
- self.log.info(f"Found DLL via {ENV_LSFG_DLL_PATH}: {dll_path_obj}")
- return {
- "detected": True,
- "path": str(dll_path_obj),
- "source": f"{ENV_LSFG_DLL_PATH} environment variable",
- "message": None,
- "error": None
- }
- return None
-
- def _check_xdg_data_home(self) -> DllDetectionResponse | None:
- """Check XDG_DATA_HOME Steam directory
-
- Returns:
- DllDetectionResponse if found, None otherwise
- """
- data_dir = os.getenv(ENV_XDG_DATA_HOME)
- if data_dir and data_dir.strip():
- dll_path = Path(data_dir.strip()) / "Steam" / STEAM_COMMON_PATH / LOSSLESS_DLL_NAME
- if dll_path.exists():
- self.log.info(f"Found DLL via {ENV_XDG_DATA_HOME}: {dll_path}")
- return {
- "detected": True,
- "path": str(dll_path),
- "source": f"{ENV_XDG_DATA_HOME} Steam directory",
- "message": None,
- "error": None
- }
- return None
-
- def _check_home_local_share(self) -> DllDetectionResponse | None:
- """Check HOME/.local/share Steam directory
-
- Returns:
- DllDetectionResponse if found, None otherwise
- """
- home_dir = os.getenv(ENV_HOME)
- if home_dir and home_dir.strip():
- dll_path = Path(home_dir.strip()) / ".local" / "share" / "Steam" / STEAM_COMMON_PATH / LOSSLESS_DLL_NAME
- if dll_path.exists():
- self.log.info(f"Found DLL via {ENV_HOME}/.local/share: {dll_path}")
- return {
- "detected": True,
- "path": str(dll_path),
- "source": f"{ENV_HOME}/.local/share Steam directory",
- "message": None,
- "error": None
- }
- return None
-
- def _check_steam_library_folders(self) -> DllDetectionResponse | None:
- """Check all Steam library folders for Lossless Scaling DLL
-
- This method parses Steam's libraryfolders.vdf file to find all
- Steam library locations and checks each one for the DLL.
-
- Returns:
- DllDetectionResponse if found, None otherwise
- """
- steam_libraries = self._get_steam_library_paths()
-
- for library_path in steam_libraries:
- dll_path = Path(library_path) / STEAM_COMMON_PATH / LOSSLESS_DLL_NAME
- if dll_path.exists():
- self.log.info(f"Found DLL in Steam library: {dll_path}")
- return {
- "detected": True,
- "path": str(dll_path),
- "source": f"Steam library folder: {library_path}",
- "message": None,
- "error": None
- }
-
- return None
-
- def _get_steam_library_paths(self) -> List[str]:
- """Get all Steam library folder paths from libraryfolders.vdf
-
- Returns:
- List of Steam library folder paths
- """
- library_paths = []
-
- steam_paths = []
-
- data_dir = os.getenv(ENV_XDG_DATA_HOME)
- if data_dir and data_dir.strip():
- steam_paths.append(Path(data_dir.strip()) / "Steam")
-
- home_dir = os.getenv(ENV_HOME)
- if home_dir and home_dir.strip():
- steam_paths.append(Path(home_dir.strip()) / ".local" / "share" / "Steam")
-
- for steam_path in steam_paths:
- if steam_path.exists():
- library_paths.append(str(steam_path))
-
- vdf_path = steam_path / "steamapps" / "libraryfolders.vdf"
- if vdf_path.exists():
- try:
- additional_paths = self._parse_library_folders_vdf(vdf_path)
- library_paths.extend(additional_paths)
- except Exception as e:
- self.log.warning(f"Failed to parse {vdf_path}: {str(e)}")
-
- seen = set()
- unique_paths = []
- for path in library_paths:
- if path not in seen:
- seen.add(path)
- unique_paths.append(path)
-
- self.log.info(f"Found {len(unique_paths)} Steam library paths: {unique_paths}")
- return unique_paths
-
- def _parse_library_folders_vdf(self, vdf_path: Path) -> List[str]:
- """Parse Steam's libraryfolders.vdf file to extract library paths
-
- Args:
- vdf_path: Path to the libraryfolders.vdf file
-
- Returns:
- List of additional Steam library folder paths
- """
- library_paths = []
-
- try:
- with open(vdf_path, 'r', encoding='utf-8', errors='ignore') as f:
- content = f.read()
-
- path_pattern = r'"path"\s*"([^"]+)"'
- matches = re.findall(path_pattern, content, re.IGNORECASE)
-
- for path_match in matches:
- path = path_match.replace('\\\\', '/').replace('\\', '/')
- library_path = Path(path)
-
- if library_path.exists() and (library_path / "steamapps").exists():
- library_paths.append(str(library_path))
- self.log.info(f"Found additional Steam library: {library_path}")
-
- except Exception as e:
- self.log.error(f"Error parsing libraryfolders.vdf: {str(e)}")
-
- return library_paths
diff --git a/py_modules/lsfg_vk/flatpak_service.py b/py_modules/lsfg_vk/flatpak_service.py
index 627bbd8..302efc7 100644
--- a/py_modules/lsfg_vk/flatpak_service.py
+++ b/py_modules/lsfg_vk/flatpak_service.py
@@ -6,13 +6,18 @@ from typing import Any, Dict, List
from .base_service import BaseService
from .config_schema import ConfigurationManager
-from .dll_detection import DllDetectionService
+from .constants import (
+ BIN_DIR,
+ FLATPAK_23_08_FILENAME,
+ FLATPAK_24_08_FILENAME,
+ FLATPAK_25_08_FILENAME,
+)
from .types import BaseResponse
class FlatpakService(BaseService):
EXTENSION_ID = "org.freedesktop.Platform.VulkanLayer.lsfgvk"
- SUPPORTED_RUNTIMES = ("24.08", "25.08")
+ SUPPORTED_RUNTIMES = ("23.08", "24.08", "25.08")
def __init__(self, logger=None):
super().__init__(logger)
@@ -51,6 +56,18 @@ class FlatpakService(BaseService):
if version not in cls.SUPPORTED_RUNTIMES:
raise ValueError("Unsupported Flatpak runtime")
+ @classmethod
+ def _bundle_filename(cls, version: str) -> str:
+ return {
+ "23.08": FLATPAK_23_08_FILENAME,
+ "24.08": FLATPAK_24_08_FILENAME,
+ "25.08": FLATPAK_25_08_FILENAME,
+ }[version]
+
+ def _bundled_extension_path(self, version: str) -> Path:
+ self._validate_runtime(version)
+ return Path(__file__).resolve().parent.parent.parent / BIN_DIR / self._bundle_filename(version)
+
def get_extension_status(self) -> Dict[str, Any]:
try:
if not self.check_flatpak_available():
@@ -70,6 +87,7 @@ class FlatpakService(BaseService):
return self._success_response(
BaseResponse,
"Flatpak runtime status retrieved",
+ installed_23_08=(self.EXTENSION_ID, "x86_64", "23.08") in installed,
installed_24_08=(self.EXTENSION_ID, "x86_64", "24.08") in installed,
installed_25_08=(self.EXTENSION_ID, "x86_64", "25.08") in installed,
)
@@ -77,6 +95,7 @@ class FlatpakService(BaseService):
return self._error_response(
BaseResponse,
str(error),
+ installed_23_08=False,
installed_24_08=False,
installed_25_08=False,
)
@@ -86,14 +105,18 @@ class FlatpakService(BaseService):
self._validate_runtime(version)
if not self.check_flatpak_available():
raise FileNotFoundError("Flatpak is not available on this system")
+ bundle_path = self._bundled_extension_path(version)
+ if not bundle_path.is_file():
+ raise FileNotFoundError(
+ f"Bundled Flatpak extension not found at {bundle_path}; reinstall the plugin"
+ )
result = self._run_flatpak_command(
[
"install",
"--user",
"--noninteractive",
"--or-update",
- "flathub",
- f"{self.EXTENSION_ID}//{version}",
+ str(bundle_path),
],
capture_output=True,
text=True,
@@ -102,7 +125,7 @@ class FlatpakService(BaseService):
raise OSError(result.stderr.strip() or "Flatpak installation failed")
return self._success_response(
BaseResponse,
- f"lsfg-vk {version} runtime extension installed",
+ f"lsfg-vk {version} runtime extension installed from the bundled asset",
)
except Exception as error:
return self._error_response(BaseResponse, str(error))
@@ -126,6 +149,14 @@ class FlatpakService(BaseService):
except Exception as error:
return self._error_response(BaseResponse, str(error))
+ def _override_output(self, app_id: str) -> str:
+ result = self._run_flatpak_command(
+ ["override", "--user", "--show", app_id],
+ capture_output=True,
+ text=True,
+ )
+ return result.stdout if result.returncode == 0 else ""
+
def _dll_directory(self) -> Path:
if self.config_file_path.exists():
try:
@@ -138,24 +169,14 @@ class FlatpakService(BaseService):
except Exception:
pass
- result = DllDetectionService(self.log).check_lossless_scaling_dll()
- if result.get("detected") and result.get("path"):
- return Path(result["path"]).parent
- return self.user_home / ".local/share/Steam/steamapps/common"
-
- def _override_output(self, app_id: str) -> str:
- result = self._run_flatpak_command(
- ["override", "--user", "--show", app_id],
- capture_output=True,
- text=True,
- )
- return result.stdout if result.returncode == 0 else ""
+ return self.user_home / ".local/share/Steam/steamapps/common/Lossless Scaling"
def _override_paths(self) -> Dict[str, str]:
return {
"config_dir": str(self.config_dir),
"config_file": str(self.config_file_path),
"dll_dir": str(self._dll_directory()),
+ "legacy_home": str(self.user_home),
"legacy_dll": str(
self.user_home
/ ".local/share/Steam/steamapps/common/Lossless Scaling/Lossless.dll"
@@ -224,6 +245,9 @@ class FlatpakService(BaseService):
f"--filesystem={paths['config_dir']}:rw",
f"--filesystem={paths['dll_dir']}:ro",
f"--env=LSFGVK_CONFIG={paths['config_file']}",
+ # Remove permissions/env from the pre-v2 plugin when an
+ # existing app is explicitly migrated or reconfigured.
+ f"--nofilesystem={paths['legacy_home']}",
f"--nofilesystem={paths['legacy_dll']}",
f"--nofilesystem={paths['legacy_script']}",
"--unset-env=LSFG_CONFIG",
@@ -259,6 +283,7 @@ class FlatpakService(BaseService):
"--user",
f"--nofilesystem={paths['config_dir']}",
f"--nofilesystem={paths['dll_dir']}",
+ f"--nofilesystem={paths['legacy_home']}",
f"--nofilesystem={paths['legacy_dll']}",
f"--nofilesystem={paths['legacy_script']}",
"--unset-env=LSFGVK_CONFIG",
@@ -288,17 +313,6 @@ class FlatpakService(BaseService):
if not self.check_flatpak_available():
return
- status = self.get_extension_status()
- for version, key in (
- ("24.08", "installed_24_08"),
- ("25.08", "installed_25_08"),
- ):
- if not status.get(key):
- continue
- result = self.install_extension(version)
- if not result.get("success"):
- self.log.warning(result.get("error"))
-
apps_result = self._run_flatpak_command(
["list", "--user", "--app", "--columns=application"],
capture_output=True,
@@ -311,7 +325,15 @@ class FlatpakService(BaseService):
app_id = app_id.strip()
if not app_id:
continue
- if "LSFG_CONFIG=" in self._override_output(app_id):
+ output = self._override_output(app_id)
+ paths = self._override_paths()
+ legacy_markers = (
+ "LSFG_CONFIG=",
+ paths["legacy_home"],
+ paths["legacy_dll"],
+ paths["legacy_script"],
+ )
+ if any(marker in output for marker in legacy_markers):
result = self.set_app_override(app_id)
if not result.get("success"):
self.log.warning(result.get("error"))
diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py
index 0566d6b..e979ae5 100644
--- a/py_modules/lsfg_vk/installation.py
+++ b/py_modules/lsfg_vk/installation.py
@@ -18,13 +18,19 @@ from .constants import (
LEGACY_LIB_FILENAME,
LIB_FILENAME,
LIB_X86_FILENAME,
+ LOCAL_SHARE,
+ UI_DESKTOP_FILENAME,
+ UI_FILENAME,
+ UI_ICON_FILENAME,
)
+from .runtime_service import RuntimeService
from .types import InstallationCheckResponse, InstallationResponse, UninstallationResponse
class InstallationService(BaseService):
- def __init__(self, logger=None):
+ def __init__(self, logger=None, runtime_service: RuntimeService = None):
super().__init__(logger)
+ self.runtime_service = runtime_service or RuntimeService(logger=self.log)
self.lib_file = self.local_lib_dir / LIB_FILENAME
self.lib_x86_file = self.local_lib_dir / LIB_X86_FILENAME
self.json_file = self.local_share_dir / JSON_FILENAME
@@ -43,9 +49,11 @@ class InstallationService(BaseService):
self._ensure_directories()
profile_data = self._prepare_config()
self._install_archive(archive_path)
+ config_content = ConfigurationManager.generate_toml_content_multi_profile(profile_data)
+ self.runtime_service.validate_config_content(config_content)
self._write_file(
self.config_file_path,
- ConfigurationManager.generate_toml_content_multi_profile(profile_data),
+ config_content,
0o644,
)
self._create_lsfg_launch_script(profile_data)
@@ -58,16 +66,25 @@ class InstallationService(BaseService):
def _payload_destinations(self) -> Dict[str, tuple[Path, int]]:
return {
f"bin/{CLI_FILENAME}": (self.cli_file, 0o755),
+ f"bin/{UI_FILENAME}": (self.local_bin_dir / UI_FILENAME, 0o755),
f"lib/{LIB_FILENAME}": (self.lib_file, 0o644),
f"lib/{LIB_X86_FILENAME}": (self.lib_x86_file, 0o644),
f"share/vulkan/implicit_layer.d/{JSON_FILENAME}": (self.json_file, 0o644),
f"share/vulkan/implicit_layer.d/{JSON_X86_FILENAME}": (self.json_x86_file, 0o644),
+ f"share/applications/{UI_DESKTOP_FILENAME}": (
+ self.user_home / LOCAL_SHARE / "applications" / UI_DESKTOP_FILENAME,
+ 0o644,
+ ),
+ f"share/icons/hicolor/256x256/apps/{UI_ICON_FILENAME}": (
+ self.user_home / LOCAL_SHARE / "icons" / "hicolor" / "256x256" / "apps" / UI_ICON_FILENAME,
+ 0o644,
+ ),
}
def _install_archive(self, archive_path: Path) -> None:
destinations = self._payload_destinations()
found = set()
- with tarfile.open(archive_path, "r:xz") as archive:
+ with tarfile.open(archive_path, "r:*") as archive:
members = {
member.name.removeprefix("./"): member
for member in archive.getmembers()
@@ -126,13 +143,6 @@ class InstallationService(BaseService):
},
)
- from .dll_detection import DllDetectionService
-
- if not profile_data["global_config"].get("dll"):
- dll_result = DllDetectionService(self.log).check_lossless_scaling_dll()
- if dll_result.get("detected") and dll_result.get("path"):
- profile_data["global_config"]["dll"] = dll_result["path"]
-
defaults = dict(ConfigurationManager.get_defaults())
for profile_name, raw_profile in list(profile_data["profiles"].items()):
validated = ConfigurationManager.validate_config({**defaults, **raw_profile})
@@ -189,35 +199,38 @@ class InstallationService(BaseService):
)
except OSError:
legacy_config = False
- return legacy_layer or legacy_config
+ if legacy_layer or legacy_config:
+ return True
+ try:
+ return not self.runtime_service.is_healthy()
+ except Exception:
+ return True
def get_launch_script_path(self) -> str:
return str(self.lsfg_launch_script_path)
def check_installation(self) -> InstallationCheckResponse:
try:
- lib_exists = self.lib_file.exists() and self.lib_x86_file.exists()
- json_exists = self.json_file.exists() and self.json_x86_file.exists()
script_exists = self.lsfg_launch_script_path.exists()
+ installation_error = None
+ try:
+ installed = script_exists and self.runtime_service.is_healthy()
+ except Exception as error:
+ installed = False
+ installation_error = str(error)
+
+ lossless_scaling = self.runtime_service.check_lossless_scaling()
return {
- "installed": lib_exists and json_exists,
- "lib_exists": lib_exists,
- "json_exists": json_exists,
- "script_exists": script_exists,
- "lib_path": str(self.lib_file),
- "json_path": str(self.json_file),
- "script_path": str(self.lsfg_launch_script_path),
- "error": None,
+ "installed": installed,
+ "lossless_scaling_installed": bool(lossless_scaling["installed"]),
+ "lossless_scaling_status": str(lossless_scaling["status"]),
+ "error": installation_error,
}
except Exception as error:
return {
"installed": False,
- "lib_exists": False,
- "json_exists": False,
- "script_exists": False,
- "lib_path": str(self.lib_file),
- "json_path": str(self.json_file),
- "script_path": str(self.lsfg_launch_script_path),
+ "lossless_scaling_installed": False,
+ "lossless_scaling_status": str(error),
"error": str(error),
}
@@ -230,6 +243,9 @@ class InstallationService(BaseService):
self.json_file,
self.json_x86_file,
self.cli_file,
+ self.local_bin_dir / UI_FILENAME,
+ self.user_home / LOCAL_SHARE / "applications" / UI_DESKTOP_FILENAME,
+ self.user_home / LOCAL_SHARE / "icons" / "hicolor" / "256x256" / "apps" / UI_ICON_FILENAME,
self.legacy_lib_file,
self.legacy_json_file,
self.lsfg_launch_script_path,
diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py
index a9eb6a6..6a8dfc5 100644
--- a/py_modules/lsfg_vk/plugin.py
+++ b/py_modules/lsfg_vk/plugin.py
@@ -6,17 +6,16 @@ Vulkan layer for frame generation on Steam Deck.
"""
import os
-import hashlib
from typing import Dict, Any
from pathlib import Path
import decky
from .installation import InstallationService
-from .dll_detection import DllDetectionService
from .configuration import ConfigurationService
from .config_schema import ConfigurationManager
from .flatpak_service import FlatpakService
+from .runtime_service import RuntimeService
class Plugin:
@@ -24,15 +23,15 @@ class Plugin:
Main plugin class for lsfg-vk management.
This class provides a unified interface for installation, configuration,
- and DLL detection services. It implements the Decky Loader plugin lifecycle
+ and Flatpak management services. It implements the Decky Loader plugin lifecycle
methods (_main, _unload, _uninstall, _migration).
"""
def __init__(self):
"""Initialize the plugin with all necessary services"""
- self.installation_service = InstallationService()
- self.dll_detection_service = DllDetectionService()
- self.configuration_service = ConfigurationService()
+ self.runtime_service = RuntimeService()
+ self.installation_service = InstallationService(runtime_service=self.runtime_service)
+ self.configuration_service = ConfigurationService(runtime_service=self.runtime_service)
self.flatpak_service = FlatpakService()
async def install_lsfg_vk(self) -> Dict[str, Any]:
@@ -59,72 +58,6 @@ class Plugin:
"""
return self.installation_service.uninstall()
- async def check_lossless_scaling_dll(self) -> Dict[str, Any]:
- """Check if Lossless Scaling DLL is available at the expected paths
-
- Returns:
- DllDetectionResponse dict with detection status and path info
- """
- return self.dll_detection_service.check_lossless_scaling_dll()
-
- async def get_dll_stats(self) -> Dict[str, Any]:
- """Get detailed statistics about the detected DLL
-
- Returns:
- Dict containing DLL path, SHA256 hash, and other stats
- """
- try:
- dll_result = self.dll_detection_service.check_lossless_scaling_dll()
-
- if not dll_result.get("detected") or not dll_result.get("path"):
- return {
- "success": False,
- "error": "DLL not detected",
- "dll_path": None,
- "dll_sha256": None
- }
-
- dll_path = dll_result["path"]
- if dll_path is None:
- return {
- "success": False,
- "error": "DLL path is None",
- "dll_path": None,
- "dll_sha256": None
- }
-
- dll_path_obj = Path(dll_path)
-
- sha256_hash = hashlib.sha256()
- try:
- with open(dll_path_obj, "rb") as f:
- for chunk in iter(lambda: f.read(4096), b""):
- sha256_hash.update(chunk)
- dll_sha256 = sha256_hash.hexdigest()
- except Exception as e:
- return {
- "success": False,
- "error": f"Failed to calculate SHA256: {str(e)}",
- "dll_path": dll_path,
- "dll_sha256": None
- }
-
- return {
- "success": True,
- "dll_path": dll_path,
- "dll_sha256": dll_sha256,
- "dll_source": dll_result.get("source"),
- "error": None
- }
-
- except Exception as e:
- return {
- "success": False,
- "error": f"Failed to get DLL stats: {str(e)}",
- "dll_path": None,
- "dll_sha256": None
- }
-
async def get_lsfg_config(self) -> Dict[str, Any]:
"""Read current lsfg script configuration
@@ -353,7 +286,7 @@ class Plugin:
"""Check status of lsfg-vk Flatpak runtime extensions
Returns:
- FlatpakExtensionStatus dict with installation status for both runtime versions
+ FlatpakExtensionStatus dict with installation status for all supported runtime versions
"""
return self.flatpak_service.get_extension_status()
@@ -361,7 +294,7 @@ class Plugin:
"""Install lsfg-vk Flatpak runtime extension
Args:
- version: Runtime version to install ("24.08" or "25.08")
+ version: Runtime version to install ("23.08", "24.08", or "25.08")
Returns:
BaseResponse dict with success status and message/error
@@ -372,7 +305,7 @@ class Plugin:
"""Uninstall lsfg-vk Flatpak runtime extension
Args:
- version: Runtime version to uninstall ("24.08" or "25.08")
+ version: Runtime version to uninstall ("23.08", "24.08", or "25.08")
Returns:
BaseResponse dict with success status and message/error
@@ -442,6 +375,7 @@ class Plugin:
try:
extension_status = self.flatpak_service.get_extension_status()
for version, key in (
+ ("23.08", "installed_23_08"),
("24.08", "installed_24_08"),
("25.08", "installed_25_08"),
):
@@ -479,10 +413,9 @@ class Plugin:
if not result.get("success"):
decky.logger.warning(f"Native v2 migration failed: {result.get('error')}")
- if self.installation_service.check_installation().get("installed"):
- try:
- self.flatpak_service.migrate_v2()
- except Exception as error:
- decky.logger.warning(f"Flatpak v2 migration skipped: {error}")
+ try:
+ self.flatpak_service.migrate_v2()
+ except Exception as error:
+ decky.logger.warning(f"Flatpak v2 migration skipped: {error}")
decky.logger.info("decky-lsfg-vk plugin migrations completed")
diff --git a/py_modules/lsfg_vk/runtime_service.py b/py_modules/lsfg_vk/runtime_service.py
new file mode 100644
index 0000000..a61220e
--- /dev/null
+++ b/py_modules/lsfg_vk/runtime_service.py
@@ -0,0 +1,120 @@
+import os
+import subprocess
+import tempfile
+from pathlib import Path
+from typing import Any, Sequence
+
+from .base_service import BaseService
+from .constants import CLI_FILENAME
+
+
+class RuntimeService(BaseService):
+ COMMAND_TIMEOUT_SECONDS = 10
+ MAX_OUTPUT_LENGTH = 12_000
+ DLL_MISSING_MARKER = "! The DLL file does not exist:"
+ DLL_NONE_MARKER = "DLL override: (none)"
+
+ def __init__(self, logger=None):
+ super().__init__(logger)
+ self.cli_path = self.local_bin_dir / CLI_FILENAME
+
+ def _environment(self) -> dict[str, str]:
+ environment = os.environ.copy()
+ environment.update(
+ HOME=str(self.user_home),
+ XDG_CONFIG_HOME=str(self.user_home / ".config"),
+ )
+ for name in ("LSFGVK_CONFIG", "LSFGVK_PROFILE", "LSFGVK_ENV"):
+ environment.pop(name, None)
+ return environment
+
+ @classmethod
+ def _output(cls, stdout: Any, stderr: Any) -> str:
+ values = []
+ for value in (stdout, stderr):
+ if isinstance(value, bytes):
+ value = value.decode("utf-8", errors="replace")
+ if value:
+ values.append(str(value).strip())
+ output = "\n".join(value for value in values if value)
+ return output if len(output) <= cls.MAX_OUTPUT_LENGTH else output[: cls.MAX_OUTPUT_LENGTH]
+
+ def _run(self, arguments: Sequence[str]) -> tuple[int, str]:
+ if not self.cli_path.is_file() or not os.access(self.cli_path, os.X_OK):
+ raise FileNotFoundError(f"{CLI_FILENAME} is not installed at {self.cli_path}")
+ result = subprocess.run(
+ [str(self.cli_path), *arguments],
+ cwd=str(self.user_home),
+ env=self._environment(),
+ capture_output=True,
+ text=True,
+ timeout=self.COMMAND_TIMEOUT_SECONDS,
+ check=False,
+ )
+ return result.returncode, self._output(result.stdout, result.stderr)
+
+ def validate_config_content(self, content: str) -> None:
+ self.config_dir.mkdir(parents=True, exist_ok=True)
+ temporary_path: Path | None = None
+ try:
+ with tempfile.NamedTemporaryFile(
+ mode="w",
+ encoding="utf-8",
+ dir=self.config_dir,
+ prefix=".conf.toml.",
+ suffix=".tmp",
+ delete=False,
+ ) as temporary_file:
+ temporary_path = Path(temporary_file.name)
+ temporary_file.write(content)
+ temporary_file.flush()
+ os.fsync(temporary_file.fileno())
+
+ returncode, output = self._run(("validate", "--config", str(temporary_path)))
+ if returncode != 0:
+ raise ValueError(output or "lsfg-vk rejected the generated configuration")
+ finally:
+ if temporary_path is not None:
+ temporary_path.unlink(missing_ok=True)
+
+ def is_healthy(self) -> bool:
+ returncode, output = self._run(("healthcheck",))
+ return returncode == 0 and "Healthcheck found issues" not in output
+
+ def check_lossless_scaling(self) -> dict[str, Any]:
+ """Report Lossless Scaling state from lsfg-vk's own validator."""
+ if not self.config_file_path.is_file():
+ return {
+ "installed": False,
+ "status": "lsfg-vk configuration is not installed",
+ }
+
+ try:
+ returncode, output = self._run(
+ ("validate", "--config", str(self.config_file_path), "--print")
+ )
+ except Exception as error:
+ return {"installed": False, "status": str(error)}
+
+ if returncode != 0:
+ return {
+ "installed": False,
+ "status": output or "lsfg-vk could not validate its configuration",
+ }
+
+ if self.DLL_MISSING_MARKER in output:
+ return {
+ "installed": False,
+ "status": "Lossless Scaling's lsfg-vk.dll was not found",
+ }
+
+ if self.DLL_NONE_MARKER in output:
+ return {
+ "installed": False,
+ "status": "Lossless Scaling's lsfg-vk.dll is not configured",
+ }
+
+ return {
+ "installed": True,
+ "status": "Lossless Scaling detected by lsfg-vk",
+ }
diff --git a/py_modules/lsfg_vk/types.py b/py_modules/lsfg_vk/types.py
index 7b7ca2b..0f0428b 100644
--- a/py_modules/lsfg_vk/types.py
+++ b/py_modules/lsfg_vk/types.py
@@ -37,21 +37,8 @@ class UninstallationResponse(BaseResponse):
class InstallationCheckResponse(TypedDict):
"""Response for installation check"""
installed: bool
- lib_exists: bool
- json_exists: bool
- script_exists: bool
- lib_path: str
- json_path: str
- script_path: str
- error: Optional[str]
-
-
-class DllDetectionResponse(TypedDict):
- """Response for DLL detection"""
- detected: bool
- path: Optional[str]
- source: Optional[str]
- message: Optional[str]
+ lossless_scaling_installed: bool
+ lossless_scaling_status: str
error: Optional[str]
diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts
index 2c9f4a9..68cf6bf 100644
--- a/src/api/lsfgApi.ts
+++ b/src/api/lsfgApi.ts
@@ -11,28 +11,8 @@ export interface InstallationResult {
export interface InstallationStatus {
installed: boolean;
- lib_exists: boolean;
- json_exists: boolean;
- script_exists: boolean;
- lib_path: string;
- json_path: string;
- script_path: string;
- error?: string;
-}
-
-export interface DllDetectionResult {
- detected: boolean;
- path?: string;
- source?: string;
- message?: string;
- error?: string;
-}
-
-export interface DllStatsResult {
- success: boolean;
- dll_path?: string;
- dll_sha256?: string;
- dll_source?: string;
+ lossless_scaling_installed: boolean;
+ lossless_scaling_status: string;
error?: string;
}
@@ -84,6 +64,7 @@ export interface FlatpakExtensionStatus {
success: boolean;
message: string;
error?: string;
+ installed_23_08: boolean;
installed_24_08: boolean;
installed_25_08: boolean;
}
@@ -131,8 +112,6 @@ export interface ProfileResult {
export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk");
export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk");
export const checkLsfgVkInstalled = callable<[], InstallationStatus>("check_lsfg_vk_installed");
-export const checkLosslessScalingDll = callable<[], DllDetectionResult>("check_lossless_scaling_dll");
-export const getDllStats = callable<[], DllStatsResult>("get_dll_stats");
export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config");
export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_schema");
export const getLaunchOption = callable<[], LaunchOptionResult>("get_launch_option");
diff --git a/src/components/Content.tsx b/src/components/Content.tsx
index 5988e79..aab2fb8 100644
--- a/src/components/Content.tsx
+++ b/src/components/Content.tsx
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import { PanelSection, showModal, ButtonItem, PanelSectionRow } from "@decky/ui";
-import { useInstallationStatus, useDllDetection, useLsfgConfig } from "../hooks/useLsfgHooks";
+import { useInstallationStatus, useLsfgConfig } from "../hooks/useLsfgHooks";
import { useProfileManagement } from "../hooks/useProfileManagement";
import { useInstallationActions } from "../hooks/useInstallationActions";
import { StatusDisplay } from "./StatusDisplay";
@@ -21,11 +21,12 @@ export function Content() {
isInstalled,
installationStatus,
setIsInstalled,
- setInstallationStatus
+ setInstallationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus,
+ checkInstallation
} = useInstallationStatus();
- const { dllDetected, dllDetectionStatus } = useDllDetection();
-
const {
config,
loadLsfgConfig,
@@ -59,11 +60,11 @@ export function Content() {
};
const onInstall = () => {
- handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig);
+ handleInstall(setIsInstalled, setInstallationStatus, loadLsfgConfig, checkInstallation);
};
const onUninstall = () => {
- handleUninstall(setIsInstalled, setInstallationStatus);
+ handleUninstall(setIsInstalled, setInstallationStatus, checkInstallation);
};
const handleShowNerdStuff = () => {
@@ -87,10 +88,10 @@ export function Content() {
/>
<StatusDisplay
- dllDetected={dllDetected}
- dllDetectionStatus={dllDetectionStatus}
isInstalled={isInstalled}
installationStatus={installationStatus}
+ losslessScalingInstalled={losslessScalingInstalled}
+ losslessScalingStatus={losslessScalingStatus}
/>
</>
)}
@@ -167,10 +168,10 @@ export function Content() {
{isInstalled && (
<>
<StatusDisplay
- dllDetected={dllDetected}
- dllDetectionStatus={dllDetectionStatus}
isInstalled={isInstalled}
installationStatus={installationStatus}
+ losslessScalingInstalled={losslessScalingInstalled}
+ losslessScalingStatus={losslessScalingStatus}
/>
<InstallationButton
diff --git a/src/components/FlatpaksModal.tsx b/src/components/FlatpaksModal.tsx
index 479b919..8245160 100644
--- a/src/components/FlatpaksModal.tsx
+++ b/src/components/FlatpaksModal.tsx
@@ -28,6 +28,7 @@ import {
FlatpakAppInfo
} from '../api/lsfgApi';
import t from '../i18n/i18n';
+import { showErrorToast } from '../utils/toastUtils';
interface FlatpaksModalProps {
closeModal?: () => void;
@@ -38,6 +39,7 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
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);
@@ -63,6 +65,7 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
const handleExtensionOperation = async (operation: 'install' | 'uninstall', version: string) => {
const operationId = `${operation}-${version}`;
setOperationInProgress(operationId);
+ setOperationError(null);
try {
const result = operation === 'install'
@@ -73,9 +76,16 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
// 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);
}
@@ -85,6 +95,7 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
const hasOverrides = app.has_filesystem_override && app.has_env_override;
const operationId = `app-${app.app_id}`;
setOperationInProgress(operationId);
+ setOperationError(null);
try {
const result = hasOverrides
@@ -95,9 +106,16 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
// 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);
}
@@ -170,8 +188,57 @@ export const FlatpaksModal: FC<FlatpaksModalProps> = ({ closeModal }) => {
<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
diff --git a/src/components/NerdStuffModal.tsx b/src/components/NerdStuffModal.tsx
index 8a79b67..f075ccb 100644
--- a/src/components/NerdStuffModal.tsx
+++ b/src/components/NerdStuffModal.tsx
@@ -7,7 +7,11 @@ import {
PanelSectionRow,
ButtonItem
} from "@decky/ui";
-import { getDllStats, DllStatsResult, getConfigFileContent, getLaunchScriptContent, FileContentResult } from "../api/lsfgApi";
+import {
+ getConfigFileContent,
+ getLaunchScriptContent,
+ FileContentResult,
+} from "../api/lsfgApi";
import t from '../i18n/i18n';
interface NerdStuffModalProps {
@@ -15,7 +19,6 @@ interface NerdStuffModalProps {
}
export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
- const [dllStats, setDllStats] = useState<DllStatsResult | null>(null);
const [configContent, setConfigContent] = useState<FileContentResult | null>(null);
const [scriptContent, setScriptContent] = useState<FileContentResult | null>(null);
const [loading, setLoading] = useState(true);
@@ -28,13 +31,11 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
setError(null);
// Load all data in parallel
- const [dllResult, configResult, scriptResult] = await Promise.all([
- getDllStats(),
+ const [configResult, scriptResult] = await Promise.all([
getConfigFileContent(),
- getLaunchScriptContent()
+ getLaunchScriptContent(),
]);
- setDllStats(dllResult);
setConfigContent(configResult);
setScriptContent(scriptResult);
} catch (err) {
@@ -47,11 +48,6 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
loadData();
}, []);
- const formatSHA256 = (hash: string) => {
- // Format SHA256 hash for better readability (add spaces every 8 characters)
- return hash.replace(/(.{8})/g, '$1 ').trim();
- };
-
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
@@ -73,41 +69,6 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
{!loading && !error && (
<>
- {/* DLL Stats Section */}
- {dllStats && (
- <>
- {!dllStats.success ? (
- <div>{dllStats.error || "Failed to get DLL stats"}</div>
- ) : (
- <div>
- <Field label={t('NERD_DLL_PATH', 'DLL Path')}>
- <Focusable
- onClick={() => dllStats.dll_path && copyToClipboard(dllStats.dll_path)}
- onActivate={() => dllStats.dll_path && copyToClipboard(dllStats.dll_path)}
- >
- {dllStats.dll_path || t('NERD_NOT_AVAILABLE', 'Not available')}
- </Focusable>
- </Field>
-
- <Field label={t('NERD_DLL_HASH', 'DLL SHA256 Hash')}>
- <Focusable
- onClick={() => dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)}
- onActivate={() => dllStats.dll_sha256 && copyToClipboard(dllStats.dll_sha256)}
- >
- {dllStats.dll_sha256 ? formatSHA256(dllStats.dll_sha256) : t('NERD_NOT_AVAILABLE', 'Not available')}
- </Focusable>
- </Field>
-
- {dllStats.dll_source && (
- <Field label={t('NERD_DETECTION_SOURCE', 'Detection Source')}>
- <div>{dllStats.dll_source}</div>
- </Field>
- )}
- </div>
- )}
- </>
- )}
-
{/* Launch Script Section */}
{scriptContent && (
<Field label={t('NERD_LAUNCH_SCRIPT', 'Launch Script')}>
@@ -168,7 +129,7 @@ export function NerdStuffModal({ closeModal }: NerdStuffModalProps) {
)}
</Field>
)}
-
+
{/* Close Button */}
<DialogControlsSection>
<PanelSectionRow>
diff --git a/src/components/StatusDisplay.tsx b/src/components/StatusDisplay.tsx
index 3a48a15..31584eb 100644
--- a/src/components/StatusDisplay.tsx
+++ b/src/components/StatusDisplay.tsx
@@ -1,24 +1,24 @@
import { PanelSectionRow } from "@decky/ui";
interface StatusDisplayProps {
- dllDetected: boolean;
- dllDetectionStatus: string;
isInstalled: boolean;
installationStatus: string;
+ losslessScalingInstalled: boolean;
+ losslessScalingStatus: string;
}
export function StatusDisplay({
- dllDetected,
- dllDetectionStatus,
isInstalled,
- installationStatus
+ installationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus
}: StatusDisplayProps) {
return (
<PanelSectionRow>
<div style={{ marginBottom: "8px", fontSize: "14px" }}>
<div
style={{
- color: dllDetected ? "#4CAF50" : "#F44336",
+ color: losslessScalingInstalled ? "#4CAF50" : "#F44336",
fontWeight: "600",
marginBottom: "6px",
display: "flex",
@@ -27,10 +27,15 @@ export function StatusDisplay({
}}
>
<span style={{ fontSize: "16px" }}>
- {dllDetected ? "✅" : "❌"}
+ {losslessScalingInstalled ? "✅" : "❌"}
</span>
- {dllDetectionStatus}
+ {losslessScalingInstalled ? "Lossless Scaling Installed" : "Lossless Scaling Not Installed"}
</div>
+ {!losslessScalingInstalled && losslessScalingStatus && (
+ <div style={{ color: "#B8B8B8", fontSize: "12px", margin: "0 0 6px 22px" }}>
+ {losslessScalingStatus}
+ </div>
+ )}
<div
style={{
color: isInstalled ? "#4CAF50" : "#FF9800",
diff --git a/src/hooks/useInstallationActions.ts b/src/hooks/useInstallationActions.ts
index f184145..41189bd 100644
--- a/src/hooks/useInstallationActions.ts
+++ b/src/hooks/useInstallationActions.ts
@@ -14,7 +14,8 @@ export function useInstallationActions() {
const handleInstall = async (
setIsInstalled: (value: boolean) => void,
setInstallationStatus: (value: string) => void,
- reloadConfig?: () => Promise<void>
+ reloadConfig?: () => Promise<void>,
+ reloadStatus?: () => Promise<boolean>
) => {
setIsInstalling(true);
setInstallationStatus("Installing lsfg-vk...");
@@ -30,6 +31,9 @@ export function useInstallationActions() {
if (reloadConfig) {
await reloadConfig();
}
+ if (reloadStatus) {
+ await reloadStatus();
+ }
} else {
setInstallationStatus(`Installation failed: ${result.error}`);
showInstallErrorToast(result.error);
@@ -44,7 +48,8 @@ export function useInstallationActions() {
const handleUninstall = async (
setIsInstalled: (value: boolean) => void,
- setInstallationStatus: (value: string) => void
+ setInstallationStatus: (value: string) => void,
+ reloadStatus?: () => Promise<boolean>
) => {
setIsUninstalling(true);
setInstallationStatus("Uninstalling lsfg-vk...");
@@ -54,6 +59,9 @@ export function useInstallationActions() {
if (result.success) {
setIsInstalled(false);
setInstallationStatus("lsfg-vk uninstalled successfully!");
+ if (reloadStatus) {
+ await reloadStatus();
+ }
showUninstallSuccessToast();
} else {
setInstallationStatus(`Uninstallation failed: ${result.error}`);
diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts
index d9bbe3e..597110e 100644
--- a/src/hooks/useLsfgHooks.ts
+++ b/src/hooks/useLsfgHooks.ts
@@ -1,7 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import {
checkLsfgVkInstalled,
- checkLosslessScalingDll,
getLsfgConfig,
updateLsfgConfigFromObject,
type ConfigUpdateResult
@@ -12,11 +11,15 @@ import { showErrorToast, ToastMessages } from "../utils/toastUtils";
export function useInstallationStatus() {
const [isInstalled, setIsInstalled] = useState<boolean>(false);
const [installationStatus, setInstallationStatus] = useState<string>("");
+ const [losslessScalingInstalled, setLosslessScalingInstalled] = useState<boolean>(false);
+ const [losslessScalingStatus, setLosslessScalingStatus] = useState<string>("");
const checkInstallation = async () => {
try {
const status = await checkLsfgVkInstalled();
setIsInstalled(status.installed);
+ setLosslessScalingInstalled(status.lossless_scaling_installed);
+ setLosslessScalingStatus(status.lossless_scaling_status || "Lossless Scaling Not Installed");
if (status.installed) {
setInstallationStatus("lsfg-vk Installed");
} else {
@@ -24,6 +27,8 @@ export function useInstallationStatus() {
}
return status.installed;
} catch (error) {
+ setLosslessScalingInstalled(false);
+ setLosslessScalingStatus("Lossless Scaling Not Installed");
setInstallationStatus("lsfg-vk Not Installed");
return false;
}
@@ -38,38 +43,12 @@ export function useInstallationStatus() {
installationStatus,
setIsInstalled,
setInstallationStatus,
+ losslessScalingInstalled,
+ losslessScalingStatus,
checkInstallation
};
}
-export function useDllDetection() {
- const [dllDetected, setDllDetected] = useState<boolean>(false);
- const [dllDetectionStatus, setDllDetectionStatus] = useState<string>("");
-
- const checkDllDetection = async () => {
- try {
- const result = await checkLosslessScalingDll();
- setDllDetected(result.detected);
- if (result.detected) {
- setDllDetectionStatus("Lossless Scaling Installed");
- } else {
- setDllDetectionStatus("Lossless Scaling Not Installed");
- }
- } catch (error) {
- setDllDetectionStatus("Lossless Scaling Not Installed");
- }
- };
-
- useEffect(() => {
- checkDllDetection();
- }, []);
-
- return {
- dllDetected,
- dllDetectionStatus
- };
-}
-
export function useLsfgConfig() {
const [config, setConfig] = useState<ConfigurationData>(() => getDefaults());
diff --git a/src/i18n/languages.json b/src/i18n/languages.json
index c1a49b8..46b4cd9 100644
--- a/src/i18n/languages.json
+++ b/src/i18n/languages.json
@@ -67,10 +67,6 @@
"FLATPAK_STEP_FINAL": "最終的な結果はこのようになります:",
"FLATPAK_CLOSE": "閉じる",
"NERD_LOADING": "情報を読み込み中...",
- "NERD_DLL_PATH": "DLLパス",
- "NERD_NOT_AVAILABLE": "利用不可",
- "NERD_DLL_HASH": "DLL SHA256ハッシュ",
- "NERD_DETECTION_SOURCE": "検出ソース",
"NERD_LAUNCH_SCRIPT": "起動スクリプト",
"NERD_SCRIPT_NOT_FOUND_PREFIX": "スクリプトが見つかりません:",
"NERD_PATH_PREFIX": "パス:",
@@ -174,10 +170,6 @@
"FLATPAK_STEP_FINAL": "최종 결과는 다음과 같아야 합니다:",
"FLATPAK_CLOSE": "닫기",
"NERD_LOADING": "정보 불러오는 중...",
- "NERD_DLL_PATH": "DLL 경로",
- "NERD_NOT_AVAILABLE": "사용 불가",
- "NERD_DLL_HASH": "DLL SHA256 해시",
- "NERD_DETECTION_SOURCE": "감지 소스",
"NERD_LAUNCH_SCRIPT": "실행 스크립트",
"NERD_SCRIPT_NOT_FOUND_PREFIX": "스크립트 없음:",
"NERD_PATH_PREFIX": "경로:",
@@ -309,10 +301,6 @@
"FLATPAK_STEP_FINAL": "Final result should look like:",
"FLATPAK_CLOSE": "Close",
"NERD_LOADING": "Loading information...",
- "NERD_DLL_PATH": "DLL Path",
- "NERD_NOT_AVAILABLE": "Not available",
- "NERD_DLL_HASH": "DLL SHA256 Hash",
- "NERD_DETECTION_SOURCE": "Detection Source",
"NERD_LAUNCH_SCRIPT": "Launch Script",
"NERD_SCRIPT_NOT_FOUND_PREFIX": "Script not found:",
"NERD_PATH_PREFIX": "Path:",