From 0025313c4d8e21851b8e2ba2eeae5c0e2f7375a9 Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Tue, 3 Nov 2026 18:48:52 +0100 Subject: [PATCH] fix(workshop): allow concurrent downloads and stop auto-applying Downloads shared a single active-id guard, so a second theme could not start until the first finished, and completing one immediately applied it over the current theme. Each download now tracks its own progress, runs concurrently, and only adds the theme to the library. The batch-selection bar and checkboxes are removed in favour of the independent buttons. --- src/lib/communityThemes.ts | 32 +- .../theme/CommunityWorkshopView.svelte | 342 +++--------------- 2 files changed, 73 insertions(+), 301 deletions(-) diff --git a/src/lib/communityThemes.ts b/src/lib/communityThemes.ts index 9facaea..9e57b3c 100644 --- a/src/lib/communityThemes.ts +++ b/src/lib/communityThemes.ts @@ -863,19 +863,27 @@ export async function downloadAndInstallCommunityTheme( reportProgress('Downloading theme package...', 10); - const unlistenDownload = await listen<{ received: number; total: number; progress: number }>( - 'download_progress', - (event) => { - reportProgress('Downloading theme package...', Math.round(10 + event.payload.progress * 50)); - } - ); + // Progress events are global, so ignore those belonging to other themes that are + // downloading at the same time. + const unlistenDownload = await listen<{ + theme_name: string; + received: number; + total: number; + progress: number; + }>('download_progress', (event) => { + if (event.payload.theme_name !== theme.name) return; + reportProgress('Downloading theme package...', Math.round(10 + event.payload.progress * 50)); + }); - const unlistenExtract = await listen<{ extracted: number; total: number; progress: number }>( - 'extract_progress', - (event) => { - reportProgress('Extracting theme files...', Math.round(60 + event.payload.progress * 38)); - } - ); + const unlistenExtract = await listen<{ + theme_name: string; + extracted: number; + total: number; + progress: number; + }>('extract_progress', (event) => { + if (event.payload.theme_name !== theme.name) return; + reportProgress('Extracting theme files...', Math.round(60 + event.payload.progress * 38)); + }); try { try { diff --git a/src/lib/components/theme/CommunityWorkshopView.svelte b/src/lib/components/theme/CommunityWorkshopView.svelte index ff27266..d58b12f 100644 --- a/src/lib/components/theme/CommunityWorkshopView.svelte +++ b/src/lib/components/theme/CommunityWorkshopView.svelte @@ -82,7 +82,7 @@ } from '@lucide/svelte'; import { sileo } from 'sileo'; import { onMount } from 'svelte'; - import { SvelteMap } from 'svelte/reactivity'; + import { SvelteMap, SvelteSet } from 'svelte/reactivity'; let { onApplyTheme, @@ -117,16 +117,8 @@ let error = $state(null); let latestRequestId = 0; - let downloadingId = $state(null); - let downloadProgress = $state(0); - let selectionMode = $state(false); - let selectedThemeIds = $state([]); - let bulkDownload = $state<{ - current: number; - total: number; - name: string; - progress: number; - } | null>(null); + let downloadingIds = new SvelteSet(); + let downloadProgressById = new SvelteMap(); let forkingId = $state(null); let deletingId = $state(null); let moderatingId = $state(null); @@ -243,15 +235,6 @@ return map; }); - function findInstalledTheme(theme: CommunityTheme): Theme | undefined { - return localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase()); - } - - const selectedThemes = $derived(themes.filter((theme) => selectedThemeIds.includes(theme.id))); - const downloadableSelectedThemes = $derived( - selectedThemes.filter((theme) => !theme.deletedAt && !findInstalledTheme(theme)) - ); - const tagOptions = $derived.by(() => { const counts = new SvelteMap(); @@ -332,7 +315,6 @@ themes = [...themes, ...res.items.filter((theme) => !knownIds.has(theme.id))]; } else { themes = res.items; - selectedThemeIds = []; } totalThemes = res.total; page = res.page; @@ -372,8 +354,6 @@ selectedView = view; selectedTag = 'all'; showHiddenOnly = false; - selectionMode = false; - selectedThemeIds = []; void loadWorkshopThemes(); } @@ -425,34 +405,21 @@ } } - async function handleDownloadAndApply(theme: CommunityTheme) { - if (downloadingId || bulkDownload || theme.deletedAt) return; + async function handleDownloadTheme(theme: CommunityTheme) { + if (theme.deletedAt || downloadingIds.has(theme.id)) return; - downloadingId = theme.id; - downloadProgress = 10; + downloadingIds.add(theme.id); + downloadProgressById.set(theme.id, 10); try { - const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => { - downloadProgress = progress; + await downloadAndInstallCommunityTheme(theme, (_status, progress) => { + downloadProgressById.set(theme.id, progress); }); sileo.success({ - title: 'Theme Installed', - description: `"${theme.name}" has been installed to your library.`, + title: 'Theme downloaded', + description: `"${theme.name}" has been added to your library.`, }); - - // Find locally installed theme object - const installed = $custom_themes.find( - (t) => t.folder_name.toLowerCase() === res.folderName.toLowerCase() - ); - - if (installed) { - if (!isThemeTrusted(installed)) { - onRequestTrust(installed); - } else { - await onApplyTheme(installed); - } - } } catch (err) { console.error(err); sileo.error({ @@ -460,113 +427,17 @@ description: err instanceof Error ? err.message : String(err), }); } finally { - downloadingId = null; - downloadProgress = 0; + downloadingIds.delete(theme.id); + downloadProgressById.delete(theme.id); } } - function toggleSelectionMode() { - if (bulkDownload) return; - selectionMode = !selectionMode; - if (!selectionMode) selectedThemeIds = []; - } - - function toggleThemeSelection(themeId: string) { - selectedThemeIds = selectedThemeIds.includes(themeId) - ? selectedThemeIds.filter((id) => id !== themeId) - : [...selectedThemeIds, themeId]; - } - - function clearThemeSelection() { - selectedThemeIds = []; - } - - function selectAllDownloadableThemes() { - selectedThemeIds = themes - .filter((theme) => !theme.deletedAt && !findInstalledTheme(theme)) - .map((theme) => theme.id); - } - - async function handleBulkDownload() { - if (bulkDownload || downloadingId) return; - - const selected = themes.filter( - (theme) => selectedThemeIds.includes(theme.id) && !theme.deletedAt - ); - const installable = selected.filter((theme) => !findInstalledTheme(theme)); - const skipped = selected.length - installable.length; - - if (installable.length === 0) { - sileo.info({ - title: 'Nothing to download', - description: - selected.length === 0 - ? 'Select at least one theme first.' - : 'The selected themes are already installed.', - }); - return; - } - - // Installs run one after another because the download/extract progress events are global, - // so concurrent installs would report each other's progress. - const failures: string[] = []; - let installed = 0; - - try { - for (let index = 0; index < installable.length; index += 1) { - const theme = installable[index]; - bulkDownload = { - current: index + 1, - total: installable.length, - name: theme.name, - progress: 5, - }; - - try { - await downloadAndInstallCommunityTheme(theme, (_status, progress) => { - if (!bulkDownload) return; - bulkDownload = { ...bulkDownload, progress }; - }); - installed += 1; - } catch (err) { - console.error(`[Workshop] Could not download "${theme.name}":`, err); - failures.push(theme.name); - } - } - } finally { - bulkDownload = null; - } - - if (installed > 0) { - sileo.success({ - title: installed === 1 ? 'Theme installed' : `${installed} themes installed`, - description: - skipped > 0 - ? `${skipped} already-installed theme${skipped === 1 ? ' was' : 's were'} skipped.` - : 'Open the library to apply them.', - }); - } - - if (failures.length > 0) { - sileo.error({ - title: - failures.length === 1 - ? 'Could not install 1 theme' - : `Could not install ${failures.length} themes`, - description: failures.join(', '), - }); - } - - selectedThemeIds = []; - selectionMode = false; - } - async function handleApplyExisting(theme: CommunityTheme) { if (theme.deletedAt) return; const installed = localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase()); if (!installed) { - await handleDownloadAndApply(theme); + await handleDownloadTheme(theme); return; } @@ -578,18 +449,18 @@ } async function handleEditTheme(theme: CommunityTheme) { - if (theme.deletedAt || downloadingId || bulkDownload || forkingId || deletingId || moderatingId) + if (theme.deletedAt || downloadingIds.has(theme.id) || forkingId || deletingId || moderatingId) return; let installed = localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase()); if (!installed) { - downloadingId = theme.id; - downloadProgress = 10; + downloadingIds.add(theme.id); + downloadProgressById.set(theme.id, 10); try { - await downloadAndInstallCommunityTheme(theme, (msg, progress) => { - downloadProgress = progress; + await downloadAndInstallCommunityTheme(theme, (_status, progress) => { + downloadProgressById.set(theme.id, progress); }); installed = $custom_themes.find( (candidate) => candidate.folder_name.toLowerCase() === theme.id.toLowerCase() @@ -601,8 +472,8 @@ }); return; } finally { - downloadingId = null; - downloadProgress = 0; + downloadingIds.delete(theme.id); + downloadProgressById.delete(theme.id); } } @@ -612,7 +483,7 @@ } async function handleRefreshOwnedTheme(theme: CommunityTheme) { - if (theme.deletedAt || downloadingId || bulkDownload || forkingId || deletingId || moderatingId) + if (theme.deletedAt || downloadingIds.has(theme.id) || forkingId || deletingId || moderatingId) return; const installed = @@ -631,11 +502,11 @@ ); if (!confirmed) return; - downloadingId = theme.id; - downloadProgress = 10; + downloadingIds.add(theme.id); + downloadProgressById.set(theme.id, 10); try { - const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => { - downloadProgress = progress; + const res = await downloadAndInstallCommunityTheme(theme, (_status, progress) => { + downloadProgressById.set(theme.id, progress); }); const refreshed = $custom_themes.find( (candidate) => candidate.folder_name.toLowerCase() === res.folderName.toLowerCase() @@ -654,13 +525,13 @@ description: err instanceof Error ? err.message : String(err), }); } finally { - downloadingId = null; - downloadProgress = 0; + downloadingIds.delete(theme.id); + downloadProgressById.delete(theme.id); } } async function handleToggleVisibility(theme: CommunityTheme) { - if (!canModerate || moderatingId || downloadingId || forkingId || deletingId) return; + if (!canModerate || moderatingId || forkingId || deletingId) return; const hiding = !theme.isHidden; let reason: string | undefined; @@ -822,7 +693,6 @@ theme.deletedAt || theme.isHidden || theme.allowForking === false || - downloadingId || forkingId || deletingId || moderatingId @@ -853,10 +723,10 @@ let installed: Theme | undefined; try { await loadWorkshopThemes(); - downloadingId = fork.id; - downloadProgress = 10; - const res = await downloadAndInstallCommunityTheme(fork, (msg, progress) => { - downloadProgress = progress; + downloadingIds.add(fork.id); + downloadProgressById.set(fork.id, 10); + const res = await downloadAndInstallCommunityTheme(fork, (_status, progress) => { + downloadProgressById.set(fork.id, progress); }); installed = $custom_themes.find( (candidate) => candidate.folder_name.toLowerCase() === res.folderName.toLowerCase() @@ -867,8 +737,8 @@ description: `${err instanceof Error ? err.message : String(err)} You can download it from the workshop.`, }); } finally { - downloadingId = null; - downloadProgress = 0; + downloadingIds.delete(fork.id); + downloadProgressById.delete(fork.id); } if (installed) { @@ -881,7 +751,7 @@ } async function handleDeleteTheme(theme: CommunityTheme) { - if (theme.deletedAt || deletingId || downloadingId || forkingId || moderatingId) return; + if (theme.deletedAt || deletingId || forkingId || moderatingId) return; const confirmed = window.confirm( `Move "${theme.name}" to the trash?\n\nYou can restore it from the Trash view later. Installed copies stay on your computer.` ); @@ -907,16 +777,14 @@ } function openRestoreDialog(theme: CommunityTheme) { - if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId) - return; + if (restoringId || purgingId || forkingId || deletingId || moderatingId) return; purgeTarget = null; trashActionError = null; restoreTarget = theme; } function openPermanentDeleteDialog(theme: CommunityTheme) { - if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId) - return; + if (restoringId || purgingId || forkingId || deletingId || moderatingId) return; restoreTarget = null; trashActionError = null; purgeTarget = theme; @@ -1604,28 +1472,6 @@ {/if} - {#if selectedView === 'workshop'} - - {#snippet trigger(props)} - - {/snippet} - - {/if} - {#if $currentUser} {#snippet trigger(props)} @@ -1663,76 +1509,6 @@ - {#if selectionMode && selectedView === 'workshop'} -
-
- {selectedThemes.length} selected - {#if selectedThemes.length > downloadableSelectedThemes.length} - - {selectedThemes.length - downloadableSelectedThemes.length} already installed - - {/if} - - -
- -
- {#if bulkDownload} -
- - - {bulkDownload.current}/{bulkDownload.total} ยท {bulkDownload.name} - -
-
-
- {bulkDownload.progress}% -
- {/if} - - -
-
- {/if} -
@@ -1777,8 +1553,8 @@ {@const isActive = Boolean( localTheme && $active_custom_theme?.folder_name === localTheme.folder_name )} - {@const isDownloading = downloadingId === theme.id} - {@const isSelected = selectionMode && selectedThemeIds.includes(theme.id)} + {@const isDownloading = downloadingIds.has(theme.id)} + {@const downloadPercent = downloadProgressById.get(theme.id) ?? 0} {@const isDeleted = Boolean(theme.deletedAt)} {@const hasUpdate = Boolean( !isDeleted && localTheme && localTheme.version !== theme.version @@ -1792,11 +1568,9 @@ {@const showOverflowMenu = !isDeleted && (owned || canModerate || canReport)}
@@ -1970,21 +1744,11 @@
-
- {#if selectionMode && !isDeleted} - toggleThemeSelection(theme.id)} - aria-label={`Select ${theme.name} for download`} - /> - {/if} -

- {theme.name} -

-
+

+ {theme.name} +

{#if isDeleted} @@ -2122,12 +1886,12 @@ {#if isDownloading}
{downloadProgress}%{downloadPercent}%
@@ -2204,7 +1968,7 @@ {/if}