From ef214d424c139313fbacf421e3416ae5a9648912 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Wed, 4 Nov 2026 14:50:03 +0100 Subject: [PATCH] feat(themes): add revert for unsynced themes Restore the published Workshop version after confirmation, keeping a backup of local edits. Owner reverts send auth headers so themes-service skips the download counter. --- src-tauri/src/commands.rs | 19 ++-- src/lib/theme/communityThemes.ts | 20 +++- src/screens/Launch.svelte | 173 +++++++++++++++++++++++++------ 3 files changed, 169 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1cbcda3..051ca19 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -3,7 +3,7 @@ use hardware_id::get_id; use reqwest::Client; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::io::Read; use std::path::{Path, PathBuf}; #[cfg(not(target_os = "macos"))] @@ -1688,14 +1688,19 @@ pub async fn download_and_extract_community_theme( download_url: String, theme_folder: String, theme_name: String, + request_headers: Option>, ) -> Result<(), String> { let client = reqwest::Client::new(); - let response = client - .get(&download_url) - .header( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", - ) + let mut request = client.get(&download_url).header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36", + ); + if let Some(headers) = request_headers { + for (name, value) in headers { + request = request.header(name.as_str(), value.as_str()); + } + } + let response = request .send() .await .map_err(|e| format!("Failed to download theme: {}", e))?; diff --git a/src/lib/theme/communityThemes.ts b/src/lib/theme/communityThemes.ts index 413b5b4..2102f1e 100644 --- a/src/lib/theme/communityThemes.ts +++ b/src/lib/theme/communityThemes.ts @@ -953,12 +953,27 @@ export async function reviewCommunityTheme( export async function downloadAndInstallCommunityTheme( theme: CommunityTheme, onProgress?: (status: string, percentage: number) => void, - options?: { folderName?: string } + options?: { folderName?: string; skipDownloadCount?: boolean } ): Promise<{ success: boolean; folderName: string }> { const { onProgress: reportProgress = () => {} } = { onProgress }; const baseThemesDir = await themesDirectory(); const folderName = options?.folderName?.trim() || theme.id; const targetFolder = await path.join(baseThemesDir, folderName); + const skipDownloadCount = options?.skipDownloadCount === true; + const downloadUrl = skipDownloadCount + ? (() => { + const url = new URL(theme.downloadUrl); + url.searchParams.set('skipDownloadCount', '1'); + return url.toString(); + })() + : theme.downloadUrl; + let requestHeaders: Record | undefined; + if (skipDownloadCount) { + requestHeaders = await getAuthHeaders(); + if (!requestHeaders['X-Username'] || !requestHeaders['X-Password']) { + throw new Error('Please log in again before reverting this theme.'); + } + } reportProgress('Downloading theme package...', 10); @@ -987,9 +1002,10 @@ export async function downloadAndInstallCommunityTheme( try { try { await invoke('download_and_extract_community_theme', { - downloadUrl: theme.downloadUrl, + downloadUrl, themeFolder: targetFolder, themeName: theme.name, + ...(requestHeaders ? { requestHeaders } : {}), }); } catch (err) { // Fallback for legacy themes directly to git repo diff --git a/src/screens/Launch.svelte b/src/screens/Launch.svelte index a3500bb..865260c 100644 --- a/src/screens/Launch.svelte +++ b/src/screens/Launch.svelte @@ -315,6 +315,7 @@ let restoringTheme = $state(false); let uninstallCandidate = $state(undefined); let uninstallingTheme = $state(false); + let revertCandidate = $state(undefined); let installedReportOpen = $state(false); let installedReportTarget = $state(null); let installedReportLoading = $state(false); @@ -768,8 +769,8 @@ } }; - const updateInstalledTheme = async (theme: Theme) => { - if (theme.status !== 'installed' || updatingThemeFolder) return; + const updateInstalledTheme = async (theme: Theme, mode: 'update' | 'revert' = 'update') => { + if (theme.status !== 'installed' || updatingThemeFolder) return false; const wasActive = $active_custom_theme?.folder_name === theme.folder_name; updatingThemeFolder = theme.folder_name; @@ -785,12 +786,19 @@ try { backup = await invoke('backup_theme_folder', { themeFolder }); } catch (error) { + if (mode === 'revert') { + throw new Error( + 'Could not back up the current theme before reverting. Your local changes were left untouched.', + { cause: error } + ); + } console.warn('Could not back up the theme before updating it', error); } try { await downloadAndInstallCommunityTheme(target, () => {}, { folderName: theme.folder_name, + skipDownloadCount: mode === 'revert', }); } catch (error) { if (backup) { @@ -820,21 +828,40 @@ themeContentHashCache.delete(theme.folder_name); communityThemeCache.delete(theme.folder_name); + await refreshThemeSyncState(theme.folder_name); sileo.success({ - title: 'Yaay!', - description: 'Theme updated successfully!', + title: mode === 'revert' ? 'Theme reverted' : 'Yaay!', + description: + mode === 'revert' + ? `"${theme.name}" was restored to the published Workshop version.` + : 'Theme updated successfully!', }); + return true; } catch (error) { console.error(error); sileo.error({ - title: 'Could not update theme', + title: mode === 'revert' ? 'Could not revert theme' : 'Could not update theme', description: error instanceof Error ? error.message : String(error), }); + return false; } finally { updatingThemeFolder = undefined; } }; + const requestThemeRevert = (theme: Theme) => { + if (theme.status !== 'installed' || !theme.workshopId || updatingThemeFolder) return; + revertCandidate = theme; + }; + + const confirmThemeRevert = async () => { + const theme = revertCandidate; + if (!theme) return; + if (await updateInstalledTheme(theme, 'revert')) { + revertCandidate = undefined; + } + }; + $effect(() => { const userId = $currentUser?.id; const themeCount = $custom_themes.length; @@ -2427,29 +2454,40 @@ - - -
- - Uninstall theme? -
-
- Remove {uninstallCandidate?.name} from your library? This - deletes the local theme files and any unpublished changes. If the theme is published to the Workshop, - that copy is not affected. -
-
+ { + if (!open && !uninstallingTheme) uninstallCandidate = undefined; + }} +> + + + + + Uninstall theme? + + + Remove {uninstallCandidate?.name} from your library? + This deletes the local theme files from this computer. + + + + {#if uninstallCandidate && uninstallCandidate.workshopId && isOwnInstalledTheme(uninstallCandidate)} +

+ + + Your published Workshop copy stays online. Only this local copy and any unpublished + changes are removed. + +

+ {/if} + + -
+ +
+
+ + { + if (!open && updatingThemeFolder !== revertCandidate?.folder_name) revertCandidate = undefined; + }} +> + + + + + Revert unsynced changes? + + + Replace the local files for {revertCandidate?.name} with the published Workshop version? Any unpublished changes will be removed. A backup of the + current local copy will be created first. + + + + + + @@ -4343,6 +4429,10 @@ workshopVersion !== localVersion} {@const isNotSynced = isOwnTheme && (theme.isNotSynced === true || versionMismatch)} + {@const canRevertTheme = + isNotSynced && + theme.status === 'installed' && + Boolean(theme.workshopId)} {@const canReportTheme = Boolean($currentUser && ownedWorkshopLoaded) && !isOwnTheme && @@ -4357,6 +4447,7 @@ canEditTheme || canRestoreTheme || canUpdateTheme || + canRevertTheme || canReportTheme || canUninstallTheme}
{/if} - {#if canUpdateTheme} + {#if canUpdateTheme && !canRevertTheme} {/if} + {#if canRevertTheme} + requestThemeRevert(theme)} + > + + Revert unsynced changes + + {/if} {#if canReportTheme} {/if} {#if canUninstallTheme} - {#if canEditTheme || canRestoreTheme || canUpdateTheme || canReportTheme} + {#if canEditTheme || canRestoreTheme || canUpdateTheme || canRevertTheme || canReportTheme} {/if}