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.
This commit is contained in:
1 parent
6b1ed5aa1a
commit
0025313c4d
2 files changed
+73
-301
No files matched your search
+20
-12
@@ -863,19 +863,27 @@ export async function downloadAndInstallCommunityTheme(
|
|||||||
|
|
||||||
reportProgress('Downloading theme package...', 10);
|
reportProgress('Downloading theme package...', 10);
|
||||||
|
|
||||||
const unlistenDownload = await listen<{ received: number; total: number; progress: number }>(
|
// Progress events are global, so ignore those belonging to other themes that are
|
||||||
'download_progress',
|
// downloading at the same time.
|
||||||
(event) => {
|
const unlistenDownload = await listen<{
|
||||||
reportProgress('Downloading theme package...', Math.round(10 + event.payload.progress * 50));
|
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 }>(
|
const unlistenExtract = await listen<{
|
||||||
'extract_progress',
|
theme_name: string;
|
||||||
(event) => {
|
extracted: number;
|
||||||
reportProgress('Extracting theme files...', Math.round(60 + event.payload.progress * 38));
|
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 {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { sileo } from 'sileo';
|
import { sileo } from 'sileo';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
onApplyTheme,
|
onApplyTheme,
|
||||||
@@ -117,16 +117,8 @@
|
|||||||
let error = $state<string | null>(null);
|
let error = $state<string | null>(null);
|
||||||
let latestRequestId = 0;
|
let latestRequestId = 0;
|
||||||
|
|
||||||
let downloadingId = $state<string | null>(null);
|
let downloadingIds = new SvelteSet<string>();
|
||||||
let downloadProgress = $state(0);
|
let downloadProgressById = new SvelteMap<string, number>();
|
||||||
let selectionMode = $state(false);
|
|
||||||
let selectedThemeIds = $state<string[]>([]);
|
|
||||||
let bulkDownload = $state<{
|
|
||||||
current: number;
|
|
||||||
total: number;
|
|
||||||
name: string;
|
|
||||||
progress: number;
|
|
||||||
} | null>(null);
|
|
||||||
let forkingId = $state<string | null>(null);
|
let forkingId = $state<string | null>(null);
|
||||||
let deletingId = $state<string | null>(null);
|
let deletingId = $state<string | null>(null);
|
||||||
let moderatingId = $state<string | null>(null);
|
let moderatingId = $state<string | null>(null);
|
||||||
@@ -243,15 +235,6 @@
|
|||||||
return map;
|
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 tagOptions = $derived.by(() => {
|
||||||
const counts = new SvelteMap<string, number>();
|
const counts = new SvelteMap<string, number>();
|
||||||
|
|
||||||
@@ -332,7 +315,6 @@
|
|||||||
themes = [...themes, ...res.items.filter((theme) => !knownIds.has(theme.id))];
|
themes = [...themes, ...res.items.filter((theme) => !knownIds.has(theme.id))];
|
||||||
} else {
|
} else {
|
||||||
themes = res.items;
|
themes = res.items;
|
||||||
selectedThemeIds = [];
|
|
||||||
}
|
}
|
||||||
totalThemes = res.total;
|
totalThemes = res.total;
|
||||||
page = res.page;
|
page = res.page;
|
||||||
@@ -372,8 +354,6 @@
|
|||||||
selectedView = view;
|
selectedView = view;
|
||||||
selectedTag = 'all';
|
selectedTag = 'all';
|
||||||
showHiddenOnly = false;
|
showHiddenOnly = false;
|
||||||
selectionMode = false;
|
|
||||||
selectedThemeIds = [];
|
|
||||||
void loadWorkshopThemes();
|
void loadWorkshopThemes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,34 +405,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDownloadAndApply(theme: CommunityTheme) {
|
async function handleDownloadTheme(theme: CommunityTheme) {
|
||||||
if (downloadingId || bulkDownload || theme.deletedAt) return;
|
if (theme.deletedAt || downloadingIds.has(theme.id)) return;
|
||||||
|
|
||||||
downloadingId = theme.id;
|
downloadingIds.add(theme.id);
|
||||||
downloadProgress = 10;
|
downloadProgressById.set(theme.id, 10);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
await downloadAndInstallCommunityTheme(theme, (_status, progress) => {
|
||||||
downloadProgress = progress;
|
downloadProgressById.set(theme.id, progress);
|
||||||
});
|
});
|
||||||
|
|
||||||
sileo.success({
|
sileo.success({
|
||||||
title: 'Theme Installed',
|
title: 'Theme downloaded',
|
||||||
description: `"${theme.name}" has been installed to your library.`,
|
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) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
sileo.error({
|
sileo.error({
|
||||||
@@ -460,113 +427,17 @@
|
|||||||
description: err instanceof Error ? err.message : String(err),
|
description: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingIds.delete(theme.id);
|
||||||
downloadProgress = 0;
|
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) {
|
async function handleApplyExisting(theme: CommunityTheme) {
|
||||||
if (theme.deletedAt) return;
|
if (theme.deletedAt) return;
|
||||||
const installed =
|
const installed =
|
||||||
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
await handleDownloadAndApply(theme);
|
await handleDownloadTheme(theme);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,18 +449,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleEditTheme(theme: CommunityTheme) {
|
async function handleEditTheme(theme: CommunityTheme) {
|
||||||
if (theme.deletedAt || downloadingId || bulkDownload || forkingId || deletingId || moderatingId)
|
if (theme.deletedAt || downloadingIds.has(theme.id) || forkingId || deletingId || moderatingId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let installed =
|
let installed =
|
||||||
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
||||||
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
downloadingId = theme.id;
|
downloadingIds.add(theme.id);
|
||||||
downloadProgress = 10;
|
downloadProgressById.set(theme.id, 10);
|
||||||
try {
|
try {
|
||||||
await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
await downloadAndInstallCommunityTheme(theme, (_status, progress) => {
|
||||||
downloadProgress = progress;
|
downloadProgressById.set(theme.id, progress);
|
||||||
});
|
});
|
||||||
installed = $custom_themes.find(
|
installed = $custom_themes.find(
|
||||||
(candidate) => candidate.folder_name.toLowerCase() === theme.id.toLowerCase()
|
(candidate) => candidate.folder_name.toLowerCase() === theme.id.toLowerCase()
|
||||||
@@ -601,8 +472,8 @@
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingIds.delete(theme.id);
|
||||||
downloadProgress = 0;
|
downloadProgressById.delete(theme.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +483,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRefreshOwnedTheme(theme: CommunityTheme) {
|
async function handleRefreshOwnedTheme(theme: CommunityTheme) {
|
||||||
if (theme.deletedAt || downloadingId || bulkDownload || forkingId || deletingId || moderatingId)
|
if (theme.deletedAt || downloadingIds.has(theme.id) || forkingId || deletingId || moderatingId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const installed =
|
const installed =
|
||||||
@@ -631,11 +502,11 @@
|
|||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
downloadingId = theme.id;
|
downloadingIds.add(theme.id);
|
||||||
downloadProgress = 10;
|
downloadProgressById.set(theme.id, 10);
|
||||||
try {
|
try {
|
||||||
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
const res = await downloadAndInstallCommunityTheme(theme, (_status, progress) => {
|
||||||
downloadProgress = progress;
|
downloadProgressById.set(theme.id, progress);
|
||||||
});
|
});
|
||||||
const refreshed = $custom_themes.find(
|
const refreshed = $custom_themes.find(
|
||||||
(candidate) => candidate.folder_name.toLowerCase() === res.folderName.toLowerCase()
|
(candidate) => candidate.folder_name.toLowerCase() === res.folderName.toLowerCase()
|
||||||
@@ -654,13 +525,13 @@
|
|||||||
description: err instanceof Error ? err.message : String(err),
|
description: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingIds.delete(theme.id);
|
||||||
downloadProgress = 0;
|
downloadProgressById.delete(theme.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleVisibility(theme: CommunityTheme) {
|
async function handleToggleVisibility(theme: CommunityTheme) {
|
||||||
if (!canModerate || moderatingId || downloadingId || forkingId || deletingId) return;
|
if (!canModerate || moderatingId || forkingId || deletingId) return;
|
||||||
|
|
||||||
const hiding = !theme.isHidden;
|
const hiding = !theme.isHidden;
|
||||||
let reason: string | undefined;
|
let reason: string | undefined;
|
||||||
@@ -822,7 +693,6 @@
|
|||||||
theme.deletedAt ||
|
theme.deletedAt ||
|
||||||
theme.isHidden ||
|
theme.isHidden ||
|
||||||
theme.allowForking === false ||
|
theme.allowForking === false ||
|
||||||
downloadingId ||
|
|
||||||
forkingId ||
|
forkingId ||
|
||||||
deletingId ||
|
deletingId ||
|
||||||
moderatingId
|
moderatingId
|
||||||
@@ -853,10 +723,10 @@
|
|||||||
let installed: Theme | undefined;
|
let installed: Theme | undefined;
|
||||||
try {
|
try {
|
||||||
await loadWorkshopThemes();
|
await loadWorkshopThemes();
|
||||||
downloadingId = fork.id;
|
downloadingIds.add(fork.id);
|
||||||
downloadProgress = 10;
|
downloadProgressById.set(fork.id, 10);
|
||||||
const res = await downloadAndInstallCommunityTheme(fork, (msg, progress) => {
|
const res = await downloadAndInstallCommunityTheme(fork, (_status, progress) => {
|
||||||
downloadProgress = progress;
|
downloadProgressById.set(fork.id, progress);
|
||||||
});
|
});
|
||||||
installed = $custom_themes.find(
|
installed = $custom_themes.find(
|
||||||
(candidate) => candidate.folder_name.toLowerCase() === res.folderName.toLowerCase()
|
(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.`,
|
description: `${err instanceof Error ? err.message : String(err)} You can download it from the workshop.`,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingIds.delete(fork.id);
|
||||||
downloadProgress = 0;
|
downloadProgressById.delete(fork.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installed) {
|
if (installed) {
|
||||||
@@ -881,7 +751,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteTheme(theme: CommunityTheme) {
|
async function handleDeleteTheme(theme: CommunityTheme) {
|
||||||
if (theme.deletedAt || deletingId || downloadingId || forkingId || moderatingId) return;
|
if (theme.deletedAt || deletingId || forkingId || moderatingId) return;
|
||||||
const confirmed = window.confirm(
|
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.`
|
`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) {
|
function openRestoreDialog(theme: CommunityTheme) {
|
||||||
if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId)
|
if (restoringId || purgingId || forkingId || deletingId || moderatingId) return;
|
||||||
return;
|
|
||||||
purgeTarget = null;
|
purgeTarget = null;
|
||||||
trashActionError = null;
|
trashActionError = null;
|
||||||
restoreTarget = theme;
|
restoreTarget = theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPermanentDeleteDialog(theme: CommunityTheme) {
|
function openPermanentDeleteDialog(theme: CommunityTheme) {
|
||||||
if (restoringId || purgingId || downloadingId || forkingId || deletingId || moderatingId)
|
if (restoringId || purgingId || forkingId || deletingId || moderatingId) return;
|
||||||
return;
|
|
||||||
restoreTarget = null;
|
restoreTarget = null;
|
||||||
trashActionError = null;
|
trashActionError = null;
|
||||||
purgeTarget = theme;
|
purgeTarget = theme;
|
||||||
@@ -1604,28 +1472,6 @@
|
|||||||
</WorkshopTooltip>
|
</WorkshopTooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if selectedView === 'workshop'}
|
|
||||||
<WorkshopTooltip
|
|
||||||
content={selectionMode ? 'Exit download selection' : 'Select themes to download'}
|
|
||||||
>
|
|
||||||
{#snippet trigger(props)}
|
|
||||||
<Button
|
|
||||||
{...props}
|
|
||||||
variant={selectionMode ? 'secondary' : 'ghost'}
|
|
||||||
size="icon"
|
|
||||||
class="size-7.5 text-muted-foreground hover:text-white"
|
|
||||||
onclick={toggleSelectionMode}
|
|
||||||
aria-label={selectionMode
|
|
||||||
? 'Exit download selection'
|
|
||||||
: 'Select themes to download'}
|
|
||||||
aria-pressed={selectionMode}
|
|
||||||
>
|
|
||||||
<Check class="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</WorkshopTooltip>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $currentUser}
|
{#if $currentUser}
|
||||||
<WorkshopTooltip content={selectedView === 'trash' ? 'Back to workshop' : 'Open trash'}>
|
<WorkshopTooltip content={selectedView === 'trash' ? 'Back to workshop' : 'Open trash'}>
|
||||||
{#snippet trigger(props)}
|
{#snippet trigger(props)}
|
||||||
@@ -1663,76 +1509,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if selectionMode && selectedView === 'workshop'}
|
|
||||||
<div
|
|
||||||
class="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-lg border border-primary/30 bg-primary/10 px-3 py-2"
|
|
||||||
>
|
|
||||||
<div class="flex min-w-0 flex-wrap items-center gap-2 text-[11px] text-theme-100">
|
|
||||||
<span class="font-medium">{selectedThemes.length} selected</span>
|
|
||||||
{#if selectedThemes.length > downloadableSelectedThemes.length}
|
|
||||||
<span class="text-muted-foreground">
|
|
||||||
{selectedThemes.length - downloadableSelectedThemes.length} already installed
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-1.5 text-[10px] text-muted-foreground hover:text-white"
|
|
||||||
onclick={selectAllDownloadableThemes}
|
|
||||||
disabled={Boolean(bulkDownload) || themes.length === 0}
|
|
||||||
>
|
|
||||||
Select all
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 px-1.5 text-[10px] text-muted-foreground hover:text-white"
|
|
||||||
onclick={clearThemeSelection}
|
|
||||||
disabled={Boolean(bulkDownload) || selectedThemes.length === 0}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{#if bulkDownload}
|
|
||||||
<div class="flex items-center gap-2 text-[10px] text-primary tabular-nums">
|
|
||||||
<LoaderCircle class="size-3 animate-spin" />
|
|
||||||
<span class="max-w-48 truncate">
|
|
||||||
{bulkDownload.current}/{bulkDownload.total} · {bulkDownload.name}
|
|
||||||
</span>
|
|
||||||
<div class="h-1.5 w-20 overflow-hidden rounded-full bg-theme-800">
|
|
||||||
<div
|
|
||||||
class="h-full bg-primary transition-all duration-200"
|
|
||||||
style="width: {bulkDownload.progress}%"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<span>{bulkDownload.progress}%</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
class="h-7 gap-1 text-xs"
|
|
||||||
onclick={() => void handleBulkDownload()}
|
|
||||||
disabled={Boolean(bulkDownload) || downloadableSelectedThemes.length === 0}
|
|
||||||
>
|
|
||||||
<Download class="size-3" />
|
|
||||||
Download selected
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
class="size-7 text-muted-foreground hover:text-white"
|
|
||||||
onclick={toggleSelectionMode}
|
|
||||||
disabled={Boolean(bulkDownload)}
|
|
||||||
aria-label="Exit download selection"
|
|
||||||
>
|
|
||||||
<X class="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Content Grid / Status -->
|
<!-- Content Grid / Status -->
|
||||||
<div class="min-h-0 flex-1 pt-1">
|
<div class="min-h-0 flex-1 pt-1">
|
||||||
<ScrollContainer class="pr-2">
|
<ScrollContainer class="pr-2">
|
||||||
@@ -1777,8 +1553,8 @@
|
|||||||
{@const isActive = Boolean(
|
{@const isActive = Boolean(
|
||||||
localTheme && $active_custom_theme?.folder_name === localTheme.folder_name
|
localTheme && $active_custom_theme?.folder_name === localTheme.folder_name
|
||||||
)}
|
)}
|
||||||
{@const isDownloading = downloadingId === theme.id}
|
{@const isDownloading = downloadingIds.has(theme.id)}
|
||||||
{@const isSelected = selectionMode && selectedThemeIds.includes(theme.id)}
|
{@const downloadPercent = downloadProgressById.get(theme.id) ?? 0}
|
||||||
{@const isDeleted = Boolean(theme.deletedAt)}
|
{@const isDeleted = Boolean(theme.deletedAt)}
|
||||||
{@const hasUpdate = Boolean(
|
{@const hasUpdate = Boolean(
|
||||||
!isDeleted && localTheme && localTheme.version !== theme.version
|
!isDeleted && localTheme && localTheme.version !== theme.version
|
||||||
@@ -1792,11 +1568,9 @@
|
|||||||
{@const showOverflowMenu = !isDeleted && (owned || canModerate || canReport)}
|
{@const showOverflowMenu = !isDeleted && (owned || canModerate || canReport)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isSelected
|
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
|
||||||
? 'border-primary/70 ring-1 ring-primary/30'
|
? 'border-primary/60 ring-1 ring-primary/20'
|
||||||
: isActive
|
: 'border-theme-800/60'} transition-all duration-300 hover:-translate-y-0.5 hover:border-white/20 hover:bg-theme-900/50 hover:shadow-xl hover:shadow-black/30"
|
||||||
? 'border-primary/60 ring-1 ring-primary/20'
|
|
||||||
: 'border-theme-800/60'} transition-all duration-300 hover:-translate-y-0.5 hover:border-white/20 hover:bg-theme-900/50 hover:shadow-xl hover:shadow-black/30"
|
|
||||||
>
|
>
|
||||||
<!-- 16:9 Thumbnail -->
|
<!-- 16:9 Thumbnail -->
|
||||||
<div class="relative aspect-video w-full shrink-0 overflow-hidden bg-theme-950/70">
|
<div class="relative aspect-video w-full shrink-0 overflow-hidden bg-theme-950/70">
|
||||||
@@ -1970,21 +1744,11 @@
|
|||||||
<div class="flex flex-1 flex-col justify-between gap-2 p-3">
|
<div class="flex flex-1 flex-col justify-between gap-2 p-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="flex min-w-0 items-center gap-2">
|
<h3
|
||||||
{#if selectionMode && !isDeleted}
|
class="truncate text-[13px] font-bold text-white group-hover:text-primary transition-colors"
|
||||||
<Checkbox
|
>
|
||||||
class="shrink-0"
|
{theme.name}
|
||||||
checked={selectedThemeIds.includes(theme.id)}
|
</h3>
|
||||||
onCheckedChange={() => toggleThemeSelection(theme.id)}
|
|
||||||
aria-label={`Select ${theme.name} for download`}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<h3
|
|
||||||
class="truncate text-[13px] font-bold text-white group-hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
{theme.name}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Interactive Like Button -->
|
<!-- Interactive Like Button -->
|
||||||
{#if isDeleted}
|
{#if isDeleted}
|
||||||
@@ -2122,12 +1886,12 @@
|
|||||||
{#if isDownloading}
|
{#if isDownloading}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-[10px] text-primary tabular-nums"
|
<span class="text-[10px] text-primary tabular-nums"
|
||||||
>{downloadProgress}%</span
|
>{downloadPercent}%</span
|
||||||
>
|
>
|
||||||
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-theme-800">
|
<div class="h-1.5 w-16 overflow-hidden rounded-full bg-theme-800">
|
||||||
<div
|
<div
|
||||||
class="h-full bg-primary transition-all duration-200"
|
class="h-full bg-primary transition-all duration-200"
|
||||||
style="width: {downloadProgress}%"
|
style="width: {downloadPercent}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2204,7 +1968,7 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-7 gap-1 text-xs bg-cyan-600 hover:bg-cyan-500 text-white font-medium"
|
class="h-7 gap-1 text-xs bg-cyan-600 hover:bg-cyan-500 text-white font-medium"
|
||||||
onclick={() => handleDownloadAndApply(theme)}
|
onclick={() => handleDownloadTheme(theme)}
|
||||||
>
|
>
|
||||||
<Download class="size-3" />
|
<Download class="size-3" />
|
||||||
Update
|
Update
|
||||||
@@ -2222,10 +1986,10 @@
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-7 gap-1 text-xs"
|
class="h-7 gap-1 text-xs"
|
||||||
onclick={() => handleDownloadAndApply(theme)}
|
onclick={() => handleDownloadTheme(theme)}
|
||||||
>
|
>
|
||||||
<Download class="size-3" />
|
<Download class="size-3" />
|
||||||
Download & Apply
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user