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}