mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Better theme displaying (#419)
* add more data add more info for users about themes and also make related themes actually show related themes * sorting and similar
This commit is contained in:
@@ -43,8 +43,24 @@
|
||||
onclick={() => setDisplayTheme(theme)}
|
||||
>
|
||||
<img src={theme.marqueeImage || theme.coverImage} alt="Theme Preview" class="object-cover w-full h-full" />
|
||||
{#if theme.featured === true}
|
||||
<div class="absolute top-4 left-4 z-[2] pointer-events-none">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-100 shadow-sm"
|
||||
aria-label="Featured theme"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3.5 h-3.5">
|
||||
<path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class='absolute bottom-0 left-0 p-8 z-[1]'>
|
||||
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||
{#if theme.author}
|
||||
<p class="text-sm text-white/90 mt-1 mb-1 line-clamp-1">By {theme.author}</p>
|
||||
{/if}
|
||||
<p class='text-lg text-white'>{theme.description}</p>
|
||||
</div>
|
||||
<div class='absolute bottom-0 left-0 w-full h-1/2 to-transparent bg-linear-to-t from-black/80'></div>
|
||||
|
||||
@@ -49,6 +49,19 @@
|
||||
class="bg-gray-50 w-full transition-all duration-500 ease-out relative group flex flex-col rounded-xl overflow-clip border hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] dark:hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto"
|
||||
transition:fade
|
||||
>
|
||||
{#if theme.featured === true}
|
||||
<div class="absolute top-2 left-2 z-20 pointer-events-none">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-100 shadow-sm"
|
||||
aria-label="Featured theme"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3.5 h-3.5">
|
||||
<path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Menu dropdown -->
|
||||
<div class="absolute top-2 right-2 z-20" data-theme-menu bind:this={menuRef}>
|
||||
<button
|
||||
@@ -90,6 +103,9 @@
|
||||
</div>
|
||||
<div class="absolute bottom-1 left-3 right-3 z-10 mb-1 flex flex-col gap-0.5">
|
||||
<span class="text-xl font-bold text-white drop-shadow-md">{theme.name}</span>
|
||||
{#if theme.author}
|
||||
<span class="text-xs text-white/85 drop-shadow-md line-clamp-1">By {theme.author}</span>
|
||||
{/if}
|
||||
<div class="flex gap-3 text-xs font-medium text-white/90 drop-shadow-sm">
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5">
|
||||
|
||||
@@ -25,18 +25,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get related themes
|
||||
function getRelatedThemes() {
|
||||
if (!theme) return [];
|
||||
return allThemes
|
||||
.filter((t: Theme) => !!t && t.id !== theme.id)
|
||||
.sort(
|
||||
(a: Theme, b: Theme) =>
|
||||
a.name.localeCompare(theme.name) - b.name.localeCompare(theme.name),
|
||||
)
|
||||
.slice(0, 4);
|
||||
function tagsOverlap(a: string[] | undefined, b: string[] | undefined): boolean {
|
||||
const lowerB = new Set((b ?? []).map((t) => t.toLowerCase()));
|
||||
return (a ?? []).some((t) => lowerB.has(t.toLowerCase()));
|
||||
}
|
||||
|
||||
const relatedThemes = $derived.by(() => {
|
||||
const t = theme;
|
||||
if (!t) return [] as Theme[];
|
||||
if ((t.tags ?? []).length === 0) return [];
|
||||
return allThemes
|
||||
.filter((x: Theme) => !!x && x.id !== t.id && tagsOverlap(t.tags, x.tags))
|
||||
.sort((a: Theme, b: Theme) => {
|
||||
const diff = (b.download_count ?? 0) - (a.download_count ?? 0);
|
||||
if (diff !== 0) return diff;
|
||||
const byName = a.name.localeCompare(b.name);
|
||||
if (byName !== 0) return byName;
|
||||
return a.id.localeCompare(b.id);
|
||||
})
|
||||
.slice(0, 4);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (displayTheme) {
|
||||
animate(
|
||||
@@ -93,9 +102,27 @@
|
||||
{'\ued8a'}
|
||||
</button>
|
||||
</div>
|
||||
<h2 class="mb-2 text-2xl font-bold">
|
||||
{theme.name}
|
||||
</h2>
|
||||
<div class="flex flex-wrap items-center gap-2 pr-12 mb-2">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{theme.name}
|
||||
</h2>
|
||||
{#if theme.featured === true}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-900 dark:bg-amber-950 dark:text-amber-100"
|
||||
aria-label="Featured theme"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-3.5 h-3.5">
|
||||
<path fill-rule="evenodd" d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Featured
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if theme.author}
|
||||
<p class="mb-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
By {theme.author}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex gap-4 mb-4 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
@@ -150,24 +177,26 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||
{#if relatedThemes.length > 0}
|
||||
<div class="my-8 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||
|
||||
<h3 class="mb-4 text-lg font-bold">
|
||||
Similar Themes
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{#each getRelatedThemes() as relatedTheme (relatedTheme.id)}
|
||||
<button onclick={() => { hideModal(relatedTheme) }} class="relative z-0 hover:z-20 w-full cursor-pointer">
|
||||
<div class="bg-gray-50 w-full transition-all duration-500 ease-out relative group group/card flex flex-col hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5">
|
||||
{relatedTheme.name}
|
||||
<h3 class="mb-4 text-lg font-bold">
|
||||
Related themes
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{#each relatedThemes as relatedTheme (relatedTheme.id)}
|
||||
<button onclick={() => { hideModal(relatedTheme) }} class="relative z-0 hover:z-20 w-full cursor-pointer">
|
||||
<div class="bg-gray-50 w-full transition-all duration-500 ease-out relative group group/card flex flex-col hover:scale-105 hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5">
|
||||
{relatedTheme.name}
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-0 w-full h-3/4 to-transparent from-black/80 bg-linear-to-t"></div>
|
||||
<img src={relatedTheme.marqueeImage || relatedTheme.coverImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-0 w-full h-3/4 to-transparent from-black/80 bg-linear-to-t"></div>
|
||||
<img src={relatedTheme.marqueeImage || relatedTheme.coverImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center h-full text-zinc-600 dark:text-zinc-300">
|
||||
|
||||
@@ -54,6 +54,17 @@
|
||||
activeTab = tab;
|
||||
};
|
||||
|
||||
/** Featured themes first; within each group, newest by `created_at` (API: Unix seconds). */
|
||||
function compareStoreThemes(a: Theme, b: Theme): number {
|
||||
const fa = a.featured === true ? 1 : 0;
|
||||
const fb = b.featured === true ? 1 : 0;
|
||||
if (fa !== fb) return fb - fa;
|
||||
const ca = a.created_at ?? 0;
|
||||
const cb = b.created_at ?? 0;
|
||||
if (ca !== cb) return cb - ca;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
const toggleFavorite = async (theme: Theme) => {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (!token) return;
|
||||
@@ -96,11 +107,8 @@
|
||||
if (!data?.success || !data?.data?.themes) {
|
||||
throw new Error(data?.error || 'Failed to fetch themes');
|
||||
}
|
||||
themes = data.data.themes;
|
||||
|
||||
// Shuffle for cover themes
|
||||
const shuffled = [...themes].sort(() => 0.5 - Math.random());
|
||||
coverThemes = shuffled.slice(0, 3);
|
||||
themes = [...data.data.themes].sort(compareStoreThemes);
|
||||
coverThemes = themes.slice(0, 3);
|
||||
|
||||
loading = false;
|
||||
} catch (err) {
|
||||
@@ -118,11 +126,14 @@
|
||||
darkMode = $settingsState.DarkMode;
|
||||
});
|
||||
|
||||
// Filter themes based on search term
|
||||
let filteredThemes = $derived(themes.filter(theme =>
|
||||
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
theme.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
));
|
||||
// Filter themes (list is already featured-first, then newest; filter preserves order)
|
||||
let filteredThemes = $derived(
|
||||
themes.filter(
|
||||
(theme) =>
|
||||
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
theme.description.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
loadBackground();
|
||||
|
||||
@@ -8,6 +8,11 @@ export type Theme = {
|
||||
is_favorited?: boolean;
|
||||
favorite_count?: number;
|
||||
download_count?: number;
|
||||
author?: string;
|
||||
featured?: boolean;
|
||||
tags?: string[];
|
||||
/** Unix time in seconds (API list/detail). */
|
||||
created_at?: number;
|
||||
/** Unix seconds — last server update (GET /api/themes). */
|
||||
updated_at?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user