diff options
| author | Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> | 2025-07-16 17:16:38 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-16 17:16:38 -0400 |
| commit | 2bdfa4541638383a097cd10276c3a601ec3c8428 (patch) | |
| tree | 5eaab7f419b277196ed4c22939ce0ea74555f4ce | |
| parent | 44393f6e126c3dff196283a2079162e3eb9245a2 (diff) | |
| parent | 2811ba4e29cd27b5893fba676278f29b155068cb (diff) | |
| download | decky-lsfg-vk-2bdfa4541638383a097cd10276c3a601ec3c8428.tar.gz decky-lsfg-vk-2bdfa4541638383a097cd10276c3a601ec3c8428.zip | |
Merge pull request #31 from xXJSONDeruloXx/self-updater
non modal updater
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | py_modules/lsfg_vk/plugin.py | 199 | ||||
| -rw-r--r-- | src/api/lsfgApi.ts | 21 | ||||
| -rw-r--r-- | src/components/Content.tsx | 4 | ||||
| -rw-r--r-- | src/components/PluginUpdateChecker.tsx | 187 | ||||
| -rw-r--r-- | src/components/index.ts | 1 |
6 files changed, 413 insertions, 1 deletions
diff --git a/package.json b/package.json index 2268d1c..5919b3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lossless-scaling-vk", - "version": "0.3.2", + "version": "0.4.0", "description": "Use Lossless Scaling on the Steam Deck using the lsfg-vk vulkan layer", "type": "module", "scripts": { diff --git a/py_modules/lsfg_vk/plugin.py b/py_modules/lsfg_vk/plugin.py index b357ee6..3e37047 100644 --- a/py_modules/lsfg_vk/plugin.py +++ b/py_modules/lsfg_vk/plugin.py @@ -6,7 +6,12 @@ Vulkan layer for Lossless Scaling frame generation on Steam Deck. """ import os +import json +import subprocess +import urllib.request +import ssl from typing import Dict, Any +from pathlib import Path from .installation import InstallationService from .dll_detection import DllDetectionService @@ -106,6 +111,200 @@ class Plugin: enable_lsfg, multiplier, flow_scale, hdr, perf_mode, immediate_mode, disable_vkbasalt, frame_cap ) + # Self-updater methods + async def check_for_plugin_update(self) -> Dict[str, Any]: + """Check for plugin updates by comparing current version with latest GitHub release + + Returns: + Dict containing update information: + { + "update_available": bool, + "current_version": str, + "latest_version": str, + "release_notes": str, + "release_date": str, + "download_url": str, + "error": str (if error occurred) + } + """ + try: + import decky + + # Read current version from package.json + package_json_path = Path(decky.DECKY_PLUGIN_DIR) / "package.json" + current_version = "0.0.0" + + if package_json_path.exists(): + try: + with open(package_json_path, 'r', encoding='utf-8') as f: + package_data = json.load(f) + current_version = package_data.get('version', '0.0.0') + except Exception as e: + decky.logger.warning(f"Failed to read package.json: {e}") + + # Fetch latest release from GitHub + api_url = "https://api.github.com/repos/xXJSONDeruloXx/decky-lossless-scaling-vk/releases/latest" + + try: + # Create SSL context that doesn't verify certificates + # This is needed on Steam Deck where certificate verification often fails + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Use urllib to fetch the latest release info + with urllib.request.urlopen(api_url, context=ssl_context) as response: + release_data = json.loads(response.read().decode('utf-8')) + + latest_version = release_data.get('tag_name', '').lstrip('v') + release_notes = release_data.get('body', '') + release_date = release_data.get('published_at', '') + + # Find the plugin zip download URL + download_url = "" + for asset in release_data.get('assets', []): + if asset.get('name', '').endswith('.zip'): + download_url = asset.get('browser_download_url', '') + break + + # Compare versions + update_available = self._compare_versions(current_version, latest_version) + + return { + "success": True, + "update_available": update_available, + "current_version": current_version, + "latest_version": latest_version, + "release_notes": release_notes, + "release_date": release_date, + "download_url": download_url + } + + except Exception as e: + decky.logger.error(f"Failed to fetch release info: {e}") + return { + "success": False, + "error": f"Failed to check for updates: {str(e)}" + } + + except Exception as e: + return { + "success": False, + "error": f"Update check failed: {str(e)}" + } + + async def download_plugin_update(self, download_url: str) -> Dict[str, Any]: + """Download the plugin update zip file to ~/Downloads + + Args: + download_url: URL to download the plugin zip from + + Returns: + Dict containing download result: + { + "success": bool, + "download_path": str, + "error": str (if error occurred) + } + """ + try: + import decky + + # Create download path + downloads_dir = Path.home() / "Downloads" + downloads_dir.mkdir(exist_ok=True) + download_path = downloads_dir / "decky-lossless-scaling-vk.zip" + + # Remove existing file if it exists + if download_path.exists(): + download_path.unlink() + + # Download the file + decky.logger.info(f"Downloading plugin update from {download_url}") + + try: + # Create SSL context that doesn't verify certificates + # This is needed on Steam Deck where certificate verification often fails + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Use urllib to download the file with SSL context + with urllib.request.urlopen(download_url, context=ssl_context) as response: + with open(download_path, 'wb') as f: + f.write(response.read()) + + # Verify the file was downloaded successfully + if download_path.exists() and download_path.stat().st_size > 0: + decky.logger.info(f"Plugin update downloaded successfully to {download_path}") + return { + "success": True, + "download_path": str(download_path) + } + else: + return { + "success": False, + "error": "Download completed but file is empty or missing" + } + + except Exception as e: + decky.logger.error(f"Download failed: {e}") + return { + "success": False, + "error": f"Download failed: {str(e)}" + } + + except Exception as e: + return { + "success": False, + "error": f"Download preparation failed: {str(e)}" + } + + def _compare_versions(self, current: str, latest: str) -> bool: + """Compare two version strings to determine if an update is available + + Args: + current: Current version string (e.g., "1.2.3") + latest: Latest version string (e.g., "1.2.4") + + Returns: + True if latest version is newer than current version + """ + try: + # Remove 'v' prefix if present and split by dots + current_parts = current.lstrip('v').split('.') + latest_parts = latest.lstrip('v').split('.') + + # Pad with zeros if needed to ensure equal length + max_len = max(len(current_parts), len(latest_parts)) + current_parts.extend(['0'] * (max_len - len(current_parts))) + latest_parts.extend(['0'] * (max_len - len(latest_parts))) + + # Compare each part numerically + for i in range(max_len): + try: + current_num = int(current_parts[i]) + latest_num = int(latest_parts[i]) + + if latest_num > current_num: + return True + elif latest_num < current_num: + return False + # If equal, continue to next part + except ValueError: + # If conversion fails, do string comparison + if latest_parts[i] > current_parts[i]: + return True + elif latest_parts[i] < current_parts[i]: + return False + + # All parts are equal + return False + + except Exception: + # If comparison fails, assume no update available + return False + # Plugin lifecycle methods async def _main(self): """ diff --git a/src/api/lsfgApi.ts b/src/api/lsfgApi.ts index 2e7964c..f7363c1 100644 --- a/src/api/lsfgApi.ts +++ b/src/api/lsfgApi.ts @@ -49,6 +49,23 @@ export interface ConfigSchemaResult { defaults: ConfigurationData; } +export interface UpdateCheckResult { + success: boolean; + update_available: boolean; + current_version: string; + latest_version: string; + release_notes: string; + release_date: string; + download_url: string; + error?: string; +} + +export interface UpdateDownloadResult { + success: boolean; + download_path?: string; + error?: string; +} + // API functions export const installLsfgVk = callable<[], InstallationResult>("install_lsfg_vk"); export const uninstallLsfgVk = callable<[], InstallationResult>("uninstall_lsfg_vk"); @@ -68,3 +85,7 @@ export const updateLsfgConfigFromObject = async (config: ConfigurationData): Pro const args = ConfigurationManager.createArgsFromConfig(config); return updateLsfgConfig(...args as [boolean, number, number, boolean, boolean, boolean, boolean, number]); }; + +// Self-updater API functions +export const checkForPluginUpdate = callable<[], UpdateCheckResult>("check_for_plugin_update"); +export const downloadPluginUpdate = callable<[string], UpdateDownloadResult>("download_plugin_update"); diff --git a/src/components/Content.tsx b/src/components/Content.tsx index ba651d4..ea3f3c1 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -8,6 +8,7 @@ import { ConfigurationSection } from "./ConfigurationSection"; import { UsageInstructions } from "./UsageInstructions"; import { WikiButton } from "./WikiButton"; import { ClipboardButton } from "./ClipboardButton"; +import { PluginUpdateChecker } from "./PluginUpdateChecker"; import { ConfigurationData } from "../config/configSchema"; export function Content() { @@ -77,6 +78,9 @@ export function Content() { <WikiButton /> <ClipboardButton /> + + {/* Plugin Update Checker */} + <PluginUpdateChecker /> </PanelSection> ); } diff --git a/src/components/PluginUpdateChecker.tsx b/src/components/PluginUpdateChecker.tsx new file mode 100644 index 0000000..0028a79 --- /dev/null +++ b/src/components/PluginUpdateChecker.tsx @@ -0,0 +1,187 @@ +import React, { useState, useEffect } from 'react'; +import { + ButtonItem, + PanelSection +} from '@decky/ui'; +import { checkForPluginUpdate, downloadPluginUpdate, UpdateCheckResult, UpdateDownloadResult } from '../api/lsfgApi'; + +interface PluginUpdateCheckerProps { + // Add any props if needed +} + +interface UpdateInfo { + updateAvailable: boolean; + currentVersion: string; + latestVersion: string; + releaseNotes: string; + releaseDate: string; + downloadUrl: string; +} + +export const PluginUpdateChecker: React.FC<PluginUpdateCheckerProps> = () => { + const [checkingUpdate, setCheckingUpdate] = useState(false); + const [downloadingUpdate, setDownloadingUpdate] = useState(false); + const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null); + const [updateError, setUpdateError] = useState<string | null>(null); + const [downloadResult, setDownloadResult] = useState<UpdateDownloadResult | null>(null); + + // Auto-hide error messages after 5 seconds + useEffect(() => { + if (updateError) { + const timer = setTimeout(() => { + setUpdateError(null); + }, 5000); + return () => clearTimeout(timer); + } + return undefined; + }, [updateError]); + + const handleCheckForUpdate = async () => { + setCheckingUpdate(true); + setUpdateError(null); + setUpdateInfo(null); + setDownloadResult(null); // Clear previous download result + + try { + const result: UpdateCheckResult = await checkForPluginUpdate(); + + if (result.success) { + setUpdateInfo({ + updateAvailable: result.update_available, + currentVersion: result.current_version, + latestVersion: result.latest_version, + releaseNotes: result.release_notes, + releaseDate: result.release_date, + downloadUrl: result.download_url + }); + + // Simple console log instead of toast since showToast may not be available + if (result.update_available) { + console.log("Update available!", `Version ${result.latest_version} is now available.`); + } else { + console.log("Up to date!", "You have the latest version installed."); + } + } else { + setUpdateError(result.error || "Failed to check for updates"); + } + } catch (error) { + setUpdateError(`Error checking for updates: ${error}`); + } finally { + setCheckingUpdate(false); + } + }; + + const handleDownloadUpdate = async () => { + if (!updateInfo?.downloadUrl) return; + + setDownloadingUpdate(true); + setUpdateError(null); + setDownloadResult(null); + + try { + const result: UpdateDownloadResult = await downloadPluginUpdate(updateInfo.downloadUrl); + + if (result.success) { + setDownloadResult(result); + console.log("✓ Download complete!", `Plugin downloaded to ${result.download_path}`); + } else { + setUpdateError(result.error || "Failed to download update"); + } + } catch (error) { + setUpdateError(`Error downloading update: ${error}`); + } finally { + setDownloadingUpdate(false); + } + }; + + const getStatusMessage = () => { + if (!updateInfo) return null; + + if (updateInfo.updateAvailable) { + if (downloadResult?.success) { + return ( + <div style={{ color: 'lightgreen', marginTop: '5px' }}> + ✓ v{updateInfo.latestVersion} downloaded - ready to install + </div> + ); + } else { + return ( + <div style={{ color: 'orange', marginTop: '5px' }}> + Update available: v{updateInfo.latestVersion} + </div> + ); + } + } else { + return ( + <div style={{ color: 'lightgreen', marginTop: '5px' }}> + Up to date (v{updateInfo.currentVersion}) + </div> + ); + } + }; + + return ( + <PanelSection title="Plugin Updates"> + <ButtonItem + layout="below" + onClick={handleCheckForUpdate} + disabled={checkingUpdate} + description={getStatusMessage()} + > + {checkingUpdate ? 'Checking for updates...' : 'Check for Updates'} + </ButtonItem> + + {updateInfo && updateInfo.updateAvailable && !downloadResult?.success && ( + <ButtonItem + layout="below" + onClick={handleDownloadUpdate} + disabled={downloadingUpdate} + description={`Download version ${updateInfo.latestVersion}`} + > + {downloadingUpdate ? 'Downloading...' : 'Download Update'} + </ButtonItem> + )} + + {downloadResult?.success && ( + <div style={{ + marginTop: '10px', + padding: '10px', + backgroundColor: 'rgba(0, 255, 0, 0.1)', + borderRadius: '4px', + border: '1px solid rgba(0, 255, 0, 0.3)' + }}> + <div style={{ color: 'lightgreen', fontWeight: 'bold', marginBottom: '5px' }}> + ✓ Download Complete! + </div> + <div style={{ fontSize: '12px', marginBottom: '10px' }}> + File saved to: {downloadResult.download_path} + </div> + <div style={{ fontSize: '12px' }}> + <strong>Installation Instructions:</strong> + <ol style={{ paddingLeft: '20px', marginTop: '5px' }}> + <li>Go to Decky Loader settings</li> + <li>Click "Developer" tab</li> + <li>Click "Uninstall" next to "Lossless Scaling"</li> + <li>Click "Install from ZIP"</li> + <li>Select the downloaded file</li> + <li>Restart Steam or reload plugins</li> + </ol> + </div> + </div> + )} + + {updateError && ( + <div style={{ + color: 'red', + marginTop: '10px', + padding: '8px', + backgroundColor: 'rgba(255, 0, 0, 0.1)', + borderRadius: '4px', + fontSize: '12px' + }}> + {updateError} + </div> + )} + </PanelSection> + ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 7304ca9..d26159d 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,3 +5,4 @@ export { ConfigurationSection } from "./ConfigurationSection"; export { UsageInstructions } from "./UsageInstructions"; export { WikiButton } from "./WikiButton"; export { ClipboardButton } from "./ClipboardButton"; +export { PluginUpdateChecker } from "./PluginUpdateChecker"; |
