From 7bc5f685186b1ff03ce0f7c99e7bec411beabaeb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Sun, 6 Sep 2026 22:04:54 -0400 Subject: feat: make ui suck less, frfr --- README.md | 41 +-------- defaults/i18n/ja.json | 5 +- defaults/i18n/ko.json | 5 +- defaults/i18n/template.json | 5 +- py_modules/lsfg_vk/config_schema.py | 52 ++--------- py_modules/lsfg_vk/configuration.py | 56 ++++-------- py_modules/lsfg_vk/installation.py | 6 +- py_modules/lsfg_vk/plugin.py | 128 --------------------------- py_modules/lsfg_vk/types.py | 31 +------ src/api/lsfgApi.ts | 36 +------- src/components/ConfigurationSection.tsx | 10 +-- src/components/ConfigurationTab.tsx | 86 +++++++++++++----- src/components/Content.tsx | 45 ++++++++-- src/components/FgmodClipboardButton.tsx | 110 ----------------------- src/components/FpsMultiplierControl.tsx | 70 ++++----------- src/components/GameConfigurationControls.tsx | 17 ++++ src/components/GameConfigurationSelector.tsx | 67 ++++++++++---- src/components/NowPlayingTab.tsx | 32 +++++++ src/components/index.ts | 3 +- src/hooks/useGameConfiguration.ts | 61 ++++++++----- src/hooks/useLsfgHooks.ts | 61 +------------ src/i18n/languages.json | 15 +--- src/styles.ts | 2 +- src/utils/clipboardUtils.ts | 64 -------------- src/utils/toastUtils.ts | 22 ----- 25 files changed, 302 insertions(+), 728 deletions(-) delete mode 100644 src/components/FgmodClipboardButton.tsx create mode 100644 src/components/GameConfigurationControls.tsx create mode 100644 src/components/NowPlayingTab.tsx delete mode 100644 src/utils/clipboardUtils.ts diff --git a/README.md b/README.md index 4be9cca..13918fd 100644 --- a/README.md +++ b/README.md @@ -34,56 +34,19 @@ A Decky plugin that streamlines the installation of **lsfg-vk** ([Lossless Scali 2. **Open the plugin** from the Decky menu 3. **Click "Install lsfg-vk"** to automatically set up the lsfg-vk vulkan layer 4. **Configure settings** using the plugin's UI - select Default or a running/configured game and adjust the upstream lsfg-vk settings -5. **Apply launch option** to games you want to use frame generation with: - - Add `~/lsfg %command%` to your game's launch options in Steam Properties - - Or use the "Launch Option Clipboard" button in the plugin to copy the command 6. **Launch your game** - frame generation activates when the game's Steam AppID matches an assigned upstream profile -## Configuration Options - -The plugin edits upstream lsfg-vk v2 profiles directly. Unassigned games leave the layer unloaded. - ### Core Settings + - **FPS Multiplier**: Choose 2x, 3x, or 4x frame generation - **Flow Scale**: Adjust motion estimation quality (lower = better performance, higher = better quality) -- **Performance Mode**: Uses a lighter processing model - recommended for most games +- **Performance Mode**: Uses a lighter processing model (recommended for most games) - **FP16 Acceleration**: Use half-precision acceleration when supported ## Feedback and Support For per-game feedback and community support, please join the [decky-lsfg-vk Discord Channel](https://discord.gg/TwvHdVucC3) -## Troubleshooting - -**Frame generation not working?** -- Ensure you've added `~/lsfg %command%` to your game's launch options -- 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 - -**Performance issues?** -- Lower the Flow Scale setting for better performance -- Enable Performance Mode (recommended for most games) -- Try reducing the FPS multiplier from 4x to 2x or 3x -- Consider using the experimental FPS limit feature for DirectX games - -## What it does - -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 -- 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 - - **Performance Mode**: Use lighter processing for better performance - - **FP16 Acceleration**: Use half-precision acceleration when supported -- **Present Mode**: Override FIFO/VSync behavior -- **Swapchain Image Count**: Preserve the application's swapchain image count -- **Hot-reloading**: Upstream reloads settings for the active profile; profile assignment itself applies on the next launch -- Easy uninstallation that removes all installed files when no longer needed - ## Credits - **[PancakeTAS](https://lsfg-vk.dev/)** for creating the lsfg-vk Vulkan compatibility layer diff --git a/defaults/i18n/ja.json b/defaults/i18n/ja.json index 7b97bc5..92a8701 100644 --- a/defaults/i18n/ja.json +++ b/defaults/i18n/ja.json @@ -87,8 +87,5 @@ "PROFILE_DELETE_DESC_SUFFIX": "この操作は取り消せません。", "PROFILE_DELETE_BTN": "削除", "PROFILE_CANNOT_RENAME_TITLE": "デフォルトプロファイルの名前は変更できません", - "PROFILE_CANNOT_RENAME_MSG": "デフォルトプロファイルの名前は変更できません", - "CLIPBOARD_COPIED": "クリップボードにコピーしました", - "CLIPBOARD_COPYING": "コピー中...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "デフォルトプロファイルの名前は変更できません" } diff --git a/defaults/i18n/ko.json b/defaults/i18n/ko.json index cbc3a8e..947640d 100644 --- a/defaults/i18n/ko.json +++ b/defaults/i18n/ko.json @@ -87,8 +87,5 @@ "PROFILE_DELETE_DESC_SUFFIX": "이 작업은 취소할 수 없습니다.", "PROFILE_DELETE_BTN": "삭제", "PROFILE_CANNOT_RENAME_TITLE": "기본 프로필 이름 변경 불가", - "PROFILE_CANNOT_RENAME_MSG": "기본 프로필의 이름은 변경할 수 없습니다", - "CLIPBOARD_COPIED": "클립보드에 복사됨", - "CLIPBOARD_COPYING": "복사 중...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "기본 프로필의 이름은 변경할 수 없습니다" } diff --git a/defaults/i18n/template.json b/defaults/i18n/template.json index b9dfcc5..fce97b4 100644 --- a/defaults/i18n/template.json +++ b/defaults/i18n/template.json @@ -87,8 +87,5 @@ "PROFILE_DELETE_DESC_SUFFIX": "? This action cannot be undone.", "PROFILE_DELETE_BTN": "Delete", "PROFILE_CANNOT_RENAME_TITLE": "Cannot rename default profile", - "PROFILE_CANNOT_RENAME_MSG": "The default profile cannot be renamed", - "CLIPBOARD_COPIED": "Copied to clipboard", - "CLIPBOARD_COPYING": "Copying...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "The default profile cannot be renamed" } diff --git a/py_modules/lsfg_vk/config_schema.py b/py_modules/lsfg_vk/config_schema.py index 4ae1f4c..675ab61 100644 --- a/py_modules/lsfg_vk/config_schema.py +++ b/py_modules/lsfg_vk/config_schema.py @@ -4,18 +4,14 @@ import json import sys import tomllib from pathlib import Path -from typing import Any, Dict, TypedDict, cast +from typing import Any, Dict, TypedDict sys.path.insert(0, str(Path(__file__).parent.parent.parent)) -DEFAULT_PROFILE_NAME = "default" ConfigurationData = Dict[str, Any] class ProfileData(TypedDict): - # Internal compatibility field; the public API no longer exposes a - # currently selected profile. - current_profile: str profiles: Dict[str, Dict[str, Any]] global_config: Dict[str, Any] @@ -57,19 +53,6 @@ class ConfigurationManager: def get_defaults() -> Dict[str, Any]: return {**GLOBAL_DEFAULTS, **PROFILE_DEFAULTS} - @staticmethod - def get_field_names() -> list[str]: - return list(ConfigurationManager.get_defaults()) - - @staticmethod - def get_field_types() -> Dict[str, str]: - return { - "dll": "string", "no_fp16": "boolean", "active_in": "array", - "pacing_mode": "string", "multiplier": "integer", "flow_scale": "float", - "performance_mode": "boolean", "override_present_mode": "boolean", - "preserve_swapchain_image_count": "boolean", - } - @staticmethod def validate_config(config: Dict[str, Any]) -> Dict[str, Any]: result = {**PROFILE_DEFAULTS, **GLOBAL_DEFAULTS} @@ -110,15 +93,6 @@ class ConfigurationManager: raw["no_fp16"] = global_config.get("no_fp16", False) return ConfigurationManager.validate_config(raw) - @staticmethod - def generate_toml_content(config: Dict[str, Any]) -> str: - data: ProfileData = { - "current_profile": DEFAULT_PROFILE_NAME, - "profiles": {DEFAULT_PROFILE_NAME: dict(config)}, - "global_config": {"dll": config.get("dll", ""), "no_fp16": config.get("no_fp16", False)}, - } - return ConfigurationManager.generate_toml_content_multi_profile(data) - @staticmethod def generate_toml_content_multi_profile(profile_data: ProfileData) -> str: global_config = {**GLOBAL_DEFAULTS, **profile_data.get("global_config", {})} @@ -127,7 +101,9 @@ class ConfigurationManager: if dll: lines.append(f"dll = {_toml_value(dll)}") lines.append(f"allow_fp16 = {_toml_value(not bool(global_config.get('no_fp16', False)))}") - profiles = sorted(profile_data["profiles"].items(), key=lambda item: (item[0] != DEFAULT_PROFILE_NAME, item[0])) + profiles = sorted(profile_data["profiles"].items()) + if not profiles: + profiles = [("", {})] for name, raw in profiles: config = ConfigurationManager.validate_config({**raw, **global_config}) lines.extend(["", "[[profile]]", f"name = {_toml_value(name)}"]) @@ -157,16 +133,11 @@ class ConfigurationManager: profiles: Dict[str, Dict[str, Any]] = {} source_profiles = data.get("game", []) if version == 1 else data.get("profile", []) for profile in source_profiles: - name = str(profile.get("exe" if version == 1 else "name", DEFAULT_PROFILE_NAME)) - profiles[name] = ConfigurationManager._config_from_profile(profile, global_config) - if not profiles: - profiles[DEFAULT_PROFILE_NAME] = ConfigurationManager.validate_config(global_config) - elif DEFAULT_PROFILE_NAME not in profiles: - source = profiles.get("decky-lsfg-vk", next(iter(profiles.values()))) - profiles[DEFAULT_PROFILE_NAME] = {**source, "active_in": []} - if profiles.get("decky-lsfg-vk", {}).get("active_in", []) == []: - profiles.pop("decky-lsfg-vk", None) - return {"current_profile": DEFAULT_PROFILE_NAME, "profiles": profiles, "global_config": global_config} + name = str(profile.get("exe" if version == 1 else "name", "")) + config = ConfigurationManager._config_from_profile(profile, global_config) + if config["active_in"]: + profiles[name] = config + return {"profiles": profiles, "global_config": global_config} @staticmethod def is_legacy_v1(content: str) -> bool: @@ -174,8 +145,3 @@ class ConfigurationManager: return tomllib.loads(content).get("version") == 1 except tomllib.TOMLDecodeError: return False - - @staticmethod - def parse_toml_content(content: str) -> Dict[str, Any]: - data = ConfigurationManager.parse_toml_content_multi_profile(content) - return cast(Dict[str, Any], data["profiles"][DEFAULT_PROFILE_NAME]) diff --git a/py_modules/lsfg_vk/configuration.py b/py_modules/lsfg_vk/configuration.py index a4ee2cc..bec3828 100644 --- a/py_modules/lsfg_vk/configuration.py +++ b/py_modules/lsfg_vk/configuration.py @@ -2,7 +2,7 @@ import re from typing import Any, Dict from .base_service import BaseService -from .config_schema import ConfigurationManager, DEFAULT_PROFILE_NAME, ProfileData +from .config_schema import ConfigurationManager, ProfileData from .runtime_service import RuntimeService @@ -14,8 +14,7 @@ class ConfigurationService(BaseService): self.runtime_service = runtime_service or RuntimeService(logger=self.log) def _default_data(self) -> ProfileData: - defaults = ConfigurationManager.validate_config({}) - return {"current_profile": DEFAULT_PROFILE_NAME, "profiles": {DEFAULT_PROFILE_NAME: defaults}, "global_config": {"dll": "", "no_fp16": False}} + return {"profiles": {}, "global_config": {"dll": "", "no_fp16": False}} def _get_profile_data(self) -> ProfileData: if not self.config_file_path.exists(): @@ -32,8 +31,6 @@ class ConfigurationService(BaseService): name = str(game_name).strip() if not name: raise ValueError("game name is required") - if name == DEFAULT_PROFILE_NAME: - name = f"{name} ({appid})" existing = data["profiles"].get(name) if existing is not None and str(appid) not in existing.get("active_in", []): name = f"{name} ({appid})" @@ -54,14 +51,6 @@ class ConfigurationService(BaseService): def _public_config(config: Dict[str, Any]) -> Dict[str, Any]: return ConfigurationManager.validate_config(config) - def get_config(self) -> Dict[str, Any]: - try: - data = self._get_profile_data() - return self._success_response(dict, config=self._public_config(data["profiles"][DEFAULT_PROFILE_NAME])) - except Exception as error: - self.log.error(f"Error reading lsfg config: {error}") - return self._error_response(dict, str(error), config=None) - def get_game_configs(self) -> Dict[str, Any]: try: data = self._get_profile_data() @@ -71,26 +60,25 @@ class ConfigurationService(BaseService): if len(active_in) != 1 or not re.fullmatch(r"-?[0-9]+", str(active_in[0])): continue games.append({"appid": str(active_in[0]), "profile": name, "config": self._public_config(raw)}) - return self._success_response(dict, default=self._public_config(data["profiles"][DEFAULT_PROFILE_NAME]), games=games) + return self._success_response(dict, global_config=dict(data["global_config"]), games=games) except Exception as error: self.log.error(f"Error reading game configs: {error}") - return self._error_response(dict, str(error), default=None, games=[]) - - def get_game_config(self, appid: str) -> Dict[str, Any]: - try: - data = self._get_profile_data() - _, profile = self._profile_for_appid(data, appid) - return self._success_response(dict, appid=str(appid), exists=profile is not None, config=self._public_config(profile or data["profiles"][DEFAULT_PROFILE_NAME])) - except Exception as error: - return self._error_response(dict, str(error), appid=str(appid), exists=False, config=None) + return self._error_response(dict, str(error), games=[]) def update_game_config(self, appid: str, game_name: str, config: Dict[str, Any]) -> Dict[str, Any]: try: data = self._get_profile_data() old_name, _ = self._profile_for_appid(data, appid) name = self._profile_name(data, appid, game_name) - validated = self._public_config(config) + merged_config = {**data["global_config"], **config} + if not config.get("dll"): + merged_config["dll"] = data["global_config"].get("dll", "") + validated = self._public_config(merged_config) validated["active_in"] = [str(appid)] + data["global_config"] = { + "dll": validated["dll"], + "no_fp16": validated["no_fp16"], + } if old_name and old_name != name: data["profiles"].pop(old_name, None) data["profiles"][name] = validated @@ -106,27 +94,15 @@ class ConfigurationService(BaseService): if name: data["profiles"].pop(name, None) self._save_profile_data(data) - return self._success_response(dict, appid=str(appid), config=self._public_config(data["profiles"][DEFAULT_PROFILE_NAME])) + return self._success_response(dict, appid=str(appid), exists=False) except Exception as error: return self._error_response(dict, str(error), appid=str(appid), config=None) def reset_all_game_configs(self) -> Dict[str, Any]: try: data = self._get_profile_data() - data["profiles"] = {DEFAULT_PROFILE_NAME: data["profiles"][DEFAULT_PROFILE_NAME]} - self._save_profile_data(data) - return self._success_response(dict, default=self._public_config(data["profiles"][DEFAULT_PROFILE_NAME]), games=[]) - except Exception as error: - return self._error_response(dict, str(error), default=None, games=[]) - - def update_config_from_dict(self, config: Dict[str, Any]) -> Dict[str, Any]: - try: - data = self._get_profile_data() - validated = self._public_config(config) - validated["active_in"] = [] - data["profiles"][DEFAULT_PROFILE_NAME] = validated - data["global_config"] = {"dll": validated.get("dll", ""), "no_fp16": validated.get("no_fp16", False)} + data["profiles"] = {} self._save_profile_data(data) - return self._success_response(dict, config=validated) + return self._success_response(dict, global_config=dict(data["global_config"]), games=[]) except Exception as error: - return self._error_response(dict, str(error), config=None) + return self._error_response(dict, str(error), games=[]) diff --git a/py_modules/lsfg_vk/installation.py b/py_modules/lsfg_vk/installation.py index 0a728c7..09bd3a3 100644 --- a/py_modules/lsfg_vk/installation.py +++ b/py_modules/lsfg_vk/installation.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Dict from .base_service import BaseService -from .config_schema import ConfigurationManager, DEFAULT_PROFILE_NAME, ProfileData +from .config_schema import ConfigurationManager, ProfileData from .constants import ( ARCHIVE_FILENAME, BIN_DIR, @@ -141,8 +141,7 @@ class InstallationService(BaseService): else: default = dict(ConfigurationManager.get_defaults()) profile_data = ProfileData( - current_profile=DEFAULT_PROFILE_NAME, - profiles={DEFAULT_PROFILE_NAME: default}, + profiles={}, global_config={ "dll": default.get("dll", ""), "no_fp16": default.get("no_fp16", False), @@ -155,7 +154,6 @@ class InstallationService(BaseService): profile_data["profiles"][profile_name] = ConfigurationManager.validate_config( {**defaults, **raw_profile, **profile_data["global_config"]} ) - profile_data["current_profile"] = DEFAULT_PROFILE_NAME return profile_data def _resolve_dll_path(self, profile_data: ProfileData) -> bool: diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index 8c788a8..6977b9c 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -7,13 +7,11 @@ Vulkan layer for frame generation on Steam Deck. import os from typing import Dict, Any -from pathlib import Path import decky from .installation import InstallationService from .configuration import ConfigurationService -from .config_schema import ConfigurationManager from .flatpak_service import FlatpakService from .runtime_service import RuntimeService from .steam_service import SteamService @@ -63,46 +61,12 @@ class Plugin: """ return self.installation_service.uninstall() - async def get_lsfg_config(self) -> Dict[str, Any]: - """Read current lsfg script configuration - - Returns: - ConfigurationResponse dict with current configuration or error - """ - return self.configuration_service.get_config() - - async def get_config_schema(self) -> Dict[str, Any]: - """Get configuration schema information for frontend - - Returns: - Dict with field names, types, defaults, and profile information - """ - return { - "field_names": ConfigurationManager.get_field_names(), - "field_types": ConfigurationManager.get_field_types(), - "defaults": ConfigurationManager.get_defaults(), - } - - async def update_lsfg_config(self, config: Dict[str, Any]) -> Dict[str, Any]: - """Update lsfg TOML configuration using object-based API (single source of truth) - - Args: - config: Configuration data dictionary containing all settings - - Returns: - ConfigurationResponse dict with success status - """ - return self.configuration_service.update_config_from_dict(config) - async def get_game_configs(self) -> Dict[str, Any]: return self.configuration_service.get_game_configs() async def get_installed_games(self) -> Dict[str, Any]: return self.steam_service.get_installed_games() - async def get_game_config(self, appid: str) -> Dict[str, Any]: - return self.configuration_service.get_game_config(appid) - async def update_game_config(self, appid: str, game_name: str, config: Dict[str, Any]) -> Dict[str, Any]: return self.configuration_service.update_game_config(appid, game_name, config) @@ -112,72 +76,6 @@ class Plugin: async def reset_all_game_configs(self) -> Dict[str, Any]: return self.configuration_service.reset_all_game_configs() - async def _legacy_get_profiles(self) -> Dict[str, Any]: - """Get list of all profiles and current profile - - Returns: - ProfilesResponse dict with profile list and current profile - """ - return self.configuration_service.get_game_configs() - - async def _legacy_create_profile(self, profile_name: str, source_profile: str = None) -> Dict[str, Any]: - """Create a new profile - - Args: - profile_name: Name for the new profile - source_profile: Optional source profile to copy from (default: current profile) - - Returns: - ProfileResponse dict with success status - """ - return {"success": False, "error": "Named profiles were replaced by per-game AppID profiles"} - - async def _legacy_delete_profile(self, profile_name: str) -> Dict[str, Any]: - """Delete a profile - - Args: - profile_name: Name of the profile to delete - - Returns: - ProfileResponse dict with success status - """ - return {"success": False, "error": "Named profiles were replaced by per-game AppID profiles"} - - async def _legacy_rename_profile(self, old_name: str, new_name: str) -> Dict[str, Any]: - """Rename a profile - - Args: - old_name: Current profile name - new_name: New profile name - - Returns: - ProfileResponse dict with success status - """ - return {"success": False, "error": "Named profiles were replaced by per-game AppID profiles"} - - async def _legacy_set_current_profile(self, profile_name: str) -> Dict[str, Any]: - """Set the current active profile - - Args: - profile_name: Name of the profile to set as current - - Returns: - ProfileResponse dict with success status - """ - return {"success": False, "error": "There is no globally selected profile"} - - async def _legacy_update_profile_config(self, profile_name: str, config: Dict[str, Any]) -> Dict[str, Any]: - """Update configuration for a specific profile - - Args: - profile_name: Name of the profile to update - config: Configuration data dictionary containing settings - - Returns: - ConfigurationResponse dict with success status - """ - return {"success": False, "error": "Use update_game_config with a Steam AppID"} - async def get_config_file_content(self) -> Dict[str, Any]: """Get the current config file content @@ -209,32 +107,6 @@ class Plugin: "error": f"Error reading config file: {str(e)}" } - async def check_fgmod_directory(self) -> Dict[str, Any]: - """Check if the fgmod directory exists in the home directory - - Returns: - Dict with exists status and directory path - """ - try: - home_path = Path(decky.DECKY_USER_HOME) - fgmod_path = home_path / "fgmod" - - exists = fgmod_path.exists() and fgmod_path.is_dir() - - return { - "success": True, - "exists": exists, - "path": str(fgmod_path) - } - - except Exception as e: - decky.logger.error(f"Error checking fgmod directory: {e}") - return { - "success": False, - "exists": False, - "error": str(e) - } - async def check_flatpak_extension_status(self) -> Dict[str, Any]: """Check status of lsfg-vk Flatpak runtime extensions diff --git a/py_modules/lsfg_vk/types.py b/py_modules/lsfg_vk/types.py index ef6bab7..7b85708 100644 --- a/py_modules/lsfg_vk/types.py +++ b/py_modules/lsfg_vk/types.py @@ -2,8 +2,7 @@ Type definitions for the lsfg-vk plugin responses. """ -from typing import TypedDict, Optional, List, Dict, Any -from .config_schema import ConfigurationData +from typing import TypedDict, Optional, List class BaseResponse(TypedDict): @@ -53,31 +52,3 @@ class SteamBranchStatusResponse(TypedDict): target_branch: str needs_switch: bool restart_required: bool - -class ConfigurationResponse(BaseResponse): - """Response for configuration operations""" - config: Optional[ConfigurationData] - message: Optional[str] - error: Optional[str] - - -class ProfileConfig(TypedDict): - """Configuration for a single profile""" - exe: str - config: ConfigurationData - - -class ProfilesResponse(BaseResponse): - """Response for per-game upstream profiles""" - default: Optional[ConfigurationData] - games: Optional[List[Dict[str, Any]]] - message: Optional[str] - error: Optional[str] - - -class ProfileResponse(BaseResponse): - """Response for a per-game upstream profile""" - appid: Optional[str] - config: Optional[ConfigurationData] - message: Optional[str] - error: Optional[str] diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 04d1309..8eaad98 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -32,12 +32,6 @@ export interface SteamBranchStatus { // Use centralized configuration data type export type LsfgConfig = ConfigurationData; -export interface ConfigResult { - success: boolean; - config?: LsfgConfig; - error?: string; -} - export interface ConfigUpdateResult { success: boolean; message?: string; @@ -51,10 +45,11 @@ export interface GameConfigEntry { } export interface InstalledGame { appid: string; name: string; nonSteam: boolean; } export interface InstalledGamesResult { success: boolean; games?: InstalledGame[]; error?: string; } +export interface GlobalConfig { dll: string; no_fp16: boolean; } export interface GameConfigsResult { success: boolean; - default?: LsfgConfig; + global_config?: GlobalConfig; games?: GameConfigEntry[]; error?: string; } @@ -65,12 +60,6 @@ export interface GameConfigResult extends ConfigUpdateResult { config?: LsfgConfig; } -export interface ConfigSchemaResult { - field_names: string[]; - field_types: Record; - defaults: ConfigurationData; -} - export interface FileContentResult { success: boolean; content?: string; @@ -78,13 +67,6 @@ export interface FileContentResult { error?: string; } -export interface FgmodCheckResult { - success: boolean; - exists: boolean; - path?: string; - error?: string; -} - // Flatpak management interfaces export interface FlatpakExtensionStatus { success: boolean; @@ -123,10 +105,7 @@ 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 getLosslessScalingBranchStatus = callable<[], SteamBranchStatus>("get_lossless_scaling_branch_status"); -export const getLsfgConfig = callable<[], ConfigResult>("get_lsfg_config"); -export const getConfigSchema = callable<[], ConfigSchemaResult>("get_config_schema"); export const getConfigFileContent = callable<[], FileContentResult>("get_config_file_content"); -export const checkFgmodDirectory = callable<[], FgmodCheckResult>("check_fgmod_directory"); // Flatpak management API functions export const checkFlatpakExtensionStatus = callable<[], FlatpakExtensionStatus>("check_flatpak_extension_status"); @@ -136,19 +115,8 @@ export const getFlatpakApps = callable<[], FlatpakAppInfo>("get_flatpak_apps"); export const setFlatpakAppOverride = callable<[string], FlatpakOperationResult>("set_flatpak_app_override"); export const removeFlatpakAppOverride = callable<[string], FlatpakOperationResult>("remove_flatpak_app_override"); -// Updated config function using object-based configuration (single source of truth) -export const updateLsfgConfig = callable< - [ConfigurationData], - ConfigUpdateResult ->("update_lsfg_config"); export const getGameConfigs = callable<[], GameConfigsResult>("get_game_configs"); export const getInstalledGames = callable<[], InstalledGamesResult>("get_installed_games"); -export const getGameConfig = callable<[string], GameConfigResult>("get_game_config"); export const updateGameConfig = callable<[string, string, LsfgConfig], GameConfigResult>("update_game_config"); export const resetGameConfig = callable<[string], GameConfigResult>("reset_game_config"); export const resetAllGameConfigs = callable<[], GameConfigsResult>("reset_all_game_configs"); - -// Legacy helper function for backward compatibility -export const updateLsfgConfigFromObject = async (config: ConfigurationData): Promise => { - return updateLsfgConfig(config); -}; diff --git a/src/components/ConfigurationSection.tsx b/src/components/ConfigurationSection.tsx index c2bba7e..1f17264 100644 --- a/src/components/ConfigurationSection.tsx +++ b/src/components/ConfigurationSection.tsx @@ -10,19 +10,19 @@ interface ConfigurationSectionProps { export function ConfigurationSection({ config, onConfigChange }: ConfigurationSectionProps) { return <> - onConfigChange(FLOW_SCALE, value)} /> + onConfigChange(FLOW_SCALE, value)} /> - onConfigChange(NO_FP16, !value)} /> + onConfigChange(NO_FP16, !value)} /> - onConfigChange(PERFORMANCE_MODE, value)} /> + onConfigChange(PERFORMANCE_MODE, value)} /> - onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> + onConfigChange(OVERRIDE_PRESENT_MODE, value)} /> - onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> + onConfigChange(PRESERVE_SWAPCHAIN_IMAGE_COUNT, value)} /> ; } diff --git a/src/components/ConfigurationTab.tsx b/src/components/ConfigurationTab.tsx index 6f3f474..dab6f19 100644 --- a/src/components/ConfigurationTab.tsx +++ b/src/components/ConfigurationTab.tsx @@ -1,19 +1,17 @@ -import { PanelSection } from "@decky/ui"; +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { useEffect, useRef, useState } from "react"; import { ConfigurationData } from "../config/configSchema"; import { GameTarget } from "../hooks/useGameConfiguration"; -import { ConfigurationSection } from "./ConfigurationSection"; -import { FgmodClipboardButton } from "./FgmodClipboardButton"; -import { FpsMultiplierControl } from "./FpsMultiplierControl"; +import { GameConfigurationControls } from "./GameConfigurationControls"; import { GameConfigurationSelector } from "./GameConfigurationSelector"; -import t from "../i18n/i18n"; interface ConfigurationTabProps { config: ConfigurationData; targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onEnable: (appid: string) => Promise; onReset: () => Promise; onResetAll: () => Promise; } @@ -22,33 +20,77 @@ export function ConfigurationTab({ config, targets, runningGame, - selectedAppId, onSelect, onConfigChange, + onEnable, onReset, onResetAll, }: ConfigurationTabProps) { - return ( - <> - - - - + const [detailAppId, setDetailAppId] = useState(null); + const promptedRunningAppId = useRef(null); + + useEffect(() => { + if (!runningGame || runningGame.configured) { + promptedRunningAppId.current = null; + return; + } + if (promptedRunningAppId.current !== runningGame.appid && detailAppId === null) { + promptedRunningAppId.current = runningGame.appid; + setDetailAppId(runningGame.appid); + } + }, [detailAppId, runningGame?.appid, runningGame?.configured]); + + const selectedTarget = detailAppId ? targets.find((target) => target.appid === detailAppId) : null; + + if (detailAppId === null) { + return ( + { + onSelect(appid); + setDetailAppId(appid); + }} onResetAll={onResetAll} /> - - - - - + ); + } + + const profileLabel = selectedTarget?.name || "Game profile"; + const running = selectedTarget?.appid === runningGame?.appid; + const profileDescription = selectedTarget + ? `${selectedTarget.nonSteam ? "Non-Steam" : "Steam"} · App ID ${selectedTarget.appid} · ${selectedTarget.configured ? running && !runningGame?.configured ? "Profile saved · applies next launch" : "Profile active" : "Not configured · changes apply next launch"}` + : "Game is no longer available"; + + return ( + setDetailAppId(null)}> + + + + + + setDetailAppId(null)}>Back to games + - + + + { + if (selectedTarget?.configured) { + promptedRunningAppId.current = detailAppId; + await onReset(); + setDetailAppId(null); + } else if (detailAppId) { + await onEnable(detailAppId); + } + }} + > + {selectedTarget?.configured ? "Remove profile" : "Enable for next launch"} + + + ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index de8f996..9281d39 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,6 +1,6 @@ import { Tabs } from "@decky/ui"; -import { useEffect, useState } from "react"; -import { FaFileAlt, FaGamepad, FaLayerGroup, FaTools } from "react-icons/fa"; +import { useEffect, useRef, useState } from "react"; +import { FaFileAlt, FaGamepad, FaLayerGroup, FaList, FaTools } from "react-icons/fa"; import { ConfigurationData } from "../config/configSchema"; import { tabStyles } from "../styles"; import { useGameConfiguration } from "../hooks/useGameConfiguration"; @@ -9,10 +9,12 @@ import { useInstallationStatus } from "../hooks/useLsfgHooks"; import { ConfigFileTab } from "./ConfigFileTab"; import { ConfigurationTab } from "./ConfigurationTab"; import { FlatpaksTab } from "./FlatpaksTab"; +import { NowPlayingTab } from "./NowPlayingTab"; import { SetupTab } from "./SetupTab"; const tabIcons = { - configuration: , + nowPlaying: , + configuration: , flatpak: , configFile: , setup: , @@ -33,9 +35,9 @@ export function Content() { config, targets, runningGame, - selectedAppId, setSelectedAppId, save, + enable, resetSelected, resetAll, reload, @@ -48,10 +50,27 @@ export function Content() { steamBranchStatus?.success === true && steamBranchStatus.installed && !steamBranchStatus.needs_switch; + const previousRunningState = useRef<{ appid: string; configured: boolean } | null>(null); useEffect(() => { - setTab(setupComplete ? "Configuration" : "Setup"); - }, [setupComplete]); + if (!setupComplete) { + setTab("Setup"); + return; + } + setTab((current) => current === "Setup" ? (runningGame?.configured ? "NowPlaying" : "Configuration") : current); + }, [runningGame?.configured, setupComplete]); + + useEffect(() => { + if (!setupComplete) return; + const current = runningGame ? { appid: runningGame.appid, configured: runningGame.configured } : null; + const previous = previousRunningState.current; + previousRunningState.current = current; + if (current?.appid && (current.appid !== previous?.appid || current.configured !== previous?.configured)) { + setTab(current.configured ? "NowPlaying" : "Configuration"); + } else if (!current && previous) { + setTab((currentTab) => currentTab === "NowPlaying" ? "Configuration" : currentTab); + } + }, [runningGame?.appid, runningGame?.configured, setupComplete]); useEffect(() => { if (isInstalled) void reload(); @@ -88,6 +107,18 @@ export function Content() { const tabs = setupComplete ? [ + ...(runningGame?.configured ? [{ + id: "NowPlaying", + title: tabIcons.nowPlaying, + content: ( + + ), + }] : []), { id: "Configuration", title: tabIcons.configuration, @@ -96,9 +127,9 @@ export function Content() { config={config} targets={targets} runningGame={runningGame} - selectedAppId={selectedAppId} onSelect={setSelectedAppId} onConfigChange={handleConfigChange} + onEnable={enable} onReset={resetSelected} onResetAll={resetAll} /> diff --git a/src/components/FgmodClipboardButton.tsx b/src/components/FgmodClipboardButton.tsx deleted file mode 100644 index efc5490..0000000 --- a/src/components/FgmodClipboardButton.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState, useEffect } from "react"; -import { PanelSectionRow, ButtonItem } from "@decky/ui"; -import { FaClipboard, FaCheck } from "react-icons/fa"; -import { checkFgmodDirectory } from "../api/lsfgApi"; -import { showClipboardErrorToast } from "../utils/toastUtils"; -import { copyWithVerification } from "../utils/clipboardUtils"; -import t from '../i18n/i18n'; - -export function FgmodClipboardButton() { - const [isLoading, setIsLoading] = useState(false); - const [showSuccess, setShowSuccess] = useState(false); - const [fgmodExists, setFgmodExists] = useState(false); - const [checkingFgmod, setCheckingFgmod] = useState(true); - - // Check for fgmod directory on component mount - useEffect(() => { - const checkFgmod = async () => { - try { - const result = await checkFgmodDirectory(); - setFgmodExists(result.exists); - } catch (error) { - console.error("Error checking fgmod directory:", error); - setFgmodExists(false); - } finally { - setCheckingFgmod(false); - } - }; - - checkFgmod(); - }, []); - - // Reset success state after 3 seconds - useEffect(() => { - if (showSuccess) { - const timer = setTimeout(() => { - setShowSuccess(false); - }, 3000); - return () => clearTimeout(timer); - } - return undefined; - }, [showSuccess]); - - const copyToClipboard = async () => { - if (isLoading || showSuccess) return; - - setIsLoading(true); - try { - const text = "~/fgmod/fgmod ~/lsfg %command%"; - const { success, verified } = await copyWithVerification(text); - - if (success) { - // Show success feedback in the button instead of toast - setShowSuccess(true); - if (!verified) { - // Copy worked but verification failed - still show success - console.log('Copy verification failed but copy likely worked'); - } - } else { - showClipboardErrorToast(); - } - } catch (error) { - showClipboardErrorToast(); - } finally { - setIsLoading(false); - } - }; - - // Don't render if fgmod directory doesn't exist or we're still checking - if (checkingFgmod || !fgmodExists) { - return null; - } - - return ( - - -
- {showSuccess ? ( - - ) : isLoading ? ( - - ) : ( - - )} -
- {showSuccess ? t('CLIPBOARD_COPIED', 'Copied to clipboard') : isLoading ? t('CLIPBOARD_COPYING', 'Copying...') : t('CLIPBOARD_LSFG_FGMOD', 'LSFG + DeckyFG')} -
-
-
- -
- ); -} diff --git a/src/components/FpsMultiplierControl.tsx b/src/components/FpsMultiplierControl.tsx index 5069c9a..e197dc3 100644 --- a/src/components/FpsMultiplierControl.tsx +++ b/src/components/FpsMultiplierControl.tsx @@ -1,4 +1,4 @@ -import { PanelSectionRow, DialogButton, Focusable } from "@decky/ui"; +import { PanelSectionRow, SliderField } from "@decky/ui"; import { ConfigurationData } from "../config/configSchema"; import { MULTIPLIER } from "../config/generatedConfigSchema"; import t from "../i18n/i18n"; @@ -12,62 +12,22 @@ export function FpsMultiplierControl({ config, onConfigChange }: FpsMultiplierControlProps) { + const multiplierLabel = config.multiplier === 1 + ? t("MULTIPLIER_OFF", "Off") + : `${config.multiplier}x`; + return ( - - onConfigChange(MULTIPLIER, Math.max(1, config.multiplier - 1))} - disabled={config.multiplier <= 1} - > - − - -
4 ? "red" : "white", - minWidth: "60px", - textAlign: "center" - }} - > - {config.multiplier === 1 ? t('MULTIPLIER_OFF', 'OFF') : `${config.multiplier}X`} -
- onConfigChange(MULTIPLIER, Math.min(4, config.multiplier + 1))} - disabled={config.multiplier >= 4} - > - + - -
+ void onConfigChange(MULTIPLIER, value)} + />
); } diff --git a/src/components/GameConfigurationControls.tsx b/src/components/GameConfigurationControls.tsx new file mode 100644 index 0000000..bdf9985 --- /dev/null +++ b/src/components/GameConfigurationControls.tsx @@ -0,0 +1,17 @@ +import { ConfigurationData } from "../config/configSchema"; +import { ConfigurationSection } from "./ConfigurationSection"; +import { FpsMultiplierControl } from "./FpsMultiplierControl"; + +interface Props { + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; +} + +export function GameConfigurationControls({ config, onConfigChange }: Props) { + return ( + <> + + + + ); +} diff --git a/src/components/GameConfigurationSelector.tsx b/src/components/GameConfigurationSelector.tsx index a231477..fcdd0aa 100644 --- a/src/components/GameConfigurationSelector.tsx +++ b/src/components/GameConfigurationSelector.tsx @@ -1,29 +1,58 @@ -import { Dropdown, DropdownOption, PanelSectionRow, ButtonItem } from "@decky/ui"; +import { ButtonItem, Field, PanelSectionRow } from "@decky/ui"; import { GameTarget } from "../hooks/useGameConfiguration"; interface Props { targets: GameTarget[]; runningGame: GameTarget | null; - selectedAppId: string; onSelect: (appid: string) => void; - onReset: () => Promise; onResetAll: () => Promise; } -export function GameConfigurationSelector({ targets, runningGame, selectedAppId, onSelect, onReset, onResetAll }: Props) { - const options: DropdownOption[] = [ - { data: "", label: runningGame ? `Default (editing template) · ${runningGame.name}` : "Default" }, - ...targets.map((target) => ({ data: target.appid, label: `${target.nonSteam ? "Non-Steam · " : ""}${target.name} · ${target.appid}` })), - ]; - return <> - - onSelect(String(option.data))} /> - - - void onReset()} disabled={!selectedAppId}>Reset selected game - - - void onResetAll()} disabled={!targets.some((target) => target.configured)}>Reset all game profiles - - ; +const profileDescription = (target: GameTarget, running: boolean, active: boolean) => [ + running ? "Now playing" : "", + target.nonSteam ? "Non-Steam" : "Steam", + target.configured + ? active ? "Profile active" : "Profile saved · applies next launch" + : "Not configured · changes apply next launch", +].filter(Boolean).join(" · "); + +export function GameConfigurationSelector({ targets, runningGame, onSelect, onResetAll }: Props) { + const games = [...targets].sort((a, b) => { + if (a.appid === runningGame?.appid) return -1; + if (b.appid === runningGame?.appid) return 1; + return a.name.localeCompare(b.name); + }); + + return ( + <> + {games.length === 0 && ( + + + + )} + {games.map((game) => ( + + onSelect(game.appid)} + highlightOnFocus + /> + + ))} + + void onResetAll()} + disabled={!targets.some((target) => target.configured)} + > + Remove all profiles + + + + ); } diff --git a/src/components/NowPlayingTab.tsx b/src/components/NowPlayingTab.tsx new file mode 100644 index 0000000..956db4a --- /dev/null +++ b/src/components/NowPlayingTab.tsx @@ -0,0 +1,32 @@ +import { ButtonItem, Field, Focusable, PanelSection, PanelSectionRow } from "@decky/ui"; +import { ConfigurationData } from "../config/configSchema"; +import { GameTarget } from "../hooks/useGameConfiguration"; +import { GameConfigurationControls } from "./GameConfigurationControls"; + +interface Props { + game: GameTarget; + config: ConfigurationData; + onConfigChange: (fieldName: keyof ConfigurationData, value: boolean | number | string | string[]) => Promise; + onRemove: () => Promise; +} + +export function NowPlayingTab({ game, config, onConfigChange, onRemove }: Props) { + return ( + + + + + + + + + void onRemove()}> + Remove profile + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 5089b6f..424a360 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,9 +3,10 @@ export { StatusDisplay } from "./StatusDisplay"; export { InstallationButton } from "./InstallationButton"; export { ConfigurationSection } from "./ConfigurationSection"; export { FpsMultiplierControl } from "./FpsMultiplierControl"; -export { FgmodClipboardButton } from "./FgmodClipboardButton"; export { ConfigurationTab } from "./ConfigurationTab"; export { SetupTab } from "./SetupTab"; export { ConfigFileTab } from "./ConfigFileTab"; export { FlatpaksTab } from "./FlatpaksTab"; export { GameConfigurationSelector } from "./GameConfigurationSelector"; +export { GameConfigurationControls } from "./GameConfigurationControls"; +export { NowPlayingTab } from "./NowPlayingTab"; diff --git a/src/hooks/useGameConfiguration.ts b/src/hooks/useGameConfiguration.ts index b177551..d279a8c 100644 --- a/src/hooks/useGameConfiguration.ts +++ b/src/hooks/useGameConfiguration.ts @@ -1,37 +1,40 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Router } from "@decky/ui"; -import { getGameConfigs, getInstalledGames, updateGameConfig, updateLsfgConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type InstalledGame } from "../api/lsfgApi"; +import { getGameConfigs, getInstalledGames, updateGameConfig, resetGameConfig, resetAllGameConfigs, type GameConfigEntry, type GlobalConfig, type InstalledGame } from "../api/lsfgApi"; import { ConfigurationData, getDefaults } from "../config/configSchema"; export interface GameTarget extends InstalledGame { configured: boolean; } export function useGameConfiguration() { - const [defaultConfig, setDefaultConfig] = useState(getDefaults()); const [games, setGames] = useState([]); + const [globalConfig, setGlobalConfig] = useState({ dll: "", no_fp16: false }); const [installedGames, setInstalledGames] = useState([]); + const [configsLoaded, setConfigsLoaded] = useState(false); const [selectedAppId, setSelectedAppId] = useState(""); const [runningGame, setRunningGame] = useState(null); - const autoSelected = useRef(false); + const previousRunningAppId = useRef(null); const load = useCallback(async () => { const [result, installed] = await Promise.all([getGameConfigs(), getInstalledGames()]); if (result.success) { - setDefaultConfig(result.default || getDefaults()); + setGlobalConfig(result.global_config || { dll: "", no_fp16: false }); setGames(result.games || []); } if (installed.success) setInstalledGames(installed.games || []); + setConfigsLoaded(true); }, []); useEffect(() => { load(); }, [load]); useEffect(() => { const poll = () => { + if (!configsLoaded) return; const app = Router.MainRunningApp as any; if (!app?.appid) return setRunningGame(null); const appid = String(app.appid); const installed = installedGames.find((game) => game.appid === appid); const name = app.display_name || installed?.name; if (!name) return setRunningGame(null); - setRunningGame({ + setRunningGame((current) => current?.appid === appid ? current : { ...(installed || { appid, name, nonSteam: false }), name, configured: games.some((game) => game.appid === appid), @@ -40,13 +43,14 @@ export function useGameConfiguration() { poll(); const interval = window.setInterval(poll, 2000); return () => window.clearInterval(interval); - }, [games, installedGames]); + }, [configsLoaded, games, installedGames]); useEffect(() => { - if (!autoSelected.current && runningGame) { - autoSelected.current = true; - setSelectedAppId(runningGame.appid); + const appid = runningGame?.appid || null; + if (appid !== previousRunningAppId.current) { + previousRunningAppId.current = appid; + setSelectedAppId(appid || ""); } - }, [runningGame]); + }, [runningGame?.appid]); const targets = useMemo(() => { const configured = installedGames.map((game) => ({ ...game, configured: games.some((item) => item.appid === game.appid) })); @@ -54,25 +58,42 @@ export function useGameConfiguration() { if (runningGame && !configured.some((game) => game.appid === runningGame.appid)) configured.unshift(runningGame); return configured; }, [games, installedGames, runningGame]); - const selected = selectedAppId ? games.find((game) => game.appid === selectedAppId)?.config : defaultConfig; - const config = selected || defaultConfig; + const template = useMemo(() => ({ ...getDefaults(), ...globalConfig }), [globalConfig]); + const config = games.find((game) => game.appid === selectedAppId)?.config || template; const save = useCallback(async (next: ConfigurationData) => { - if (!selectedAppId) { - const result = await updateLsfgConfig(next); - if (result.success) setDefaultConfig(next); - return; - } const selectedTarget = targets.find((target) => target.appid === selectedAppId); if (!selectedTarget?.name) return; const result = await updateGameConfig(selectedAppId, selectedTarget.name, next); if (result.success) await load(); }, [load, selectedAppId, targets]); + const enable = useCallback(async (appid: string) => { + const target = targets.find((item) => item.appid === appid); + if (!target?.name) return false; + const result = await updateGameConfig(appid, target.name, template); + if (result.success) await load(); + return result.success; + }, [load, targets, template]); + const resetSelected = useCallback(async () => { - if (selectedAppId) { await resetGameConfig(selectedAppId); setSelectedAppId(""); await load(); } + if (selectedAppId) { + const result = await resetGameConfig(selectedAppId); + if (result.success) { + setRunningGame((current) => current?.appid === selectedAppId ? { ...current, configured: false } : current); + setSelectedAppId(""); + await load(); + } + } }, [load, selectedAppId]); - const resetAll = useCallback(async () => { await resetAllGameConfigs(); setSelectedAppId(""); await load(); }, [load]); + const resetAll = useCallback(async () => { + const result = await resetAllGameConfigs(); + if (result.success) { + setRunningGame((current) => current ? { ...current, configured: false } : current); + setSelectedAppId(""); + await load(); + } + }, [load]); - return { config, defaultConfig, games, targets, runningGame, selectedAppId, setSelectedAppId, save, resetSelected, resetAll, reload: load }; + return { config, games, targets, runningGame, selectedAppId, setSelectedAppId, save, enable, resetSelected, resetAll, reload: load }; } diff --git a/src/hooks/useLsfgHooks.ts b/src/hooks/useLsfgHooks.ts index ea8b3d0..0b71ee9 100644 --- a/src/hooks/useLsfgHooks.ts +++ b/src/hooks/useLsfgHooks.ts @@ -1,14 +1,9 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect } from "react"; import { checkLsfgVkInstalled, - getLsfgConfig, getLosslessScalingBranchStatus, - updateLsfgConfigFromObject, - type ConfigUpdateResult, type SteamBranchStatus } from "../api/lsfgApi"; -import { ConfigurationData, getDefaults } from "../config/configSchema"; -import { showErrorToast, ToastMessages } from "../utils/toastUtils"; export function useInstallationStatus() { const [isInstalled, setIsInstalled] = useState(false); @@ -60,57 +55,3 @@ export function useInstallationStatus() { checkInstallation }; } - -export function useLsfgConfig() { - const [config, setConfig] = useState(() => getDefaults()); - - const loadLsfgConfig = useCallback(async () => { - try { - const result = await getLsfgConfig(); - if (result.success && result.config) { - setConfig(result.config); - } else { - console.log("lsfg config not available, using defaults:", result.error); - setConfig(getDefaults()); - } - } catch (error) { - console.error("Error loading lsfg config:", error); - setConfig(getDefaults()); - } - }, []); - - const updateConfig = useCallback(async (newConfig: ConfigurationData): Promise => { - try { - const result = await updateLsfgConfigFromObject(newConfig); - if (result.success) { - setConfig(newConfig); - } else { - showErrorToast( - ToastMessages.CONFIG_UPDATE_ERROR.title, - result.error || ToastMessages.CONFIG_UPDATE_ERROR.body - ); - } - return result; - } catch (error) { - showErrorToast(ToastMessages.CONFIG_UPDATE_ERROR.title, String(error)); - return { success: false, error: String(error) }; - } - }, []); - - const updateField = useCallback(async (fieldName: keyof ConfigurationData, value: boolean | number | string): Promise => { - const newConfig = { ...config, [fieldName]: value }; - return updateConfig(newConfig); - }, [config, updateConfig]); - - useEffect(() => { - loadLsfgConfig(); - }, []); - - return { - config, - setConfig, - loadLsfgConfig, - updateConfig, - updateField - }; -} diff --git a/src/i18n/languages.json b/src/i18n/languages.json index 0528341..3132083 100644 --- a/src/i18n/languages.json +++ b/src/i18n/languages.json @@ -88,10 +88,7 @@ "PROFILE_DELETE_DESC_SUFFIX": "この操作は取り消せません。", "PROFILE_DELETE_BTN": "削除", "PROFILE_CANNOT_RENAME_TITLE": "デフォルトプロファイルの名前は変更できません", - "PROFILE_CANNOT_RENAME_MSG": "デフォルトプロファイルの名前は変更できません", - "CLIPBOARD_COPIED": "クリップボードにコピーしました", - "CLIPBOARD_COPYING": "コピー中...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "デフォルトプロファイルの名前は変更できません" }, "ko": { "CONTENT_FPS_MULTIPLIER": "FPS 배율", @@ -182,10 +179,7 @@ "PROFILE_DELETE_DESC_SUFFIX": "이 작업은 취소할 수 없습니다.", "PROFILE_DELETE_BTN": "삭제", "PROFILE_CANNOT_RENAME_TITLE": "기본 프로필 이름 변경 불가", - "PROFILE_CANNOT_RENAME_MSG": "기본 프로필의 이름은 변경할 수 없습니다", - "CLIPBOARD_COPIED": "클립보드에 복사됨", - "CLIPBOARD_COPYING": "복사 중...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "기본 프로필의 이름은 변경할 수 없습니다" }, "language_metadata": { "ko": { @@ -304,9 +298,6 @@ "PROFILE_DELETE_DESC_SUFFIX": "? This action cannot be undone.", "PROFILE_DELETE_BTN": "Delete", "PROFILE_CANNOT_RENAME_TITLE": "Cannot rename default profile", - "PROFILE_CANNOT_RENAME_MSG": "The default profile cannot be renamed", - "CLIPBOARD_COPIED": "Copied to clipboard", - "CLIPBOARD_COPYING": "Copying...", - "CLIPBOARD_LSFG_FGMOD": "LSFG + DeckyFG" + "PROFILE_CANNOT_RENAME_MSG": "The default profile cannot be renamed" } } diff --git a/src/styles.ts b/src/styles.ts index fe40a59..1bc089b 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,5 +1,5 @@ export const tabStyles = ` - .lsfg-vk-tabs > div > div:first-child::before { + .lsfg-vk-tabs > div > div:first-child { background: #0D141C; box-shadow: none; backdrop-filter: none; diff --git a/src/utils/clipboardUtils.ts b/src/utils/clipboardUtils.ts deleted file mode 100644 index 8a04caa..0000000 --- a/src/utils/clipboardUtils.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Clipboard utilities for reliable copy operations across different environments - */ - -/** - * Reliably copy text to clipboard using multiple fallback methods - * This is especially important in gaming mode where clipboard APIs may behave differently - */ -export async function copyToClipboard(text: string): Promise { - const tempInput = document.createElement('input'); - tempInput.value = text; - tempInput.style.position = 'absolute'; - tempInput.style.left = '-9999px'; - document.body.appendChild(tempInput); - - try { - tempInput.focus(); - tempInput.select(); - - let copySuccess = false; - try { - if (document.execCommand('copy')) { - copySuccess = true; - } - } catch (e) { - try { - await navigator.clipboard.writeText(text); - copySuccess = true; - } catch (clipboardError) { - console.error('Both copy methods failed:', e, clipboardError); - } - } - - return copySuccess; - } finally { - document.body.removeChild(tempInput); - } -} - -/** - * Verify that text was successfully copied to clipboard - */ -export async function verifyCopy(expectedText: string): Promise { - try { - const readBack = await navigator.clipboard.readText(); - return readBack === expectedText; - } catch (e) { - return true; - } -} - -/** - * Copy text with verification and return success status - */ -export async function copyWithVerification(text: string): Promise<{ success: boolean; verified: boolean }> { - const copySuccess = await copyToClipboard(text); - - if (!copySuccess) { - return { success: false, verified: false }; - } - - const verified = await verifyCopy(text); - return { success: true, verified }; -} diff --git a/src/utils/toastUtils.ts b/src/utils/toastUtils.ts index dce0a59..cbbbc55 100644 --- a/src/utils/toastUtils.ts +++ b/src/utils/toastUtils.ts @@ -53,14 +53,6 @@ export const ToastMessages = { CONFIG_UPDATE_ERROR: { title: "Update Failed", body: "Failed to update configuration" - }, - CLIPBOARD_SUCCESS: { - title: "Copied to Clipboard!", - body: "Launch option ready to paste" - }, - CLIPBOARD_ERROR: { - title: "Copy Failed", - body: "Unable to copy to clipboard" } } as const; @@ -99,17 +91,3 @@ export function showUninstallSuccessToast(): void { export function showUninstallErrorToast(error?: string): void { showErrorToast(ToastMessages.UNINSTALL_ERROR.title, error || ToastMessages.UNINSTALL_ERROR.body); } - -/** - * Show clipboard success toast - */ -export function showClipboardSuccessToast(): void { - showSuccessToast(ToastMessages.CLIPBOARD_SUCCESS.title, ToastMessages.CLIPBOARD_SUCCESS.body); -} - -/** - * Show clipboard error toast - */ -export function showClipboardErrorToast(): void { - showErrorToast(ToastMessages.CLIPBOARD_ERROR.title, ToastMessages.CLIPBOARD_ERROR.body); -} -- cgit v1.2.3