mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
Merge branch 'svelte' of https://github.com/BetterSEQTA/BetterSEQTA-Plus into svelte
This commit is contained in:
@@ -95,7 +95,6 @@
|
||||
"sortablejs": "^1.15.3",
|
||||
"svelte": "5.0.0-next.244",
|
||||
"svelte-hash-router": "^1.0.1",
|
||||
"svelte-motion": "^0.12.2",
|
||||
"swiper": "latest",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"ts-loader": "^9.5.1",
|
||||
|
||||
@@ -37,6 +37,7 @@ import injectedCSS from '@/css/injected.scss?inline'
|
||||
import documentLoadCSS from '@/css/documentload.scss?inline'
|
||||
import renderSvelte from '@/svelte-interface/main'
|
||||
import Settings from '@/svelte-interface/pages/settings.svelte'
|
||||
import { renderStore } from './seqta/ui/renderStore'
|
||||
|
||||
let SettingsClicked = false
|
||||
export let MenuOptionsOpen = false
|
||||
@@ -343,6 +344,44 @@ export function OpenWhatsNewPopup() {
|
||||
})
|
||||
}
|
||||
|
||||
export function OpenStorePage() {
|
||||
const remove = renderStore()
|
||||
/* export function renderStore() {
|
||||
const container = document.querySelector('#container');
|
||||
if (!container) {
|
||||
throw new Error('Container not found');
|
||||
}
|
||||
|
||||
const child = document.createElement('div');
|
||||
child.id = 'store';
|
||||
container!.appendChild(child);
|
||||
|
||||
const shadow = child.attachShadow({ mode: 'open' });
|
||||
const app = renderSvelte(Store, shadow);
|
||||
|
||||
return () => unmount(app)
|
||||
}
|
||||
*/
|
||||
|
||||
// add a close button to the page
|
||||
const closeButton = document.createElement('button')
|
||||
closeButton.id = 'storeclosebutton'
|
||||
closeButton.classList.add('closeButton')
|
||||
closeButton.innerHTML = 'Close'
|
||||
document.body.appendChild(closeButton)
|
||||
|
||||
closeButton.addEventListener('click', () => {
|
||||
closeButton.classList.add('hide')
|
||||
document.getElementById('store')!.classList.add('hide')
|
||||
|
||||
setTimeout(() => {
|
||||
remove()
|
||||
document.body.removeChild(closeButton)
|
||||
document.getElementById('store')!.remove()
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
|
||||
export function OpenAboutPage() {
|
||||
const background = document.createElement('div')
|
||||
background.id = 'whatsnewbk'
|
||||
|
||||
@@ -62,6 +62,58 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
#store {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9998;
|
||||
animation: fadeIn 500ms forwards;
|
||||
animation-delay: 200ms;
|
||||
opacity: 0;
|
||||
|
||||
&.hide {
|
||||
animation: fadeOut 500ms forwards;
|
||||
}
|
||||
}
|
||||
#storeclosebutton {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 9999;
|
||||
animation: fadeIn 500ms forwards;
|
||||
animation-delay: 200ms;
|
||||
opacity: 0;
|
||||
|
||||
&.hide {
|
||||
animation: fadeOut 500ms forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dark #storeclosebutton {
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .resizeBar {
|
||||
background-color: rgb(28 28 30);
|
||||
}
|
||||
@@ -2532,6 +2584,7 @@ li.MessageList__unread___3imtO {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the checkmark when checked */
|
||||
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after {
|
||||
display: block;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import renderSvelte from '@/svelte-interface/main';
|
||||
import Store from '@/svelte-interface/pages/store.svelte'
|
||||
|
||||
import { unmount } from 'svelte'
|
||||
|
||||
export function renderStore() {
|
||||
const container = document.querySelector('#container');
|
||||
if (!container) {
|
||||
throw new Error('Container not found');
|
||||
}
|
||||
|
||||
const child = document.createElement('div');
|
||||
child.id = 'store';
|
||||
container!.appendChild(child);
|
||||
|
||||
const shadow = child.attachShadow({ mode: 'open' });
|
||||
const app = renderSvelte(Store, shadow);
|
||||
|
||||
return () => unmount(app)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
let { onClick, text, ...props } = $props<{ onClick: () => void, text: string, [key: string]: any }>();
|
||||
let { onClick, text } = $props<{ onClick: () => void, text: string, [key: string]: any }>();
|
||||
</script>
|
||||
|
||||
<button onclick={onClick} class='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md' {...props}>
|
||||
<button onclick={onClick} class='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>
|
||||
{text}
|
||||
</button>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { animate as motionAnimate, spring } from 'motion';
|
||||
|
||||
let { initial, animate, exit, transition, children, class: className } = $props<{
|
||||
initial?: any,
|
||||
animate?: any,
|
||||
exit?: any,
|
||||
transition?: any,
|
||||
children?: any,
|
||||
class?: string
|
||||
}>();
|
||||
|
||||
let divElement: HTMLElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const playAnimation = (keyframe: any) => {
|
||||
if (divElement && keyframe) {
|
||||
let animationOptions = transition;
|
||||
|
||||
// If transition is not defined or is of type 'spring', use spring animations
|
||||
if (!transition || transition.type === 'spring') {
|
||||
animationOptions = {
|
||||
easing: spring(transition || { stiffness: 250, damping: 25 }),
|
||||
};
|
||||
}
|
||||
|
||||
const animation = motionAnimate(divElement, keyframe, animationOptions);
|
||||
return animation.finished;
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
// Apply initial state if provided
|
||||
if (initial) {
|
||||
await playAnimation(initial);
|
||||
}
|
||||
// Then animate to the `animate` state
|
||||
if (animate) {
|
||||
await playAnimation(animate);
|
||||
}
|
||||
|
||||
// Dispatch animation end event
|
||||
dispatch('animationend');
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (animate) {
|
||||
playAnimation(animate);
|
||||
}
|
||||
|
||||
dispatch('animationend');
|
||||
});
|
||||
|
||||
// Handle unmounting with the `exit` animation
|
||||
onDestroy(async () => {
|
||||
if (exit) {
|
||||
await playAnimation(exit);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class={className} bind:this={divElement} style="will-change: transform, opacity;">
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
// @ts-expect-error umm idk
|
||||
import { MotionDiv } from 'svelte-motion';
|
||||
import MotionDiv from './MotionDiv.svelte';
|
||||
import './TabbedContainer.css';
|
||||
import type { Component } from 'svelte'
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { tabs } = $props<{ tabs: { title: string, Content: Component }[] }>();
|
||||
let { tabs } = $props<{ tabs: { title: string, Content: any }[] }>();
|
||||
let activeTab = $state(0);
|
||||
let hoveredTab = $state<number | null>(null);
|
||||
let containerRef: HTMLElement | null = null;
|
||||
@@ -72,11 +70,11 @@
|
||||
animate={{ x: `${-activeTab * 100}%` }}
|
||||
transition={springTransition}
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="flex h-screen">
|
||||
{#each tabs as { Content }, index}
|
||||
<div class="absolute w-full transition-opacity duration-300 overflow-y-scroll {activeTab === index ? 'opacity-100' : 'opacity-0'}"
|
||||
style="left: {index * 100}%;">
|
||||
<Content />
|
||||
{@render Content()}
|
||||
</div>
|
||||
{/each}
|
||||
</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>
|
||||
|
||||
@@ -25,3 +25,16 @@ input {
|
||||
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>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Switch from "../../components/Switch.svelte"
|
||||
import Button from "../../components/Button.svelte"
|
||||
//import PickerSwatch from "../../components/PickerSwatch.svelte"
|
||||
import PickerSwatch from "../../components/PickerSwatch.svelte"
|
||||
import Slider from "../../components/Slider.svelte"
|
||||
|
||||
import browser from "webextension-polyfill"
|
||||
@@ -100,7 +100,7 @@
|
||||
onChange: (isOn: boolean) => settingsState.onoff = isOn
|
||||
}
|
||||
}
|
||||
] as setting}
|
||||
{@render Setting(setting)}
|
||||
] as option}
|
||||
{@render Setting(option)}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
// @ts-expect-error umm idk
|
||||
import { MotionDiv } from 'svelte-motion';
|
||||
import MotionDiv from '@/svelte-interface/components/MotionDiv.svelte';
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||
import Switch from "@/svelte-interface/components/Switch.svelte"
|
||||
|
||||
|
||||
@@ -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