diff options
| author | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-05 23:44:37 -0400 |
|---|---|---|
| committer | xXJSONDeruloXx <danielhimebauch@gmail.com> | 2026-09-05 23:44:37 -0400 |
| commit | e8e469f99078858dc953663cba6f3428e80b1c5d (patch) | |
| tree | 6d0e6730bf439ec2cb5b3564c636ce8342466df1 | |
| parent | 77656ad88655effe0411808baf2fb90c629a24f8 (diff) | |
| download | decky-lsfg-vk-e8e469f99078858dc953663cba6f3428e80b1c5d.tar.gz decky-lsfg-vk-e8e469f99078858dc953663cba6f3428e80b1c5d.zip | |
refactor: delegate runtime checks to lsfg-vk
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | package.json | 17 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/config_schema.py | 9 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/configuration.py | 13 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/constants.py | 16 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/dll_detection.py | 223 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/flatpak_service.py | 80 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/installation.py | 70 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/plugin.py | 93 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/runtime_service.py | 120 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/types.py | 17 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 27 | ||||
| -rw-r--r-- | src/components/Content.tsx | 21 | ||||
| -rw-r--r-- | src/components/FlatpaksModal.tsx | 67 | ||||
| -rw-r--r-- | src/components/NerdStuffModal.tsx | 55 | ||||
| -rw-r--r-- | src/components/StatusDisplay.tsx | 21 | ||||
| -rw-r--r-- | src/hooks/useInstallationActions.ts | 12 | ||||
| -rw-r--r-- | src/hooks/useLsfgHooks.ts | 37 | ||||
| -rw-r--r-- | src/i18n/languages.json | 12 |
19 files changed, 383 insertions, 531 deletions
@@ -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:", |
