feat(themes): polish installed cards and add workshop theme details
Installed themes now use a 16:9 preview with stable card spacing and a single overflow menu for edit, restore, update, report and uninstall actions. Community theme previews and titles open a large detail dialog with a full-size preview, metadata, tags, review status and the relevant install action.
This commit is contained in:
1 parent
5613c3be25
commit
47c022642f
2 files changed
+435
-100
No files matched your search
@@ -231,6 +231,7 @@
|
|||||||
let actionError = $state<string | null>(null);
|
let actionError = $state<string | null>(null);
|
||||||
let actionBusy = $state(false);
|
let actionBusy = $state(false);
|
||||||
let actionDialogContent = $state<HTMLElement | null>(null);
|
let actionDialogContent = $state<HTMLElement | null>(null);
|
||||||
|
let infoTheme = $state<CommunityTheme | null>(null);
|
||||||
|
|
||||||
const REPORT_CATEGORIES: Array<{ id: ThemeReportCategory; label: string }> = [
|
const REPORT_CATEGORIES: Array<{ id: ThemeReportCategory; label: string }> = [
|
||||||
{ id: 'spam', label: 'Spam / advertising' },
|
{ id: 'spam', label: 'Spam / advertising' },
|
||||||
@@ -426,6 +427,29 @@
|
|||||||
actionError = null;
|
actionError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openThemeInfo(theme: CommunityTheme) {
|
||||||
|
infoTheme = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeThemeInfo() {
|
||||||
|
infoTheme = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openThemeInfoTag(tag: string) {
|
||||||
|
closeThemeInfo();
|
||||||
|
handleTagClick(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editFromThemeInfo(theme: CommunityTheme) {
|
||||||
|
closeThemeInfo();
|
||||||
|
void handleEditTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFromThemeInfo(theme: CommunityTheme) {
|
||||||
|
closeThemeInfo();
|
||||||
|
void handleApplyExisting(theme);
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmPendingAction() {
|
async function confirmPendingAction() {
|
||||||
const action = pendingAction;
|
const action = pendingAction;
|
||||||
if (!action || actionBusy) return;
|
if (!action || actionBusy) return;
|
||||||
@@ -2055,7 +2079,27 @@
|
|||||||
: '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-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 cursor-zoom-in overflow-hidden bg-theme-950/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label={`View ${theme.name} details`}
|
||||||
|
onclick={() => openThemeInfo(theme)}
|
||||||
|
onkeydown={(event) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') return;
|
||||||
|
event.preventDefault();
|
||||||
|
openThemeInfo(theme);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 z-[5] flex items-center justify-center opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="rounded-full border border-white/15 bg-theme-950/80 px-3 py-1.5 text-[10px] font-medium text-white shadow-lg backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
View details
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
src={theme.previewUrl}
|
src={theme.previewUrl}
|
||||||
alt={theme.name}
|
alt={theme.name}
|
||||||
@@ -2257,11 +2301,14 @@
|
|||||||
<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">
|
||||||
<h3
|
<button
|
||||||
class="truncate text-[13px] font-bold text-white group-hover:text-primary transition-colors"
|
type="button"
|
||||||
|
class="min-w-0 truncate rounded text-left text-[13px] font-bold text-white transition-colors hover:text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/60"
|
||||||
|
title={`View ${theme.name} details`}
|
||||||
|
onclick={() => openThemeInfo(theme)}
|
||||||
>
|
>
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</h3>
|
</button>
|
||||||
|
|
||||||
<!-- Interactive Like Button -->
|
<!-- Interactive Like Button -->
|
||||||
{#if isDeleted}
|
{#if isDeleted}
|
||||||
@@ -3993,6 +4040,246 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<Dialog.Root
|
||||||
|
open={infoTheme !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) closeThemeInfo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Content
|
||||||
|
class="flex max-h-[calc(100vh-2rem)] w-[calc(100%_-_2rem)] max-w-5xl flex-col gap-0 overflow-hidden rounded-xl border-theme-800 bg-theme-950 p-0 text-white shadow-2xl sm:rounded-xl"
|
||||||
|
>
|
||||||
|
{#if infoTheme}
|
||||||
|
{@const themeInfo = infoTheme}
|
||||||
|
{@const infoLocal =
|
||||||
|
localThemeMap.get(themeInfo.id.toLowerCase()) ??
|
||||||
|
localThemeMap.get(themeInfo.name.toLowerCase())}
|
||||||
|
{@const infoInstalled = Boolean(infoLocal)}
|
||||||
|
{@const infoActive = Boolean(
|
||||||
|
infoLocal && $active_custom_theme?.folder_name === infoLocal.folder_name
|
||||||
|
)}
|
||||||
|
{@const infoDownloading = activeThemeDownloads.has(themeInfo.id)}
|
||||||
|
{@const infoPercent = themeDownloadProgress.get(themeInfo.id) ?? 0}
|
||||||
|
{@const infoOwned = isOwnedByCurrentUser(themeInfo)}
|
||||||
|
{@const infoLegacy =
|
||||||
|
themeInfo.apiVersion === '1.0' || Boolean(themeInfo.tags?.includes('legacy'))}
|
||||||
|
{@const infoHasUpdate = Boolean(
|
||||||
|
!themeInfo.deletedAt && infoLocal && infoLocal.version !== themeInfo.version
|
||||||
|
)}
|
||||||
|
{@const infoUnavailable = themeInfo.isHidden || themeInfo.isQuarantined}
|
||||||
|
|
||||||
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<Dialog.Title class="truncate text-base font-semibold text-white">
|
||||||
|
{themeInfo.name}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description
|
||||||
|
class="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-[11px] text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span>by <span class="text-white/80">{themeInfo.author.name}</span></span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>v{themeInfo.version}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span
|
||||||
|
>{themeInfo.apiVersion === '1.0'
|
||||||
|
? 'Legacy v1'
|
||||||
|
: `API ${themeInfo.apiVersion}`}</span
|
||||||
|
>
|
||||||
|
{#if infoActive}
|
||||||
|
<span>·</span>
|
||||||
|
<span class="font-medium text-primary">In use</span>
|
||||||
|
{/if}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="size-7 shrink-0 text-muted-foreground hover:text-white"
|
||||||
|
onclick={closeThemeInfo}
|
||||||
|
aria-label="Close theme details"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||||
|
<div class="grid md:grid-cols-[minmax(0,1.6fr)_minmax(280px,0.9fr)]">
|
||||||
|
<div class="space-y-3 bg-black/25 p-3 sm:p-4">
|
||||||
|
<div class="overflow-hidden rounded-xl border border-theme-800 bg-theme-950">
|
||||||
|
<img
|
||||||
|
src={themeInfo.previewUrl}
|
||||||
|
alt={`${themeInfo.name} preview`}
|
||||||
|
class="aspect-video w-full object-cover object-center"
|
||||||
|
onerror={(e) => {
|
||||||
|
(e.currentTarget as HTMLImageElement).src = DefaultThemePreview;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[10px] text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center gap-1">
|
||||||
|
<Download class="size-3" />{themeInfo.downloadCount} downloads
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-1">
|
||||||
|
<Heart class="size-3" />{themeInfo.likeCount} likes
|
||||||
|
</span>
|
||||||
|
<span>{formatBytes(themeInfo.fileSize)}</span>
|
||||||
|
<span>Updated {formatModerationDate(themeInfo.updatedAt)}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-[11px] leading-relaxed text-white/80">
|
||||||
|
{themeInfo.description || 'No description provided.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-4 border-t border-theme-800/70 p-4 md:border-t-0 md:border-l"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="text-[10px] font-semibold tracking-wide text-white/60 uppercase">Tags</p>
|
||||||
|
{#if themeInfo.tags.length > 0}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{#each themeInfo.tags as tag (tag)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-theme-700 bg-theme-900 px-2 py-0.5 text-[10px] text-muted-foreground transition-colors hover:border-primary/40 hover:text-white"
|
||||||
|
onclick={() => openThemeInfoTag(tag)}
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1.5 text-[10px] text-muted-foreground">No tags.</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if themeInfo.reviewStatus !== 'approved'}
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-amber-500/25 bg-amber-500/5 p-2.5 text-[10px] leading-relaxed"
|
||||||
|
>
|
||||||
|
{#if themeInfo.reviewStatus === 'pending'}
|
||||||
|
<p class="text-amber-200">
|
||||||
|
<LoaderCircle class="mr-1 inline size-3 align-[-2px]" /> Awaiting staff review.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-red-200">
|
||||||
|
<X class="mr-1 inline size-3 align-[-2px]" /> Not approved.
|
||||||
|
</p>
|
||||||
|
{#if themeInfo.reviewNote}
|
||||||
|
<p class="mt-1 text-muted-foreground">{themeInfo.reviewNote}</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if infoUnavailable}
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/15 p-2.5 text-[10px] leading-relaxed"
|
||||||
|
>
|
||||||
|
{#if themeInfo.isQuarantined}
|
||||||
|
<p class="text-red-200">
|
||||||
|
Taken down: {themeInfo.quarantineReason ?? 'No reason provided.'}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{#if themeInfo.isHidden}
|
||||||
|
<p class="text-red-200">
|
||||||
|
Hidden: {themeInfo.hiddenReason ?? 'No reason provided.'}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if themeInfo.forkedFrom}
|
||||||
|
{@const forkedFrom = themeInfo.forkedFrom}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-start gap-1.5 text-left text-[10px] text-muted-foreground transition-colors hover:text-white"
|
||||||
|
onclick={() => {
|
||||||
|
closeThemeInfo();
|
||||||
|
void handleOpenForkSource(themeInfo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GitFork class="mt-0.5 size-3 shrink-0" />
|
||||||
|
<span class="truncate">
|
||||||
|
Forked from {forkedFrom.name} by {forkedFrom.authorName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-auto space-y-2 pt-2">
|
||||||
|
{#if themeInfo.deletedAt}
|
||||||
|
<p
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/15 p-2.5 text-[10px] text-red-200"
|
||||||
|
>
|
||||||
|
This theme is in your trash. Restore it from the Trash view.
|
||||||
|
</p>
|
||||||
|
{:else if infoUnavailable}
|
||||||
|
<Button class="w-full gap-1.5" variant="secondary" disabled>
|
||||||
|
<EyeOff class="size-3.5" /> Currently unavailable
|
||||||
|
</Button>
|
||||||
|
{:else if infoDownloading}
|
||||||
|
<div class="space-y-1.5 rounded-lg border border-primary/30 bg-primary/10 p-2.5">
|
||||||
|
<div class="flex items-center justify-between text-[10px]">
|
||||||
|
<span class="text-primary">Downloading...</span>
|
||||||
|
<span class="text-primary tabular-nums">{infoPercent}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 overflow-hidden rounded-full bg-theme-800">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary transition-all duration-200"
|
||||||
|
style={`width: ${infoPercent}%`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if infoOwned && infoInstalled && !infoLegacy}
|
||||||
|
<Button class="w-full gap-1.5" onclick={() => editFromThemeInfo(themeInfo)}>
|
||||||
|
<Pencil class="size-3.5" /> Edit theme
|
||||||
|
</Button>
|
||||||
|
{:else if infoActive}
|
||||||
|
<Button class="w-full gap-1.5" disabled>
|
||||||
|
<Check class="size-3.5" /> Theme in use
|
||||||
|
</Button>
|
||||||
|
{:else if infoInstalled && infoHasUpdate}
|
||||||
|
<Button
|
||||||
|
class="w-full gap-1.5"
|
||||||
|
onclick={() => void handleDownloadTheme(themeInfo)}
|
||||||
|
>
|
||||||
|
<Download class="size-3.5" /> Update to v{themeInfo.version}
|
||||||
|
</Button>
|
||||||
|
{:else if infoInstalled}
|
||||||
|
<Button class="w-full gap-1.5" onclick={() => applyFromThemeInfo(themeInfo)}>
|
||||||
|
<Check class="size-3.5" /> Apply theme
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
class="w-full gap-1.5"
|
||||||
|
onclick={() => void handleDownloadTheme(themeInfo)}
|
||||||
|
>
|
||||||
|
<Download class="size-3.5" /> Download & install
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !infoOwned && $currentUser && !infoUnavailable && !themeInfo.deletedAt}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="w-full gap-1.5 text-[11px] text-muted-foreground hover:text-white"
|
||||||
|
onclick={() => {
|
||||||
|
closeThemeInfo();
|
||||||
|
openReportDialog(themeInfo);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flag class="size-3.5" /> Report theme
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
<Dialog.Root
|
<Dialog.Root
|
||||||
open={pendingAction !== null}
|
open={pendingAction !== null}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
+144
-96
@@ -163,6 +163,7 @@
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Clock,
|
Clock,
|
||||||
|
Ellipsis,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Flag,
|
Flag,
|
||||||
Globe,
|
Globe,
|
||||||
@@ -505,6 +506,29 @@
|
|||||||
await config.value<string>('theme').set(theme.name);
|
await config.value<string>('theme').set(theme.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uninstallInstalledTheme = async (theme: Theme) => {
|
||||||
|
try {
|
||||||
|
const deleteResult = await deleteTheme(theme);
|
||||||
|
if (deleteResult) {
|
||||||
|
sileo.success({
|
||||||
|
title: 'Theme uninstalled',
|
||||||
|
description: `"${theme.name}" was removed from your library.`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Could not uninstall theme',
|
||||||
|
description: 'The theme files could not be removed.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
sileo.error({
|
||||||
|
title: 'Could not uninstall theme',
|
||||||
|
description: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updateInstalledTheme = async (theme: Theme) => {
|
const updateInstalledTheme = async (theme: Theme) => {
|
||||||
const wasActive = $active_custom_theme?.folder_name === theme.folder_name;
|
const wasActive = $active_custom_theme?.folder_name === theme.folder_name;
|
||||||
const updateResult = await downloadTheme(theme, true);
|
const updateResult = await downloadTheme(theme, true);
|
||||||
@@ -3911,6 +3935,28 @@
|
|||||||
{#each filteredThemes as theme, index (theme.folder_name)}
|
{#each filteredThemes as theme, index (theme.folder_name)}
|
||||||
{@const isActive =
|
{@const isActive =
|
||||||
$active_custom_theme?.folder_name === theme.folder_name}
|
$active_custom_theme?.folder_name === theme.folder_name}
|
||||||
|
{@const canEditTheme =
|
||||||
|
theme.status === 'installed' && Boolean(theme.definition)}
|
||||||
|
{@const canRestoreTheme =
|
||||||
|
theme.status === 'installed' && (theme.backups?.length ?? 0) > 0}
|
||||||
|
{@const canUpdateTheme =
|
||||||
|
theme.status === 'installed' && theme.updateAvailable}
|
||||||
|
{@const canReportTheme =
|
||||||
|
Boolean($currentUser && ownedWorkshopLoaded) &&
|
||||||
|
!isOwnInstalledTheme(theme) &&
|
||||||
|
theme.status === 'installed' &&
|
||||||
|
theme.name !== 'Default' &&
|
||||||
|
theme.folder_name !== 'default'}
|
||||||
|
{@const canUninstallTheme =
|
||||||
|
(theme.status === 'installed' || theme.status === 'deleting') &&
|
||||||
|
!isActive &&
|
||||||
|
theme.name !== 'Default'}
|
||||||
|
{@const hasThemeMenu =
|
||||||
|
canEditTheme ||
|
||||||
|
canRestoreTheme ||
|
||||||
|
canUpdateTheme ||
|
||||||
|
canReportTheme ||
|
||||||
|
canUninstallTheme}
|
||||||
<div
|
<div
|
||||||
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
|
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
|
||||||
? 'border-primary/60 ring-1 ring-primary/15'
|
? 'border-primary/60 ring-1 ring-primary/15'
|
||||||
@@ -3925,7 +3971,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="relative h-36 w-full shrink-0 overflow-hidden bg-theme-950/60"
|
class="relative aspect-video w-full shrink-0 overflow-hidden bg-theme-950/60"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="h-full w-full {$reduceAnimations
|
class="h-full w-full {$reduceAnimations
|
||||||
@@ -4034,13 +4080,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if theme.description}
|
<p
|
||||||
<p
|
class="min-h-8 line-clamp-2 text-[10px] leading-snug text-muted-foreground"
|
||||||
class="line-clamp-2 text-[10px] leading-snug text-muted-foreground"
|
>
|
||||||
>
|
{theme.description}
|
||||||
{theme.description}
|
</p>
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
<div class="mt-auto flex items-center gap-1 pt-0.5">
|
<div class="mt-auto flex items-center gap-1 pt-0.5">
|
||||||
<Button
|
<Button
|
||||||
class="relative h-8 min-w-0 flex-1 overflow-hidden px-2 text-xs {theme.status ===
|
class="relative h-8 min-w-0 flex-1 overflow-hidden px-2 text-xs {theme.status ===
|
||||||
@@ -4089,94 +4133,98 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
{#if theme.status === 'installed' && theme.definition}
|
{#if hasThemeMenu}
|
||||||
<Button
|
<DropdownMenu.Root>
|
||||||
variant="secondary"
|
<DropdownMenu.Trigger>
|
||||||
size="icon"
|
{#snippet child({ props })}
|
||||||
class="size-7 shrink-0"
|
<Button
|
||||||
title="Edit theme"
|
{...props}
|
||||||
onclick={() => openThemeEditor(theme)}
|
variant="secondary"
|
||||||
>
|
size="icon"
|
||||||
<ThemeIcon name="edit" set={iconSet} class="size-3.5" />
|
class="size-7 shrink-0 text-muted-foreground hover:text-white"
|
||||||
</Button>
|
title="More theme actions"
|
||||||
{/if}
|
aria-label={`More actions for ${theme.name}`}
|
||||||
{#if theme.status === 'installed' && (theme.backups?.length ?? 0) > 0}
|
>
|
||||||
<Button
|
<Ellipsis class="size-3.5" />
|
||||||
variant="secondary"
|
</Button>
|
||||||
size="icon"
|
{/snippet}
|
||||||
class="size-7 shrink-0"
|
</DropdownMenu.Trigger>
|
||||||
title="Restore previous version ({new Date(
|
<DropdownMenu.Content
|
||||||
theme.backups![0].createdAt
|
align="end"
|
||||||
).toLocaleDateString()})"
|
class="w-48 border-theme-700 bg-theme-950 text-theme-50"
|
||||||
onclick={() => requestThemeRestore(theme, theme.backups![0])}
|
>
|
||||||
>
|
{#if canEditTheme}
|
||||||
<ThemeIcon name="history" set={iconSet} class="size-3.5" />
|
<DropdownMenu.Item
|
||||||
</Button>
|
class="gap-2 text-[11px]"
|
||||||
{/if}
|
onclick={() => openThemeEditor(theme)}
|
||||||
{#if theme.status === 'installed' && theme.updateAvailable}
|
>
|
||||||
<Button
|
<ThemeIcon name="edit" set={iconSet} class="size-3.5" />
|
||||||
variant="secondary"
|
Edit theme
|
||||||
size="icon"
|
</DropdownMenu.Item>
|
||||||
class="size-7 shrink-0"
|
{/if}
|
||||||
title={`Update theme${theme.availableVersion ? ` to v${theme.availableVersion}` : ''}`}
|
{#if canRestoreTheme}
|
||||||
onclick={() => updateInstalledTheme(theme)}
|
<DropdownMenu.Item
|
||||||
>
|
class="gap-2 text-[11px]"
|
||||||
<ThemeIcon name="download" set={iconSet} class="size-3.5" />
|
onclick={() =>
|
||||||
</Button>
|
requestThemeRestore(theme, theme.backups![0])}
|
||||||
{/if}
|
>
|
||||||
{#if (theme.status === 'installed' || theme.status === 'deleting') && !isActive && theme.name !== 'Default'}
|
<ThemeIcon
|
||||||
<Button
|
name="history"
|
||||||
variant="destructive"
|
set={iconSet}
|
||||||
size="icon"
|
class="size-3.5"
|
||||||
class="size-7 shrink-0"
|
/>
|
||||||
title="Uninstall theme"
|
Restore previous version
|
||||||
disabled={theme.status !== 'installed'}
|
</DropdownMenu.Item>
|
||||||
onclick={async () => {
|
{/if}
|
||||||
try {
|
{#if canUpdateTheme}
|
||||||
const deleteResult = await deleteTheme(theme);
|
<DropdownMenu.Item
|
||||||
if (deleteResult) {
|
class="gap-2 text-[11px]"
|
||||||
sileo.success({
|
onclick={() => updateInstalledTheme(theme)}
|
||||||
title: 'Yaay!',
|
>
|
||||||
description: 'Theme uninstalled successfully!',
|
<ThemeIcon
|
||||||
});
|
name="download"
|
||||||
} else {
|
set={iconSet}
|
||||||
sileo.error({
|
class="size-3.5"
|
||||||
title: 'Uhhm..',
|
/>
|
||||||
description: 'Failed to uninstall theme.',
|
Update theme
|
||||||
});
|
</DropdownMenu.Item>
|
||||||
}
|
{/if}
|
||||||
} catch (err) {
|
{#if canReportTheme}
|
||||||
console.log(err);
|
<DropdownMenu.Item
|
||||||
sileo.error({
|
class="gap-2 text-[11px]"
|
||||||
title: 'Uhhm..',
|
disabled={installedReportLoading}
|
||||||
description: 'Failed to uninstall theme.',
|
onclick={() => void openInstalledReport(theme)}
|
||||||
});
|
>
|
||||||
}
|
<Flag class="size-3.5" />
|
||||||
}}
|
Report theme
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
{#if theme.status === 'deleting'}
|
{/if}
|
||||||
<ThemeIcon
|
{#if canUninstallTheme}
|
||||||
name="loader"
|
<DropdownMenu.Separator class="my-1 bg-theme-800" />
|
||||||
set={iconSet}
|
<DropdownMenu.Item
|
||||||
class="size-3.5 animate-spin"
|
class="gap-2 text-[11px] text-red-300 data-[highlighted]:bg-red-500/15 data-[highlighted]:text-red-200"
|
||||||
/>
|
disabled={theme.status !== 'installed'}
|
||||||
{:else}
|
onclick={() => void uninstallInstalledTheme(theme)}
|
||||||
<ThemeIcon name="trash" set={iconSet} class="size-3.5" />
|
>
|
||||||
{/if}
|
{#if theme.status === 'deleting'}
|
||||||
</Button>
|
<ThemeIcon
|
||||||
{/if}
|
name="loader"
|
||||||
{#if $currentUser && ownedWorkshopLoaded && !isOwnInstalledTheme(theme) && theme.status === 'installed' && theme.name !== 'Default' && theme.folder_name !== 'default'}
|
set={iconSet}
|
||||||
<Button
|
class="size-3.5 animate-spin"
|
||||||
variant="secondary"
|
/>
|
||||||
size="icon"
|
Uninstalling...
|
||||||
class="size-7 shrink-0 text-muted-foreground hover:bg-amber-500/15 hover:text-amber-300"
|
{:else}
|
||||||
title="Report theme"
|
<ThemeIcon
|
||||||
aria-label={`Report ${theme.name}`}
|
name="trash"
|
||||||
disabled={installedReportLoading}
|
set={iconSet}
|
||||||
onclick={() => void openInstalledReport(theme)}
|
class="size-3.5"
|
||||||
>
|
/>
|
||||||
<Flag class="size-3.5" />
|
Uninstall theme
|
||||||
</Button>
|
{/if}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user