mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
feat(Store): added store page
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<script lang="ts">
|
||||
let { width, height} = $props<{width?: string, height?: string}>()
|
||||
</script>
|
||||
|
||||
<div style="width: {width ? width : '100%'}; height: {height ? height : '100%'}; background: #e0e0e0;" class="animate-pulse"></div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { Theme } from '@/svelte-interface/types/Theme';
|
||||
import { register, type SwiperContainer } from 'swiper/element/bundle';
|
||||
|
||||
let { coverThemes } = $props<{ coverThemes: Theme[] }>();
|
||||
let swiperEl = $state<SwiperContainer | undefined>();
|
||||
|
||||
|
||||
const slidePrev = () => swiperEl?.swiper.slidePrev();
|
||||
const slideNext = () => swiperEl?.swiper.slideNext();
|
||||
|
||||
onMount(() => {
|
||||
register();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if coverThemes.length > 0}
|
||||
<div class="relative w-full transition-opacity rounded-xl overflow-clip" transition:fade>
|
||||
<swiper-container bind:this={swiperEl} slides-per-view="1" space-between="20" loop="true" autoplay="true" disable-on-interaction="false" pause-on-mouse-enter="true" class="w-full aspect-[8/3]">
|
||||
{#each coverThemes as theme, index (index)}
|
||||
<swiper-slide class="relative cursor-pointer rounded-xl overflow-clip">
|
||||
<img src={theme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-full" />
|
||||
<div class='absolute bottom-0 left-0 p-8 z-[1]'>
|
||||
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||
<p class='text-lg text-white'>{theme.description}</p>
|
||||
</div>
|
||||
<div class='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent'></div>
|
||||
</swiper-slide>
|
||||
{/each}
|
||||
</swiper-container>
|
||||
|
||||
<!-- Pagination buttons -->
|
||||
<div class='absolute z-10 flex gap-2 bottom-2 right-2'>
|
||||
<button onclick={slidePrev} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<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 onclick={slideNext} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Theme from '@/svelte-interface/pages/settings/theme.svelte'
|
||||
|
||||
let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>();
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}>
|
||||
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col 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" transition:fade>
|
||||
<div class="absolute z-10 mb-1 text-xl font-bold text-white bottom-1 left-3">
|
||||
{theme.name}
|
||||
</div>
|
||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent'></div>
|
||||
<div class='w-full'>
|
||||
<img src={theme.coverImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { Theme } from '@/svelte-interface/types/Theme'
|
||||
import ThemeCard from './ThemeCard.svelte';
|
||||
import ThemeModal from './ThemeModal.svelte';
|
||||
|
||||
let { themes, searchTerm } = $props<{ themes: Theme[]; searchTerm: string }>();
|
||||
let displayTheme = $state<Theme | null>();
|
||||
|
||||
let filteredThemes = $derived(themes.filter((theme: Theme) =>
|
||||
theme.name.toLowerCase().includes(searchTerm.toLowerCase()) || theme.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
));
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
<ThemeCard theme={theme} onClick={() => displayTheme = theme} />
|
||||
{/each}
|
||||
|
||||
<!-- "Got a Theme Idea?" card -->
|
||||
<a href="https://betterseqta.gitbook.io/betterseqta-docs" class='w-full cursor-pointer'>
|
||||
<div class="bg-zinc-50 h-48 w-full transition-all hover:scale-105 duration-500 relative justify-center items-center group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] rounded-xl overflow-clip border">
|
||||
<div class="text-2xl font-IconFamily">{'\uecb3'}</div>
|
||||
<div class="text-xl font-bold text-center transition-all duration-500 dark:text-white">
|
||||
Got a Theme Idea?
|
||||
<p class="text-lg font-light subtitle">Transform it into a stunning theme!</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{#if filteredThemes.length === 0}
|
||||
<div class="flex flex-col items-center justify-center w-full text-center h-96">
|
||||
<h1 class="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">That doesn't exist! 😭😭😭</h1>
|
||||
<p class="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, we couldn't find the theme you're looking for. Maybe... you could create it?</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if displayTheme}
|
||||
<ThemeModal theme={displayTheme} onClose={() => displayTheme = null} onInstall={() => {}} onRemove={() => {}} />
|
||||
{/if}
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
export let theme;
|
||||
export let currentThemes = [];
|
||||
export let onClose;
|
||||
export let onInstall;
|
||||
export let onRemove;
|
||||
let installing = false;
|
||||
|
||||
// Transitions
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70" on:click={onClose} transition:fade>
|
||||
<div class="w-full max-w-xl h-[95%] p-4 bg-white rounded-t-2xl dark:bg-zinc-800 overflow-scroll" on:click|stopPropagation transition:slide={{ axis: 'y' }}>
|
||||
<div class="relative h-auto">
|
||||
<button class="absolute top-0 right-0 p-2 text-xl font-bold text-gray-600 font-IconFamily dark:text-gray-200" on:click={onClose}>
|
||||
{'\ued8a'}
|
||||
</button>
|
||||
<h2 class="mb-4 text-2xl font-bold">
|
||||
{theme.name}
|
||||
</h2>
|
||||
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover w-full mb-4 rounded-md" />
|
||||
<p class="mb-4 text-gray-700 dark:text-gray-300">
|
||||
{theme.description}
|
||||
</p>
|
||||
{#if currentThemes.includes(theme.id)}
|
||||
<button on:click={() => {installing = true; onRemove(theme.id); installing = false;}} class="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
{#if installing}
|
||||
Removing...
|
||||
{:else}
|
||||
Remove
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => {installing = true; onInstall(theme.id); installing = false;}} class="flex px-4 py-2 mt-4 ml-auto rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
{#if installing}
|
||||
Installing...
|
||||
{:else}
|
||||
Install
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,6 +8,7 @@
|
||||
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
|
||||
import { setTheme } from '@/seqta/ui/themes/setTheme'
|
||||
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
|
||||
import { OpenStorePage } from '@/SEQTA'
|
||||
|
||||
let themes = $state<ThemeList | null>(null);
|
||||
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||
@@ -183,17 +184,16 @@
|
||||
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href={''}
|
||||
target="_blank"
|
||||
<button
|
||||
onclick={() => OpenStorePage()}
|
||||
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
<span class="ml-2">Theme Store</span>
|
||||
</a>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => OpenThemeCreator}
|
||||
onclick={() => OpenThemeCreator()}
|
||||
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
|
||||
@@ -24,4 +24,17 @@ input {
|
||||
&:focus {
|
||||
box-shadow: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { mount } from 'svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export default function renderSvelte(
|
||||
Component: ComponentType,
|
||||
Component: ComponentType | any,
|
||||
mountPoint: ShadowRoot | HTMLElement,
|
||||
props: Record<string, any> = {}
|
||||
) {
|
||||
@@ -19,6 +19,5 @@ export default function renderSvelte(
|
||||
style.setAttribute("type", "text/css");
|
||||
style.innerHTML = styles;
|
||||
mountPoint.appendChild(style);
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="grid border-b border-b-zinc-200/40 place-items-center">
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" />
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" />
|
||||
<button onclick={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-0 bg-zinc-100 dark:bg-zinc-700"></button>
|
||||
<button onclick={openChangelog} class="absolute right-0 w-8 h-8 text-lg rounded-xl font-IconFamily top-1 bg-zinc-100 dark:bg-zinc-700"></button>
|
||||
<button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700">ⓘ</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// Import child components
|
||||
import CoverSwiper from '../components/store/CoverSwiper.svelte';
|
||||
import ThemeGrid from '../components/store/ThemeGrid.svelte';
|
||||
import SkeletonLoader from '../components/SkeletonLoader.svelte';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
import type { Theme } from '../types/Theme'
|
||||
import browser from 'webextension-polyfill'
|
||||
|
||||
// State variables
|
||||
let searchTerm = $state<string>('');
|
||||
let themes = $state<Theme[]>([]);
|
||||
let coverThemes = $state<Theme[]>([]);
|
||||
let loading = $state<boolean>(true);
|
||||
let darkMode = $state<boolean>(false);
|
||||
|
||||
// Fetch themes and initialize app
|
||||
const fetchThemes = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${(new Date()).getTime()}`, { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
themes = data.themes;
|
||||
|
||||
// Shuffle for cover themes
|
||||
const shuffled = [...themes].sort(() => 0.5 - Math.random());
|
||||
coverThemes = shuffled.slice(0, 3);
|
||||
|
||||
loading = false;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch themes', error);
|
||||
setTimeout(fetchThemes, 5000); // Retry after 5 seconds if failure occurs
|
||||
}
|
||||
};
|
||||
|
||||
// On mount
|
||||
onMount(async () => {
|
||||
await fetchThemes();
|
||||
|
||||
darkMode = (await browser.storage.local.get('DarkMode')).DarkMode === 'true';
|
||||
|
||||
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())
|
||||
));
|
||||
</script>
|
||||
|
||||
<div class="w-screen h-screen overflow-y-scroll bg-white {darkMode ? 'dark' : ''}">
|
||||
<div class="bg-zinc-200/50 dark:bg-zinc-900 dark:text-white pt-[4.25rem] h-full px-12">
|
||||
|
||||
<!-- Search Input (optional) -->
|
||||
<div class="px-8 py-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search Themes"
|
||||
bind:value={searchTerm}
|
||||
class="w-full p-2 bg-white border rounded-lg dark:bg-zinc-800 border-zinc-300 dark:border-zinc-600 text-zinc-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
{#if loading}
|
||||
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
<SkeletonLoader width="100%" height="200px" />
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
{#if searchTerm === ''}
|
||||
<CoverSwiper coverThemes={coverThemes} />
|
||||
{/if}
|
||||
|
||||
<!-- ThemeGrid to display filtered themes -->
|
||||
<ThemeGrid themes={filteredThemes} searchTerm={searchTerm} />
|
||||
|
||||
{#if filteredThemes.length === 0 && !loading}
|
||||
<div class="flex flex-col items-center justify-center w-full text-center h-96">
|
||||
<h1 class="mt-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">No results! 😭</h1>
|
||||
<p class="mt-6 text-lg leading-7 text-zinc-600 dark:text-zinc-300">Sorry, no themes match your search. Maybe create one?</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Theme = {
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
marqueeImage: string;
|
||||
id: string;
|
||||
};
|
||||
Reference in New Issue
Block a user