mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-13 23:24:40 +00:00
feat: theme flavours for theme varients
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { Theme } from '@/interface/types/Theme';
|
||||
import type { ThemeCoverSlide } from '@/interface/types/Theme';
|
||||
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
|
||||
let { coverThemes, setDisplayTheme } = $props<{ coverThemes: Theme[], setDisplayTheme: (theme: Theme) => void }>();
|
||||
let { slides, setDisplayTheme } = $props<{
|
||||
slides: ThemeCoverSlide[];
|
||||
setDisplayTheme: (theme: import('@/interface/types/Theme').Theme) => void;
|
||||
}>();
|
||||
let emblaApi = $state();
|
||||
|
||||
const options = { loop: true };
|
||||
@@ -12,8 +15,8 @@
|
||||
Autoplay({
|
||||
delay: 5000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true
|
||||
})
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
];
|
||||
|
||||
function onInit(event: CustomEvent) {
|
||||
@@ -26,57 +29,77 @@
|
||||
const slideNext = () => emblaApi?.scrollNext();
|
||||
</script>
|
||||
|
||||
{#if coverThemes.length > 0}
|
||||
{#if slides.length > 0}
|
||||
<div class="relative w-full overflow-clip rounded-xl transition-opacity" transition:fade>
|
||||
<div
|
||||
class="w-full aspect-[5/1] max-h-[500px]"
|
||||
use:emblaCarouselSvelte={{ options, plugins }}
|
||||
<div
|
||||
class="w-full aspect-[5/1] max-h-[500px]"
|
||||
use:emblaCarouselSvelte={{ options, plugins }}
|
||||
onemblaInit={onInit}
|
||||
>
|
||||
<div class="flex">
|
||||
{#each coverThemes as theme}
|
||||
{#each slides as slide (slide.imageUrl + slide.title + (slide.subtitle ?? ''))}
|
||||
<div
|
||||
class="relative flex-[0_0_100%] cursor-pointer rounded-xl overflow-clip"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => { if (e.key === 'Enter') setDisplayTheme(theme) }}
|
||||
onclick={() => setDisplayTheme(theme)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') setDisplayTheme(slide.openTheme);
|
||||
}}
|
||||
onclick={() => setDisplayTheme(slide.openTheme)}
|
||||
>
|
||||
<img src={theme.marqueeImage || theme.coverImage} alt="Theme Preview" class="object-cover w-full h-full" />
|
||||
{#if theme.featured === true}
|
||||
<img src={slide.imageUrl} alt="" class="object-cover w-full h-full" />
|
||||
{#if slide.badgeFeatured === 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" />
|
||||
<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>
|
||||
<div class="absolute bottom-0 left-0 p-8 z-[1]">
|
||||
<h2 class="text-4xl font-bold text-white">{slide.title}</h2>
|
||||
{#if slide.subtitle}
|
||||
<p class="text-lg font-medium text-white/95 mt-1 line-clamp-2">{slide.subtitle}</p>
|
||||
{/if}
|
||||
{#if slide.openTheme.author && !slide.subtitle}
|
||||
<p class="text-sm text-white/90 mt-1 mb-1 line-clamp-1">By {slide.openTheme.author}</p>
|
||||
{/if}
|
||||
{#if slide.openTheme.description && !slide.subtitle}
|
||||
<p class="text-lg text-white line-clamp-3">{slide.openTheme.description}</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>
|
||||
<div class="absolute bottom-0 left-0 w-full h-1/2 to-transparent bg-linear-to-t from-black/80"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<div class='flex absolute right-2 bottom-2 z-10 gap-2'>
|
||||
<button aria-label="Previous" onclick={slidePrev} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||
<div class="flex absolute right-2 bottom-2 z-10 gap-2">
|
||||
<button
|
||||
aria-label="Previous"
|
||||
onclick={slidePrev}
|
||||
type="button"
|
||||
class="flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800 transition-all duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Next" onclick={slideNext} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||
<button
|
||||
aria-label="Next"
|
||||
onclick={slideNext}
|
||||
type="button"
|
||||
class="flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800 transition-all duration-200"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
|
||||
@@ -1,14 +1,54 @@
|
||||
<script lang="ts">
|
||||
import type { Theme } from '@/interface/types/Theme'
|
||||
import {
|
||||
masterGridDisplayDownloadCount,
|
||||
gridCardPreviewImageUrls,
|
||||
} from '@/interface/utils/themeStoreFlavours'
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onMount } from 'svelte';
|
||||
let { theme, onClick, toggleFavorite, isLoggedIn, onRequestSignIn } = $props<{
|
||||
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
let { theme, onClick, toggleFavorite, isLoggedIn, onRequestSignIn, allStoreThemeRows } = $props<{
|
||||
theme: Theme;
|
||||
onClick: () => void;
|
||||
toggleFavorite: (theme: Theme) => void;
|
||||
isLoggedIn: boolean;
|
||||
onRequestSignIn?: () => void;
|
||||
/** Raw API themes (includes hidden slaves) for aggregated master download totals */
|
||||
allStoreThemeRows?: Theme[];
|
||||
}>();
|
||||
|
||||
const displayDownloadCount = $derived(
|
||||
allStoreThemeRows != null
|
||||
? masterGridDisplayDownloadCount(theme, allStoreThemeRows)
|
||||
: (theme.download_count ?? 0),
|
||||
);
|
||||
|
||||
const gridRotatorUrls = $derived(gridCardPreviewImageUrls(theme, allStoreThemeRows));
|
||||
|
||||
/** Mirrors CoverSwiper (featured bar): horizontal slides + autoplay */
|
||||
function prefersReducedMotion(): boolean {
|
||||
return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
/** Read once synchronously where `window` exists so reduced-motion doesn’t briefly mount carousel */
|
||||
let allowSlideAutoplay = $state(!prefersReducedMotion());
|
||||
|
||||
const gridEmblaKey = $derived(gridRotatorUrls.join('|'));
|
||||
|
||||
const gridEmblaOptions = $derived({ loop: gridRotatorUrls.length > 1 });
|
||||
|
||||
const gridEmblaPlugins = $derived.by(() => {
|
||||
if (!allowSlideAutoplay || gridRotatorUrls.length <= 1) return [];
|
||||
return [
|
||||
Autoplay({
|
||||
delay: 2000,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
let menuOpen = $state(false);
|
||||
let menuRef: HTMLDivElement;
|
||||
|
||||
@@ -111,7 +151,7 @@
|
||||
<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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
{(theme.download_count ?? 0).toLocaleString()}
|
||||
{displayDownloadCount.toLocaleString()}
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill={theme.is_favorited ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.5" class="w-3.5 h-3.5">
|
||||
@@ -122,8 +162,40 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></div>
|
||||
<div class='w-full'>
|
||||
<img src={theme.marqueeImage || theme.coverImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
|
||||
</div>
|
||||
{#if gridRotatorUrls.length === 0}
|
||||
<div class="relative w-full h-48 overflow-hidden rounded-md bg-zinc-200 dark:bg-zinc-700" aria-hidden="true"></div>
|
||||
{:else if !allowSlideAutoplay || gridRotatorUrls.length === 1}
|
||||
<div class="relative w-full h-48 overflow-hidden rounded-md">
|
||||
<img
|
||||
src={gridRotatorUrls[0] ?? theme.marqueeImage ?? theme.coverImage}
|
||||
alt=""
|
||||
class="object-cover w-full h-full"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
{#key gridEmblaKey}
|
||||
<div
|
||||
class="relative w-full h-48 overflow-hidden rounded-md"
|
||||
use:emblaCarouselSvelte={{
|
||||
options: gridEmblaOptions,
|
||||
plugins: gridEmblaPlugins,
|
||||
}}
|
||||
>
|
||||
<div class="flex h-full">
|
||||
{#each gridRotatorUrls as url (url)}
|
||||
<div class="relative flex-[0_0_100%] min-w-0 h-full shrink-0">
|
||||
<img
|
||||
src={url}
|
||||
alt=""
|
||||
class="object-cover w-full h-full select-none"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,23 @@
|
||||
import type { Theme } from '@/interface/types/Theme'
|
||||
import ThemeCard from './ThemeCard.svelte';
|
||||
|
||||
let { themes, searchTerm, setDisplayTheme, toggleFavorite, isLoggedIn, onRequestSignIn } = $props<{
|
||||
let {
|
||||
themes,
|
||||
searchTerm,
|
||||
setDisplayTheme,
|
||||
toggleFavorite,
|
||||
isLoggedIn,
|
||||
onRequestSignIn,
|
||||
allStoreThemeRows,
|
||||
} = $props<{
|
||||
themes: Theme[];
|
||||
searchTerm: string;
|
||||
setDisplayTheme: (theme: Theme) => void;
|
||||
toggleFavorite: (theme: Theme) => void;
|
||||
isLoggedIn: boolean;
|
||||
onRequestSignIn?: () => void;
|
||||
/** Raw API list (includes `slave` rows) for master download aggregation */
|
||||
allStoreThemeRows?: Theme[];
|
||||
}>();
|
||||
|
||||
let filteredThemes = $derived(themes.filter((theme: Theme) =>
|
||||
@@ -25,6 +35,7 @@
|
||||
{toggleFavorite}
|
||||
{isLoggedIn}
|
||||
{onRequestSignIn}
|
||||
{allStoreThemeRows}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user