mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 09:11:06 +00:00
feat: refine settings, stores, and message folders
This commit is contained in:
+2
-38
@@ -162,40 +162,6 @@ select option {
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .resizeBar {
|
||||
background-color: rgb(28 28 30);
|
||||
}
|
||||
@@ -1817,12 +1783,10 @@ html.transparencyEffects
|
||||
-webkit-box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0px 5px 16px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
/* Not named `.border` — Tailwind’s utility of that name is injected into the
|
||||
page via content-script CSS in production and would paint a 1px outline. */
|
||||
.bsplus-rounded {
|
||||
/* `.border` is also a public hook used by existing themes. */
|
||||
.home-container .border {
|
||||
border-radius: 16px;
|
||||
overflow: clip;
|
||||
border-width: 0;
|
||||
}
|
||||
.shortcut-container h2 {
|
||||
font-size: 20px;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
let { onclick, label = "Close" } = $props<{
|
||||
onclick: () => void;
|
||||
label?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
{onclick}
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center text-zinc-500 transition-[color,transform] duration-150 hover:text-zinc-950 active:scale-[0.96] focus:outline-none dark:text-zinc-400 dark:hover:text-white"
|
||||
aria-label={label}
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 6l12 12M18 6 6 18" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Select from "@/interface/components/Select.svelte";
|
||||
import Slider from "@/interface/components/Slider.svelte";
|
||||
import Switch from "@/interface/components/Switch.svelte";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import {
|
||||
DEFAULT_SIDEBAR_BLUR,
|
||||
@@ -187,16 +188,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" class:disabled={!transparencyOn}>
|
||||
<div class="row">
|
||||
<div class="copy">
|
||||
<h3 class="row-title">Transparency Effects</h3>
|
||||
<p class="row-desc">Use glass effects on supported surfaces (may impact battery life)</p>
|
||||
</div>
|
||||
<div class="control toggle">
|
||||
<Switch
|
||||
state={transparencyOn}
|
||||
onChange={(value) => (settingsState.transparencyEffects = value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if transparencyOn}
|
||||
<div class="row">
|
||||
<div class="copy">
|
||||
<h3 class="row-title">Blur Strength</h3>
|
||||
<p class="row-desc">
|
||||
{#if transparencyOn}
|
||||
Glass blur on the sidebar ({blur}px)
|
||||
{:else}
|
||||
Enable Transparency Effects to use blur
|
||||
{/if}
|
||||
</p>
|
||||
<p class="row-desc">Glass blur on the sidebar ({blur}px)</p>
|
||||
</div>
|
||||
<div class="control slider">
|
||||
<Slider
|
||||
@@ -208,6 +217,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -538,10 +548,6 @@
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.row.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.copy {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
@@ -574,4 +580,9 @@
|
||||
min-width: 10rem;
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
.control.toggle {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import SidebarNavItem from "./SidebarNavItem.svelte";
|
||||
|
||||
type NavGroup = {
|
||||
label?: string;
|
||||
items: {
|
||||
id: string;
|
||||
label: string;
|
||||
divided?: boolean;
|
||||
nested?: boolean;
|
||||
expanded?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
let { groups, selectedId, onselect } = $props<{
|
||||
groups: NavGroup[];
|
||||
selectedId: string;
|
||||
onselect: (id: string) => void;
|
||||
}>();
|
||||
|
||||
let trackEl = $state<HTMLElement | null>(null);
|
||||
let indicatorX = $state(0);
|
||||
let indicatorY = $state(0);
|
||||
let indicatorW = $state(0);
|
||||
let indicatorH = $state(40);
|
||||
let indicatorReady = $state(false);
|
||||
|
||||
const updateIndicator = () => {
|
||||
if (!trackEl) return;
|
||||
const item = trackEl.querySelector<HTMLElement>(`[data-nav-section="${selectedId}"]`);
|
||||
if (!item) return;
|
||||
const trackRect = trackEl.getBoundingClientRect();
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
indicatorX = itemRect.left - trackRect.left;
|
||||
indicatorY = itemRect.top - trackRect.top + trackEl.scrollTop;
|
||||
indicatorW = itemRect.width;
|
||||
indicatorH = itemRect.height;
|
||||
indicatorReady = true;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
selectedId;
|
||||
groups;
|
||||
trackEl;
|
||||
queueMicrotask(updateIndicator);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative flex min-h-0 flex-1 flex-col gap-5 overflow-y-auto no-scrollbar"
|
||||
bind:this={trackEl}
|
||||
>
|
||||
{#if indicatorReady}
|
||||
<div
|
||||
class="pointer-events-none absolute left-0 top-0 z-0 rounded-lg bg-zinc-200/90 dark:bg-zinc-700/90"
|
||||
style="transform: translate({indicatorX}px, {indicatorY}px); width: {indicatorW}px; height: {indicatorH}px; transition: {$settingsState.animations
|
||||
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), width 0.28s cubic-bezier(0.22, 1, 0.36, 1), height 0.28s cubic-bezier(0.22, 1, 0.36, 1)'
|
||||
: 'none'};"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#each groups as group}
|
||||
<div class="relative flex flex-col gap-1">
|
||||
{#if group.label}
|
||||
<p
|
||||
class="relative z-10 mb-1.5 px-3 text-sm font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
{group.label}
|
||||
</p>
|
||||
{/if}
|
||||
{#each group.items as item (item.id)}
|
||||
{#if item.divided}
|
||||
<div class="my-2 border-t border-zinc-200 dark:border-zinc-700 {item.nested ? 'ml-4' : ''}"></div>
|
||||
{/if}
|
||||
<SidebarNavItem
|
||||
label={item.label}
|
||||
active={selectedId === item.id}
|
||||
nested={item.nested}
|
||||
emphasized={item.expanded}
|
||||
trackId={item.id}
|
||||
onclick={() => onselect(item.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
label,
|
||||
active = false,
|
||||
nested = false,
|
||||
emphasized = false,
|
||||
trackId,
|
||||
onclick,
|
||||
} = $props<{
|
||||
label: string;
|
||||
active?: boolean;
|
||||
nested?: boolean;
|
||||
emphasized?: boolean;
|
||||
trackId?: string;
|
||||
onclick: () => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-nav-section={trackId}
|
||||
{onclick}
|
||||
class="relative z-10 rounded-lg text-left transition-[color,background-color] duration-200
|
||||
{nested
|
||||
? 'ml-4 w-[calc(100%-1rem)] px-3 py-2 text-base'
|
||||
: 'w-full px-3 py-2.5 text-lg'}
|
||||
{active || emphasized
|
||||
? 'font-medium text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-300 dark:hover:text-white'}"
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
@@ -1,3 +0,0 @@
|
||||
.tab-width {
|
||||
width: var(--tab-width);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import MotionDiv from './MotionDiv.svelte';
|
||||
import './TabbedContainer.css';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { tabs, activeTab = $bindable(0) } = $props<{ tabs: { title: string, Content: any, props?: any }[]; activeTab?: number }>();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.bsplus-font-picker-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50000;
|
||||
z-index: 200001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
type Background = { id: string; category: string; type: string; lowResUrl: string; highResUrl: string; name: string; description: string; featured?: boolean };
|
||||
let { searchTerm } = $props<{ searchTerm: string }>();
|
||||
let { searchTerm, selectedCategory, onCategoriesChange } = $props<{
|
||||
searchTerm: string;
|
||||
selectedCategory: string;
|
||||
onCategoriesChange: (categories: string[]) => void;
|
||||
}>();
|
||||
|
||||
// Existing states
|
||||
let backgrounds = $state<Background[]>([]);
|
||||
let selectedCategory = $state<string>('All');
|
||||
let error = $state<string | null>(null);
|
||||
let selectedBackground = $state<string | null>(null);
|
||||
let isLoading = $state<boolean>(true);
|
||||
@@ -33,6 +36,7 @@
|
||||
}
|
||||
const data = await response.json();
|
||||
backgrounds = data.backgrounds;
|
||||
onCategoriesChange([...new Set(backgrounds.map((background) => background.category))]);
|
||||
debugInfo = `Loaded ${backgrounds.length} backgrounds`;
|
||||
await loadSavedBackgrounds();
|
||||
} catch (e) {
|
||||
@@ -97,7 +101,6 @@
|
||||
})());
|
||||
|
||||
let categories = $derived([...new Set(backgrounds.map(bg => bg.category))]);
|
||||
|
||||
// Background management functions
|
||||
async function saveBackgroundFromUrl(url: string, id: string, fileType: string): Promise<void> {
|
||||
try {
|
||||
@@ -162,41 +165,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="p-4 w-64 h-full border-r border-zinc-200 dark:border-zinc-700">
|
||||
<div class="mb-8">
|
||||
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
|
||||
<nav class="space-y-2">
|
||||
<button
|
||||
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'All' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||
onclick={() => selectedCategory = 'All'}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === 'Featured' ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||
onclick={() => selectedCategory = 'Featured'}
|
||||
>
|
||||
Featured
|
||||
</button>
|
||||
|
||||
<div class="my-2 border-b border-zinc-200 dark:border-zinc-700"></div>
|
||||
|
||||
{#each categories as category}
|
||||
<button
|
||||
class={`w-full px-4 py-2 text-left bg-transparent rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition ${selectedCategory === category ? 'bg-blue-100 dark:bg-zinc-800' : ''}`}
|
||||
onclick={() => selectedCategory = category}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="overflow-auto flex-1">
|
||||
<div class="h-full overflow-auto">
|
||||
<!-- Header -->
|
||||
<div class="sticky top-0 z-10 p-4 border-b bg-[#F1F1F3] dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
@@ -294,7 +263,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if settingsState.devMode}
|
||||
@@ -307,4 +275,3 @@
|
||||
<p>Selected Category: {selectedCategory}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleButtonClick}
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-[0.75rem] rounded-lg shadow-2xl border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 dark:text-white transition-colors duration-200"
|
||||
class="flex h-12 items-center gap-2 rounded-full bg-zinc-100/80 px-4 text-sm font-medium text-zinc-700 transition-colors duration-150 hover:bg-zinc-200 hover:text-zinc-950 focus:outline-none dark:bg-zinc-900/50 dark:text-zinc-200 dark:hover:bg-zinc-700 dark:hover:text-white"
|
||||
>
|
||||
{#if cloudState.isLoggedIn}
|
||||
{#if cloudState.user?.pfpUrl}
|
||||
@@ -85,14 +85,14 @@
|
||||
{/if}
|
||||
<span
|
||||
class={alwaysShowUserName
|
||||
? "inline max-w-[10rem] truncate text-[0.75rem]"
|
||||
: "hidden max-w-24 truncate sm:inline text-[0.75rem]"}
|
||||
? "inline max-w-[10rem] truncate"
|
||||
: "hidden max-w-24 truncate sm:inline"}
|
||||
>
|
||||
{cloudState.user?.displayName || cloudState.user?.username || cloudState.user?.email || "User"}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-sm font-IconFamily" aria-hidden="true">{'\ued53'}</span>
|
||||
<span class="text-[0.75rem]">Sign in</span>
|
||||
<span>Sign in</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
</script>
|
||||
|
||||
{#if slides.length > 0}
|
||||
<div class="relative w-full overflow-clip rounded-xl transition-opacity" transition:fade>
|
||||
<div class="relative w-full overflow-clip rounded-xl ring-1 ring-black/10 transition-opacity dark:ring-white/10" transition:fade>
|
||||
<div
|
||||
class="w-full aspect-[5/1] max-h-[500px]"
|
||||
class="w-full overflow-hidden"
|
||||
use:emblaCarouselSvelte={{ options, plugins }}
|
||||
onemblaInit={onInit}
|
||||
>
|
||||
<div class="flex">
|
||||
{#each slides as slide (slide.imageUrl + slide.title + (slide.subtitle ?? ''))}
|
||||
<div
|
||||
class="relative flex-[0_0_100%] cursor-pointer rounded-xl overflow-clip"
|
||||
class="cover-slide relative isolate min-w-0 flex-[0_0_100%] cursor-pointer overflow-hidden"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => {
|
||||
@@ -51,7 +51,7 @@
|
||||
}}
|
||||
onclick={() => setDisplayTheme(slide.openTheme)}
|
||||
>
|
||||
<img src={slide.imageUrl} alt="" class="object-cover w-full h-full" />
|
||||
<img src={slide.imageUrl} alt="" class="absolute inset-0 z-0 h-full w-full object-cover" />
|
||||
{#if slide.badgeFeatured === true}
|
||||
<div class="absolute top-4 left-4 z-[2] pointer-events-none">
|
||||
<span
|
||||
@@ -69,7 +69,7 @@
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="absolute bottom-0 left-0 p-8 z-[1]">
|
||||
<div class="absolute bottom-0 left-0 z-[2] p-8">
|
||||
<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>
|
||||
@@ -81,7 +81,6 @@
|
||||
<p class="text-lg text-white line-clamp-3">{slide.openTheme.description}</p>
|
||||
{/if}
|
||||
</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>
|
||||
@@ -111,3 +110,26 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.cover-slide {
|
||||
aspect-ratio: 3 / 1;
|
||||
}
|
||||
|
||||
.cover-slide::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgb(0 0 0 / 46%) 0%,
|
||||
rgb(0 0 0 / 42.5%) 18%,
|
||||
rgb(0 0 0 / 34%) 36%,
|
||||
rgb(0 0 0 / 21%) 54%,
|
||||
rgb(0 0 0 / 8%) 72%,
|
||||
transparent 88%
|
||||
);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,77 +1,120 @@
|
||||
<script lang="ts">
|
||||
import logo from '@/resources/icons/betterseqta-dark-full.png';
|
||||
import logoDark from '@/resources/icons/betterseqta-light-full.png';
|
||||
import browser from 'webextension-polyfill';
|
||||
import CloudHeader from './CloudHeader.svelte';
|
||||
import darkLogo from "@/resources/icons/betterseqta-dark-full.png";
|
||||
import lightLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||
import MotionDiv from "../MotionDiv.svelte";
|
||||
import PlainCloseButton from "../PlainCloseButton.svelte";
|
||||
import CloudHeader from "./CloudHeader.svelte";
|
||||
|
||||
const handleCloseStore = () => {
|
||||
void import('@/seqta/ui/renderStore').then((module) => module.closeStore());
|
||||
};
|
||||
type SettingsPage = "settings" | "themes" | "backgrounds";
|
||||
|
||||
// Props
|
||||
let { searchTerm, setSearchTerm, darkMode, activeTab, setActiveTab } = $props<{
|
||||
searchTerm: string,
|
||||
setSearchTerm: (term: string) => void,
|
||||
darkMode: boolean,
|
||||
activeTab: string,
|
||||
setActiveTab: (tab: string) => void
|
||||
let {
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
activePage,
|
||||
setActivePage,
|
||||
showStoreTools,
|
||||
onLogoClick,
|
||||
onClose,
|
||||
} = $props<{
|
||||
searchTerm: string;
|
||||
setSearchTerm: (term: string) => void;
|
||||
activePage: SettingsPage;
|
||||
setActivePage: (page: SettingsPage) => void;
|
||||
showStoreTools: boolean;
|
||||
onLogoClick: () => void;
|
||||
onClose: () => void;
|
||||
}>();
|
||||
|
||||
// Clear search input function
|
||||
const clearSearch = () => {
|
||||
setSearchTerm('');
|
||||
};
|
||||
const tabs: { id: SettingsPage; title: string }[] = [
|
||||
{ id: "settings", title: "Settings" },
|
||||
{ id: "themes", title: "Themes" },
|
||||
{ id: "backgrounds", title: "Backgrounds" },
|
||||
];
|
||||
|
||||
const activeIndex = $derived(tabs.findIndex((tab) => tab.id === activePage));
|
||||
</script>
|
||||
|
||||
<header class="fixed top-0 z-50 w-full h-[4.25rem] bg-white border-b shadow-md border-b-white/10 dark:bg-zinc-950/90 backdrop-blur-xl dark:text-white">
|
||||
<div class="flex justify-between items-center px-4 py-1">
|
||||
<div class="flex gap-4 place-items-center cursor-pointer" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
|
||||
<img src={browser.runtime.getURL(logo)} class="h-14 {darkMode ? 'hidden' : ''}" alt="Logo" />
|
||||
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
|
||||
|
||||
<div class="w-[1px] h-10 my-auto bg-zinc-400 dark:bg-zinc-600"></div>
|
||||
|
||||
<button
|
||||
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'themes' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
|
||||
onclick={() => setActiveTab('themes')}
|
||||
>
|
||||
Themes
|
||||
<header
|
||||
class="flex shrink-0 items-center gap-4 border-b border-zinc-200/60 px-5 py-4 dark:border-zinc-700/50"
|
||||
>
|
||||
<button type="button" class="hidden shrink-0 lg:block" onclick={onLogoClick}>
|
||||
<img
|
||||
src={resolveExtensionAssetUrl(darkLogo)}
|
||||
class="h-11 w-52 object-cover dark:hidden"
|
||||
alt="BetterSEQTA+"
|
||||
/>
|
||||
<img
|
||||
src={resolveExtensionAssetUrl(lightLogo)}
|
||||
class="hidden h-11 w-52 object-cover dark:block"
|
||||
alt="BetterSEQTA+"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 font-semibold text-lg transition-colors duration-200 {activeTab === 'backgrounds' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-600 dark:text-gray-300 hover:text-blue-500 dark:hover:text-blue-400'}"
|
||||
onclick={() => setActiveTab('backgrounds')}
|
||||
|
||||
<div
|
||||
class="h-12 min-w-[14rem] flex-1 rounded-full bg-zinc-100/80 p-1 dark:bg-zinc-900/50"
|
||||
role="tablist"
|
||||
aria-label="Settings pages"
|
||||
>
|
||||
Backgrounds
|
||||
<div class="relative flex">
|
||||
<MotionDiv
|
||||
class="absolute inset-y-0 left-0 z-0 w-1/3 rounded-full bg-white shadow-sm dark:bg-zinc-700"
|
||||
animate={{ x: `${activeIndex * 100}%` }}
|
||||
transition={{ type: "spring", stiffness: 250, damping: 25 }}
|
||||
/>
|
||||
{#each tabs as tab (tab.id)}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activePage === tab.id}
|
||||
onclick={() => setActivePage(tab.id)}
|
||||
class="relative z-10 h-10 flex-1 rounded-full px-4 text-base transition-colors duration-200
|
||||
{activePage === tab.id
|
||||
? 'font-semibold text-zinc-900 dark:text-white'
|
||||
: 'text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}"
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
{#if showStoreTools}
|
||||
<label class="relative hidden w-40 md:block xl:w-56">
|
||||
<span class="sr-only">Search {activePage}</span>
|
||||
<svg
|
||||
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.35-4.35m1.35-5.65a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
placeholder={activePage === "themes" ? "Search themes" : "Search backgrounds"}
|
||||
value={searchTerm}
|
||||
oninput={(event) => setSearchTerm(event.currentTarget.value)}
|
||||
class="store-search h-12 w-full rounded-full bg-zinc-100/80 pl-9 pr-4 text-sm text-zinc-900 transition-colors duration-150 placeholder:text-zinc-400 focus:bg-zinc-200/70 dark:bg-zinc-900/50 dark:text-white dark:focus:bg-zinc-700"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<CloudHeader />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex relative gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search themes..."
|
||||
value={searchTerm}
|
||||
oninput={(e: any) => setSearchTerm(e.target.value)}
|
||||
class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" />
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 w-5 h-5 text-gray-400 transform -translate-y-1/2 dark:text-gray-200"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
onclick={handleCloseStore}
|
||||
class="p-1 px-3"
|
||||
>
|
||||
<span class="text-2xl font-IconFamily"></span>
|
||||
</button>
|
||||
</div>
|
||||
<PlainCloseButton onclick={onClose} label="Close settings" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.store-search,
|
||||
.store-search:focus,
|
||||
.store-search:focus-visible {
|
||||
appearance: none;
|
||||
border: 0 !important;
|
||||
outline: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
masterGridDisplayDownloadCount,
|
||||
gridCardPreviewImageUrls,
|
||||
} from '@/interface/utils/themeStoreFlavours'
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onMount } from 'svelte';
|
||||
import emblaCarouselSvelte from 'embla-carousel-svelte';
|
||||
import Autoplay from 'embla-carousel-autoplay';
|
||||
let { theme, onClick, toggleFavorite, isLoggedIn, onRequestSignIn, allStoreThemeRows } = $props<{
|
||||
@@ -49,21 +47,8 @@
|
||||
];
|
||||
});
|
||||
|
||||
let menuOpen = $state(false);
|
||||
let menuRef: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
const closeMenu = (e: MouseEvent) => {
|
||||
if (menuOpen && menuRef && !menuRef.contains(e.target as Node)) {
|
||||
menuOpen = false;
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', closeMenu);
|
||||
return () => document.removeEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
function handleCardClick(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).closest('[data-theme-menu]')) return;
|
||||
if ((e.target as HTMLElement).closest('[data-theme-favorite]')) return;
|
||||
onClick();
|
||||
}
|
||||
|
||||
@@ -74,7 +59,6 @@
|
||||
} else {
|
||||
onRequestSignIn?.();
|
||||
}
|
||||
menuOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -86,8 +70,7 @@
|
||||
onclick={handleCardClick}
|
||||
>
|
||||
<div
|
||||
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
|
||||
class="theme-card isolate bg-gray-50 w-full transition-[transform,box-shadow,border-color] duration-300 ease-out relative group flex flex-col rounded-xl overflow-clip border hover:scale-[1.02] hover:shadow-xl dark:hover:shadow-black/60 dark:bg-zinc-800 dark:border-white/[0.1] h-auto"
|
||||
>
|
||||
{#if theme.featured === true}
|
||||
<div class="absolute top-2 left-2 z-20 pointer-events-none">
|
||||
@@ -102,29 +85,21 @@
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Menu dropdown -->
|
||||
<div class="absolute top-2 right-2 z-20" data-theme-menu bind:this={menuRef}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex justify-center items-center w-8 h-8 rounded-lg bg-black/40 hover:bg-black/60 text-white transition-all"
|
||||
onclick={(e) => { e.stopPropagation(); menuOpen = !menuOpen; }}
|
||||
aria-label="Theme options"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" class="w-5 h-5">
|
||||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="absolute right-0 top-full mt-1 py-1 min-w-[140px] rounded-lg bg-white dark:bg-zinc-800 shadow-lg border border-zinc-200 dark:border-zinc-700"
|
||||
role="menu"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex gap-2 items-center w-full px-3 py-2 text-left text-sm hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
||||
role="menuitem"
|
||||
data-theme-favorite
|
||||
class="pointer-events-none absolute right-2 top-2 z-20 flex h-10 w-10 scAle-[0.25] items-center justify-center rounded-full bg-black/50 text-white opacity-0 blur-[4px] transition-[opacity,transform,filter,background-color] duration-200 ease-[cubic-bezier(0.2,0,0,1)] hover:bg-black/70 active:scale-[0.96] group-hover:pointer-events-auto group-hover:scale-100 group-hover:opacity-100 group-hover:blur-0 focus-visible:pointer-events-auto focus-visible:scale-100 focus-visible:opacity-100 focus-visible:blur-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||||
onclick={handleFavoriteClick}
|
||||
title={isLoggedIn ? (theme.is_favorited ? 'Remove from favorites' : 'Add to favorites') : 'Sign in to favorite themes'}
|
||||
aria-label={isLoggedIn
|
||||
? theme.is_favorited
|
||||
? 'Remove from favourites'
|
||||
: 'Add to favourites'
|
||||
: 'Sign in to favourite themes'}
|
||||
title={isLoggedIn
|
||||
? theme.is_favorited
|
||||
? 'Remove from favourites'
|
||||
: 'Add to favourites'
|
||||
: 'Sign in to favourite themes'}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -132,15 +107,12 @@
|
||||
fill={theme.is_favorited ? 'currentColor' : 'none'}
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="w-5 h-5 {theme.is_favorited ? 'text-red-500' : ''}"
|
||||
class="h-5 w-5 {theme.is_favorited ? 'text-red-500' : ''}"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 0 1 6.364 0L12 7.636l1.318-1.318a4.5 4.5 0 0 1 6.364 6.364L12 20.364l-7.682-7.682a4.5 4.5 0 0 1 0-6.364Z" />
|
||||
</svg>
|
||||
{theme.is_favorited ? 'Favorited' : 'Favorite'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</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}
|
||||
@@ -161,7 +133,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></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}
|
||||
@@ -199,3 +170,22 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-card::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgb(0 0 0 / 92%) 0%,
|
||||
rgb(0 0 0 / 85%) 18%,
|
||||
rgb(0 0 0 / 68%) 36%,
|
||||
rgb(0 0 0 / 42%) 54%,
|
||||
rgb(0 0 0 / 16%) 72%,
|
||||
transparent 88%
|
||||
);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</script>
|
||||
|
||||
<div class="relative" >
|
||||
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-4 py-6 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
<ThemeCard
|
||||
{theme}
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
{#if filteredThemes.length !== 0}
|
||||
<a href="https://docs.betterseqta.org/theme-creation/" class="block relative z-0 hover:z-20 w-full cursor-pointer">
|
||||
<div class="bg-zinc-50 h-48 w-full transition-all duration-500 ease-out relative overflow-clip rounded-xl border group group/card flex flex-col justify-center items-center 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]">
|
||||
<div class="bg-zinc-50 h-48 w-full transition-[transform,box-shadow,border-color] duration-300 ease-out relative overflow-clip rounded-xl border group group/card flex flex-col justify-center items-center hover:scale-[1.02] hover:shadow-xl dark:hover:shadow-black/60 dark:bg-zinc-800 dark:border-white/[0.1]">
|
||||
<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?
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
let themes = $state<ThemeList | null>(null);
|
||||
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||
let { isEditMode, showNavigation = true } = $props<{
|
||||
isEditMode: boolean;
|
||||
showNavigation?: boolean;
|
||||
}>();
|
||||
let isDragging = $state(false);
|
||||
let tempTheme = $state(null);
|
||||
let favoriteStatus = $state<Record<string, boolean>>({});
|
||||
@@ -128,7 +131,6 @@
|
||||
|
||||
const openStorePage = async () => {
|
||||
const { OpenStorePage } = await import('@/seqta/ui/renderStore')
|
||||
closeExtensionPopup()
|
||||
await OpenStorePage()
|
||||
}
|
||||
|
||||
@@ -166,7 +168,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="pt-5 mb-1 w-full"
|
||||
class="pt-5 mb-1 w-full max-w-lg mx-auto"
|
||||
role="list"
|
||||
tabindex="-1"
|
||||
ondragover={handleDragOver}
|
||||
@@ -188,7 +190,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="pb-2 text-lg font-bold">Themes</h2>
|
||||
<div class="flex flex-col gap-2 px-2">
|
||||
{#if themes}
|
||||
{#each themes.themes as theme (theme.id)}
|
||||
@@ -267,10 +268,38 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if themes && themes.themes.length > 0}
|
||||
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
|
||||
{#if themes && themes.themes.length === 0 && !tempTheme}
|
||||
<div class="flex min-h-56 flex-col items-center justify-center px-4 py-10 text-center">
|
||||
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white" style="text-wrap: balance">
|
||||
No themes installed
|
||||
</h3>
|
||||
<p class="mt-2 max-w-md text-base text-zinc-500 dark:text-zinc-400" style="text-wrap: pretty">
|
||||
Install a theme from the store or create one of your own.
|
||||
</p>
|
||||
<div class="mt-6 grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => void openStorePage()}
|
||||
class="flex h-11 items-center justify-center gap-2 rounded-lg bg-zinc-200 px-4 font-medium text-zinc-900 transition-[background-color,color,transform] duration-150 hover:bg-zinc-300 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600"
|
||||
>
|
||||
<span class="font-IconFamily text-lg" aria-hidden="true"></span>
|
||||
Install a theme
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => void openThemeCreator()}
|
||||
class="flex h-11 items-center justify-center gap-2 rounded-lg bg-zinc-200 px-4 font-medium text-zinc-900 transition-[background-color,color,transform] duration-150 hover:bg-zinc-300 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:bg-zinc-700 dark:text-white dark:hover:bg-zinc-600"
|
||||
>
|
||||
<span class="font-IconFamily text-lg" aria-hidden="true"></span>
|
||||
Create a theme
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showNavigation && themes && themes.themes.length > 0}
|
||||
<div id="divider" class="w-full h-[1px] my-2 bg-zinc-100 dark:bg-zinc-600"></div>
|
||||
|
||||
<button
|
||||
onclick={() => void openStorePage()}
|
||||
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
@@ -286,6 +315,7 @@
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
<span class="ml-2">Create your own</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "./index.css";
|
||||
import Settings from "./pages/settings.svelte";
|
||||
import IconFamily from "@/resources/fonts/IconFamily.woff";
|
||||
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||
|
||||
@@ -16,6 +16,14 @@ export default function renderSvelte(
|
||||
props: Record<string, any> = {},
|
||||
shadowStyle: ShadowStyleVariant = "settings",
|
||||
) {
|
||||
if (mountPoint instanceof ShadowRoot || props.standalone === true) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.textContent = shadowStyles[shadowStyle];
|
||||
(mountPoint instanceof ShadowRoot ? mountPoint : document.head).appendChild(
|
||||
styleElement,
|
||||
);
|
||||
}
|
||||
|
||||
const app = mount(Component, {
|
||||
target: mountPoint,
|
||||
props: {
|
||||
@@ -24,11 +32,5 @@ export default function renderSvelte(
|
||||
},
|
||||
});
|
||||
|
||||
if (mountPoint instanceof ShadowRoot) {
|
||||
const styleElement = document.createElement("style");
|
||||
styleElement.textContent = shadowStyles[shadowStyle];
|
||||
mountPoint.appendChild(styleElement);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
+258
-228
@@ -2,7 +2,11 @@
|
||||
import Settings from "./settings/general.svelte";
|
||||
import Shortcuts from "./settings/shortcuts.svelte";
|
||||
import Theme from "./settings/theme.svelte";
|
||||
import browser from "webextension-polyfill";
|
||||
import Store from "./store.svelte";
|
||||
import TabbedContainer from "../components/TabbedContainer.svelte";
|
||||
import darkLogo from "@/resources/icons/betterseqta-dark-full.png";
|
||||
import lightLogo from "@/resources/icons/betterseqta-light-full.png";
|
||||
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
||||
|
||||
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
@@ -17,8 +21,15 @@
|
||||
import CloudPanel from "../components/CloudPanel.svelte";
|
||||
import DisclaimerModal from "../components/DisclaimerModal.svelte";
|
||||
import FeedbackModal from "../components/FeedbackModal.svelte";
|
||||
import SidebarNav from "../components/SidebarNav.svelte";
|
||||
import StoreHeader from "../components/store/Header.svelte";
|
||||
import { settingsPopup } from "@/seqta/utils/settingsPopup";
|
||||
import { consumeOpenFeedbackRequest } from "@/seqta/utils/feedback/client";
|
||||
import {
|
||||
consumeSettingsDestination,
|
||||
SETTINGS_NAVIGATION_EVENT,
|
||||
type SettingsDestination,
|
||||
} from "@/seqta/utils/settingsNavigation";
|
||||
import {
|
||||
checkGithubReleaseUpdate,
|
||||
dismissNightlyUpdate,
|
||||
@@ -26,46 +37,30 @@
|
||||
isGhReleaseUpdateCheckEnabled,
|
||||
type GhReleaseUpdateInfo,
|
||||
} from "@/utils/githubReleaseUpdate";
|
||||
type PageId = "settings" | "shortcuts" | "themes";
|
||||
type PageId = "settings" | "themes" | "backgrounds";
|
||||
type StoreTab = "themes" | "backgrounds";
|
||||
type ThemeView = "theme-settings" | "theme-store";
|
||||
type BackgroundView = "background-settings" | "background-store";
|
||||
|
||||
type NavItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
divided?: boolean;
|
||||
nested?: boolean;
|
||||
expanded?: boolean;
|
||||
};
|
||||
|
||||
const BACKGROUND_CATEGORY_PREFIX = "background-category:";
|
||||
|
||||
let devModeSequence = "";
|
||||
let compactActiveTab = $state(0);
|
||||
let activePage = $state<PageId>("settings");
|
||||
let activeSection = $state("general");
|
||||
let navTrackEl = $state<HTMLElement | null>(null);
|
||||
let indicatorY = $state(0);
|
||||
let indicatorH = $state(40);
|
||||
let indicatorReady = $state(false);
|
||||
|
||||
const updateNavIndicator = () => {
|
||||
if (!navTrackEl || activePage !== "settings") {
|
||||
indicatorReady = false;
|
||||
return;
|
||||
}
|
||||
const btn = navTrackEl.querySelector<HTMLElement>(
|
||||
`[data-nav-section="${activeSection}"]`,
|
||||
);
|
||||
if (!btn) {
|
||||
indicatorReady = false;
|
||||
return;
|
||||
}
|
||||
const trackRect = navTrackEl.getBoundingClientRect();
|
||||
const btnRect = btn.getBoundingClientRect();
|
||||
indicatorY = btnRect.top - trackRect.top + navTrackEl.scrollTop;
|
||||
indicatorH = btnRect.height;
|
||||
indicatorReady = true;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
activeSection;
|
||||
activePage;
|
||||
navTrackEl;
|
||||
queueMicrotask(updateNavIndicator);
|
||||
});
|
||||
let activeThemeView = $state<ThemeView>("theme-settings");
|
||||
let activeBackgroundView = $state<BackgroundView>("background-settings");
|
||||
let selectedBackgroundCategory = $state("All");
|
||||
let backgroundCategories = $state<string[]>([]);
|
||||
let storeSearchTerm = $state("");
|
||||
|
||||
let showDisclaimerModal = $state(false);
|
||||
let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
|
||||
@@ -75,12 +70,6 @@
|
||||
const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel();
|
||||
let ghReleaseUpdate = $state<GhReleaseUpdateInfo | null>(null);
|
||||
|
||||
const pages: { id: PageId; title: string }[] = [
|
||||
{ id: "settings", title: "Settings" },
|
||||
{ id: "shortcuts", title: "Shortcuts" },
|
||||
{ id: "themes", title: "Themes" },
|
||||
];
|
||||
|
||||
const userNav: NavItem[] = [
|
||||
{ id: "account", label: "My Account" },
|
||||
{ id: "general", label: "General" },
|
||||
@@ -92,15 +81,77 @@
|
||||
{ id: "assessments", label: "Assessments" },
|
||||
{ id: "features", label: "Features" },
|
||||
{ id: "advanced", label: "Advanced" },
|
||||
{ id: "shortcuts", label: "Shortcuts" },
|
||||
];
|
||||
|
||||
const sectionTitle = $derived.by(() => {
|
||||
if (activePage === "shortcuts") return "Shortcuts";
|
||||
if (activePage === "themes") return "Themes";
|
||||
const all = [...userNav, ...appNav];
|
||||
return all.find((item) => item.id === activeSection)?.label ?? "Settings";
|
||||
const settingsNavGroups = [
|
||||
{ label: "User Settings", items: userNav },
|
||||
{ label: "App Settings", items: appNav },
|
||||
];
|
||||
|
||||
const themeNavGroups = [
|
||||
{
|
||||
label: "Themes",
|
||||
items: [
|
||||
{ id: "theme-settings", label: "Theme settings" },
|
||||
{ id: "create-theme", label: "Create theme" },
|
||||
{ id: "theme-store", label: "Store" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const backgroundNavGroups = $derived.by(() => {
|
||||
const storeOpen = activeBackgroundView === "background-store";
|
||||
const categoryItems: NavItem[] = storeOpen
|
||||
? ["All", "Featured", ...backgroundCategories].map((category, index) => ({
|
||||
id: `${BACKGROUND_CATEGORY_PREFIX}${category}`,
|
||||
label: category,
|
||||
nested: true,
|
||||
divided: index === 2,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return [
|
||||
{
|
||||
label: "Backgrounds",
|
||||
items: [
|
||||
{ id: "background-settings", label: "Background settings" },
|
||||
{ id: "background-store", label: "Store", expanded: storeOpen },
|
||||
...categoryItems,
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const navGroups = $derived(
|
||||
activePage === "settings"
|
||||
? settingsNavGroups
|
||||
: activePage === "themes"
|
||||
? themeNavGroups
|
||||
: backgroundNavGroups,
|
||||
);
|
||||
|
||||
const selectedNavId = $derived(
|
||||
activePage === "settings"
|
||||
? activeSection
|
||||
: activePage === "themes"
|
||||
? activeThemeView
|
||||
: activeBackgroundView === "background-store"
|
||||
? `${BACKGROUND_CATEGORY_PREFIX}${selectedBackgroundCategory}`
|
||||
: activeBackgroundView,
|
||||
);
|
||||
|
||||
const sectionTitle = $derived.by(() => {
|
||||
if (activePage === "themes") return "Themes";
|
||||
if (activePage === "backgrounds") return "Backgrounds";
|
||||
return [...userNav, ...appNav].find((item) => item.id === activeSection)?.label ?? "Settings";
|
||||
});
|
||||
|
||||
const isStoreView = $derived(
|
||||
(activePage === "themes" && activeThemeView === "theme-store") ||
|
||||
(activePage === "backgrounds" && activeBackgroundView === "background-store"),
|
||||
);
|
||||
|
||||
const openGhRelease = () => {
|
||||
const url =
|
||||
ghReleaseUpdate?.url ?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
|
||||
@@ -198,15 +249,52 @@
|
||||
}
|
||||
};
|
||||
|
||||
const selectPage = (page: PageId) => {
|
||||
activePage = page;
|
||||
const selectNavItem = (id: string) => {
|
||||
if (activePage === "settings") {
|
||||
activeSection = id;
|
||||
return;
|
||||
}
|
||||
|
||||
if (activePage === "themes") {
|
||||
if (id === "create-theme") {
|
||||
void openThemeCreator();
|
||||
} else {
|
||||
activeThemeView = id as ThemeView;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.startsWith(BACKGROUND_CATEGORY_PREFIX)) {
|
||||
activeBackgroundView = "background-store";
|
||||
selectedBackgroundCategory = id.slice(BACKGROUND_CATEGORY_PREFIX.length);
|
||||
} else {
|
||||
activeBackgroundView = id as BackgroundView;
|
||||
}
|
||||
};
|
||||
|
||||
const selectSection = (id: string) => {
|
||||
activeSection = id;
|
||||
activePage = "settings";
|
||||
const openThemeCreator = async () => {
|
||||
const { OpenThemeCreator } = await import("@/plugins/built-in/themes/ThemeCreator");
|
||||
OpenThemeCreator();
|
||||
closeExtensionPopup();
|
||||
};
|
||||
|
||||
const applyDestination = (destination: SettingsDestination) => {
|
||||
activePage = destination.page;
|
||||
if (destination.page === "settings" && destination.section) {
|
||||
activeSection = destination.section;
|
||||
} else if (destination.page === "themes" && destination.view) {
|
||||
activeThemeView = destination.view === "store" ? "theme-store" : "theme-settings";
|
||||
} else if (destination.page === "backgrounds" && destination.view) {
|
||||
activeBackgroundView = destination.view === "store" ? "background-store" : "background-settings";
|
||||
}
|
||||
};
|
||||
|
||||
const utilityActions = [
|
||||
{ label: "About", icon: "\ueb73", onclick: openAbout },
|
||||
{ label: "What's new", icon: "\ue929", onclick: openChangelog },
|
||||
{ label: "Privacy", icon: "\uecba", onclick: openPrivacyStatement },
|
||||
] as const;
|
||||
|
||||
onMount(() => {
|
||||
settingsPopup.addListener(closePopupsOnSettingsClose);
|
||||
|
||||
@@ -231,6 +319,14 @@
|
||||
};
|
||||
window.addEventListener("bsplus:open-feedback", onOpenFeedback);
|
||||
|
||||
const onNavigateSettings = (event: Event) => {
|
||||
applyDestination((event as CustomEvent<SettingsDestination>).detail);
|
||||
consumeSettingsDestination();
|
||||
};
|
||||
window.addEventListener(SETTINGS_NAVIGATION_EVENT, onNavigateSettings);
|
||||
const pendingDestination = consumeSettingsDestination();
|
||||
if (pendingDestination) applyDestination(pendingDestination);
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && !standalone) {
|
||||
closeExtensionPopup();
|
||||
@@ -241,6 +337,7 @@
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
window.removeEventListener("bsplus:open-feedback", onOpenFeedback);
|
||||
window.removeEventListener(SETTINGS_NAVIGATION_EVENT, onNavigateSettings);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -249,126 +346,21 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet navButton(item: NavItem)}
|
||||
<button
|
||||
type="button"
|
||||
data-nav-section={item.id}
|
||||
onclick={() => selectSection(item.id)}
|
||||
class="relative z-10 w-full px-3 py-2.5 text-left text-lg rounded-lg transition-colors duration-200
|
||||
{activePage === 'settings' && activeSection === item.id
|
||||
? 'text-zinc-900 dark:text-white font-medium'
|
||||
: 'text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white'}"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet settingsShell()}
|
||||
<div
|
||||
class="flex flex-col h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white text-[18px] {standalone
|
||||
? ''
|
||||
: 'rounded-xl shadow-2xl border border-zinc-200/60 dark:border-zinc-700/60'}"
|
||||
>
|
||||
<!-- Top bar: logo + page selectors + actions -->
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-4 px-5 py-4 border-b border-zinc-200/60 dark:border-zinc-700/50"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img
|
||||
src={browser.runtime.getURL("resources/icons/betterseqta-dark-full.png")}
|
||||
class="h-9 w-auto dark:hidden shrink-0 cursor-pointer"
|
||||
alt="BetterSEQTA+"
|
||||
onclick={handleDevModeToggle}
|
||||
<StoreHeader
|
||||
searchTerm={storeSearchTerm}
|
||||
setSearchTerm={(term) => (storeSearchTerm = term)}
|
||||
{activePage}
|
||||
setActivePage={(page) => (activePage = page)}
|
||||
showStoreTools={isStoreView}
|
||||
onLogoClick={handleDevModeToggle}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img
|
||||
src={browser.runtime.getURL("resources/icons/betterseqta-light-full.png")}
|
||||
class="hidden h-9 w-auto dark:block shrink-0 cursor-pointer"
|
||||
alt="BetterSEQTA+"
|
||||
onclick={handleDevModeToggle}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-1 items-center justify-center gap-1 p-1.5 rounded-full bg-zinc-100/80 dark:bg-zinc-900/50"
|
||||
role="tablist"
|
||||
aria-label="Settings pages"
|
||||
>
|
||||
{#each pages as page (page.id)}
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activePage === page.id}
|
||||
onclick={() => selectPage(page.id)}
|
||||
class="flex-1 px-4 py-2.5 text-lg rounded-full transition-all duration-200
|
||||
{activePage === page.id
|
||||
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white font-semibold shadow-sm'
|
||||
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200'}"
|
||||
>
|
||||
{page.title}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if !standalone}
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
{#if ghReleaseUpdateEnabled}
|
||||
<div class="flex flex-col items-end gap-0.5 max-w-[8.5rem] mr-0.5">
|
||||
{#if ghReleaseUpdate?.available}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openGhRelease}
|
||||
class="px-1.5 py-0.5 text-[10px] font-semibold leading-tight text-white rounded-full bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
|
||||
title="Open GitHub release"
|
||||
>
|
||||
Update — {ghReleaseUpdate.label}
|
||||
</button>
|
||||
{/if}
|
||||
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
|
||||
{#if ghReleaseChannelLabel}
|
||||
{ghReleaseChannelLabel} — do not upload to stores.
|
||||
{:else}
|
||||
GitHub build — do not upload to stores.
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openAbout}
|
||||
class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
aria-label="About"
|
||||
>
|
||||

|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={openChangelog}
|
||||
class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
aria-label="Changelog"
|
||||
>
|
||||

|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={openPrivacyStatement}
|
||||
class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
aria-label="Privacy Statement"
|
||||
>
|
||||

|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleClose}
|
||||
class="flex justify-center items-center w-9 h-9 text-xl rounded-lg font-IconFamily bg-zinc-100 dark:bg-zinc-700 transition-all duration-200 hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-800"
|
||||
aria-label="Close settings"
|
||||
>
|
||||

|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Body: left nav + content -->
|
||||
<div class="flex flex-1 min-h-0 overflow-hidden">
|
||||
@@ -378,79 +370,47 @@
|
||||
: 'w-[260px] px-4 py-5'}"
|
||||
aria-label="Settings categories"
|
||||
>
|
||||
<div class="relative flex flex-col flex-1 min-h-0 gap-5 overflow-y-auto no-scrollbar" bind:this={navTrackEl}>
|
||||
{#if activePage === "settings" && indicatorReady}
|
||||
<div
|
||||
class="absolute left-0 right-0 top-0 z-0 rounded-lg bg-zinc-200/90 dark:bg-zinc-700/90 pointer-events-none"
|
||||
style="transform: translateY({indicatorY}px); height: {indicatorH}px; transition: {$settingsState.animations
|
||||
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), height 0.28s cubic-bezier(0.22, 1, 0.36, 1)'
|
||||
: 'none'};"
|
||||
></div>
|
||||
<SidebarNav
|
||||
groups={navGroups}
|
||||
selectedId={selectedNavId}
|
||||
onselect={selectNavItem}
|
||||
/>
|
||||
|
||||
<div class="mt-4 flex shrink-0 flex-col gap-1 border-t border-zinc-200/60 pt-4 dark:border-zinc-700/50">
|
||||
{#if ghReleaseUpdateEnabled}
|
||||
<div class="px-3 pb-2">
|
||||
{#if ghReleaseUpdate?.available}
|
||||
<button
|
||||
type="button"
|
||||
onclick={openGhRelease}
|
||||
class="mb-1 px-1.5 py-0.5 text-[10px] font-semibold leading-tight text-white rounded-full bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-500 transition-colors duration-200"
|
||||
title="Open GitHub release"
|
||||
>
|
||||
Update — {ghReleaseUpdate.label}
|
||||
</button>
|
||||
{/if}
|
||||
<p class="text-[9px] leading-tight text-zinc-500 dark:text-zinc-400">
|
||||
{ghReleaseChannelLabel ?? "GitHub build"} — do not upload to stores.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activePage === "settings"}
|
||||
<div class="flex flex-col gap-1">
|
||||
<p
|
||||
class="relative z-10 px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
User Settings
|
||||
</p>
|
||||
{#each userNav as item (item.id)}
|
||||
{@render navButton(item)}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p
|
||||
class="relative z-10 px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
App Settings
|
||||
</p>
|
||||
{#each appNav as item (item.id)}
|
||||
{@render navButton(item)}
|
||||
{/each}
|
||||
</div>
|
||||
{:else if activePage === "shortcuts"}
|
||||
<div class="flex flex-col gap-1">
|
||||
<p
|
||||
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
Shortcuts
|
||||
</p>
|
||||
{#each utilityActions as action (action.label)}
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
|
||||
onclick={action.onclick}
|
||||
class="flex h-10 w-full items-center gap-3 rounded-lg px-3 text-left text-[16px] font-medium text-zinc-600 transition-[color,background-color,transform] duration-150 hover:bg-zinc-200/70 hover:text-zinc-950 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:text-zinc-300 dark:hover:bg-zinc-700/70 dark:hover:text-white dark:focus:ring-offset-zinc-900"
|
||||
>
|
||||
Shortcuts
|
||||
<span class="w-5 text-center font-IconFamily text-[18px]" aria-hidden="true">{action.icon}</span>
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
<p
|
||||
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||
>
|
||||
Themes
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
|
||||
>
|
||||
Themes
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openFeedback()}
|
||||
class="shrink-0 mt-4 w-full px-3 py-2.5 text-left text-[18px] font-medium rounded-lg transition-all duration-200
|
||||
text-zinc-700 dark:text-zinc-200
|
||||
bg-zinc-200/70 dark:bg-zinc-800/80
|
||||
hover:bg-zinc-300/80 dark:hover:bg-zinc-700
|
||||
hover:scale-[1.02] active:scale-95
|
||||
focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:focus:ring-offset-zinc-900"
|
||||
class="mt-2 flex w-full items-center gap-3 rounded-lg bg-zinc-200/70 px-3 py-2.5 text-left text-[16px] font-medium text-zinc-700 transition-[color,background-color,transform] duration-150 hover:bg-zinc-300/80 active:scale-[0.96] focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:bg-zinc-800/80 dark:text-zinc-200 dark:hover:bg-zinc-700 dark:focus:ring-offset-zinc-900"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg
|
||||
class="w-5 h-5 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -464,10 +424,26 @@
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>Send us feedback!</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{#if isStoreView}
|
||||
<div class="min-w-0 min-h-0 flex-1">
|
||||
<Store
|
||||
activeTab={activePage as StoreTab}
|
||||
searchTerm={storeSearchTerm}
|
||||
{selectedBackgroundCategory}
|
||||
setActiveTab={(tab) => {
|
||||
activePage = tab;
|
||||
if (tab === "themes") activeThemeView = "theme-store";
|
||||
else activeBackgroundView = "background-store";
|
||||
}}
|
||||
setSearchTerm={(term) => (storeSearchTerm = term)}
|
||||
setBackgroundCategories={(categories) => (backgroundCategories = categories)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col flex-1 min-w-0 min-h-0">
|
||||
<div class="shrink-0 px-6 pt-5 pb-3">
|
||||
<h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-white">
|
||||
@@ -476,6 +452,9 @@
|
||||
</div>
|
||||
<div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar">
|
||||
{#if activePage === "settings"}
|
||||
{#if activeSection === "shortcuts"}
|
||||
<Shortcuts />
|
||||
{:else}
|
||||
<Settings
|
||||
showColourPicker={openColourPicker}
|
||||
showFontPicker={openFontPicker}
|
||||
@@ -483,13 +462,63 @@
|
||||
showCloudPanel={openCloudPanel}
|
||||
{activeSection}
|
||||
/>
|
||||
{:else if activePage === "shortcuts"}
|
||||
<Shortcuts />
|
||||
{/if}
|
||||
{:else if activePage === "themes"}
|
||||
<Theme section="themes" />
|
||||
{:else}
|
||||
<Theme />
|
||||
<Theme section="backgrounds" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet compactSettings()}
|
||||
<div
|
||||
class="flex h-full min-h-0 flex-col gap-2 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white"
|
||||
>
|
||||
<div
|
||||
class="grid shrink-0 place-items-center border-b border-zinc-200/40 dark:border-zinc-700/40"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img
|
||||
src={resolveExtensionAssetUrl(darkLogo)}
|
||||
class="w-4/5 dark:hidden"
|
||||
alt="BetterSEQTA+"
|
||||
onclick={handleDevModeToggle}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img
|
||||
src={resolveExtensionAssetUrl(lightLogo)}
|
||||
class="hidden w-4/5 dark:block"
|
||||
alt="BetterSEQTA+"
|
||||
onclick={handleDevModeToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<TabbedContainer
|
||||
bind:activeTab={compactActiveTab}
|
||||
tabs={[
|
||||
{
|
||||
title: "Settings",
|
||||
Content: Settings,
|
||||
props: {
|
||||
showColourPicker: openColourPicker,
|
||||
showFontPicker: openFontPicker,
|
||||
showDisclaimer,
|
||||
showCloudPanel: openCloudPanel,
|
||||
activeSection: "all",
|
||||
},
|
||||
},
|
||||
{ title: "Shortcuts", Content: Shortcuts },
|
||||
{ title: "Themes", Content: Theme },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -500,7 +529,7 @@
|
||||
? 'dark'
|
||||
: ''}"
|
||||
>
|
||||
{@render settingsShell()}
|
||||
{@render compactSettings()}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
@@ -520,6 +549,7 @@
|
||||
|
||||
<div
|
||||
class="relative z-10 w-[min(1180px,96vw)] h-[min(860px,92vh)] no-scrollbar overflow-clip"
|
||||
data-settings-panel
|
||||
>
|
||||
{@render settingsShell()}
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,11 @@
|
||||
notificationCollector: "features",
|
||||
};
|
||||
|
||||
const showsSection = (section: string) =>
|
||||
activeSection === "all" || activeSection === section;
|
||||
|
||||
const pluginBelongsInSection = (pluginId: string) =>
|
||||
activeSection === "all" ||
|
||||
(pluginSectionById[pluginId] ?? "features") === activeSection;
|
||||
|
||||
|
||||
@@ -191,7 +195,7 @@
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
||||
{#if activeSection === "account"}
|
||||
{#if showsSection("account")}
|
||||
{@render Setting({
|
||||
title: "Connect Mobile App",
|
||||
description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn",
|
||||
@@ -220,7 +224,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activeSection === "general"}
|
||||
{#if showsSection("general")}
|
||||
{#each [
|
||||
...(!isEngage
|
||||
? [
|
||||
@@ -266,16 +270,6 @@
|
||||
onChange: (isOn: boolean) => settingsState.timeFormat = isOn ? "12" : "24"
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Transparency Effects",
|
||||
description: "Enable transparency effects on certain elements, such as blur (May impact battery life)",
|
||||
id: 1,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.transparencyEffects,
|
||||
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Default Page",
|
||||
description: "Choose which page loads first when you open SEQTA",
|
||||
@@ -335,7 +329,7 @@
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if activeSection === "appearance"}
|
||||
{#if showsSection("appearance")}
|
||||
{#each [
|
||||
{
|
||||
title: "Custom Theme Colour",
|
||||
@@ -410,7 +404,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activeSection === "general"}
|
||||
{#if showsSection("general")}
|
||||
<div class="border-none">
|
||||
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
|
||||
<div class="flex justify-between items-center px-5 py-4">
|
||||
@@ -590,7 +584,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if activeSection === "advanced"}
|
||||
{#if showsSection("advanced")}
|
||||
{@render Setting({
|
||||
title: "BetterSEQTA+",
|
||||
description: "Enables BetterSEQTA+ features",
|
||||
|
||||
@@ -3,21 +3,32 @@
|
||||
import ThemeSelector from "@/interface/components/themes/ThemeSelector.svelte"
|
||||
import { standalone } from "@/interface/utils/standalone.svelte"
|
||||
|
||||
let { section = "all" } = $props<{
|
||||
section?: "all" | "themes" | "backgrounds";
|
||||
}>();
|
||||
|
||||
// backgrounds
|
||||
let selectedBackground = $state<string | null>(null);
|
||||
let selectNoBackground = $state<() => void>(() => { });
|
||||
|
||||
let clearTheme = $derived(selectedBackground !== null);
|
||||
let editMode = $state<boolean>(false);
|
||||
let showBackgrounds = $derived(section !== "themes");
|
||||
let showThemes = $derived(section !== "backgrounds");
|
||||
</script>
|
||||
|
||||
<div class="py-4">
|
||||
{#if !standalone.standalone}
|
||||
{#if showBackgrounds}
|
||||
<button
|
||||
onclick={() => selectNoBackground()}
|
||||
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
|
||||
{ clearTheme ? 'Clear Theme' : 'Select a Theme' }
|
||||
disabled={section === "backgrounds" && !clearTheme}
|
||||
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50 disabled:opacity-50">
|
||||
{section === "all"
|
||||
? clearTheme ? 'Clear Theme' : 'Select a Theme'
|
||||
: clearTheme ? 'Clear Background' : 'No Background Selected'}
|
||||
</button>
|
||||
{/if}
|
||||
<div class="relative w-full">
|
||||
<button
|
||||
onclick={() => editMode = !editMode}
|
||||
@@ -26,8 +37,12 @@
|
||||
<span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span>
|
||||
</button>
|
||||
|
||||
{#if showBackgrounds}
|
||||
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
|
||||
<ThemeSelector isEditMode={editMode} />
|
||||
{/if}
|
||||
{#if showThemes}
|
||||
<ThemeSelector isEditMode={editMode} showNavigation={section === "all"} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
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 { visibleStoreThemes, buildCoverSlidesForThemes, normalizeStoreTheme } from '@/interface/utils/themeStoreFlavours'
|
||||
import browser from 'webextension-polyfill'
|
||||
import ThemeModal from '../components/store/ThemeModal.svelte'
|
||||
import Header from '../components/store/Header.svelte'
|
||||
import { themeUpdates } from '../hooks/ThemeUpdates'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
|
||||
@@ -21,6 +19,22 @@
|
||||
import { consumePendingHighlightThemeId } from '@/seqta/utils/openThemeStoreWithHighlight'
|
||||
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
type StoreTab = 'themes' | 'backgrounds';
|
||||
let {
|
||||
activeTab,
|
||||
searchTerm,
|
||||
selectedBackgroundCategory,
|
||||
setActiveTab,
|
||||
setSearchTerm,
|
||||
setBackgroundCategories,
|
||||
} = $props<{
|
||||
activeTab: StoreTab;
|
||||
searchTerm: string;
|
||||
selectedBackgroundCategory: string;
|
||||
setActiveTab: (tab: StoreTab) => void;
|
||||
setSearchTerm: (term: string) => void;
|
||||
setBackgroundCategories: (categories: string[]) => void;
|
||||
}>();
|
||||
let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn);
|
||||
|
||||
$effect(() => {
|
||||
@@ -29,7 +43,6 @@
|
||||
});
|
||||
|
||||
// State variables
|
||||
let searchTerm = $state('');
|
||||
let themes = $state<Theme[]>([]);
|
||||
|
||||
/** Grid/search/cover: hides flat-listed slaves when API sends them */
|
||||
@@ -38,10 +51,8 @@
|
||||
/** Cover marquee slides (master + flavour imagery for top masters) */
|
||||
let coverSlides = $derived(buildCoverSlidesForThemes(listThemes.slice(0, 3)));
|
||||
let loading = $state(true);
|
||||
let darkMode = $state(false);
|
||||
let displayTheme = $state<Theme | null>(null);
|
||||
let currentThemes = $state<string[]>([]);
|
||||
let activeTab = $state('themes');
|
||||
|
||||
let error = $state<string | null>(null);
|
||||
let fetchAttempt = $state(0);
|
||||
@@ -69,14 +80,6 @@
|
||||
displayTheme = theme;
|
||||
};
|
||||
|
||||
const setSearchTerm = (term: string) => {
|
||||
searchTerm = term;
|
||||
};
|
||||
|
||||
const setActiveTab = (tab: string) => {
|
||||
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;
|
||||
@@ -155,8 +158,8 @@
|
||||
const match = themes.find((t) => t.id === themeId)
|
||||
?? themes.find((t) => t.flavours?.some((f) => f.id === themeId));
|
||||
if (match) {
|
||||
activeTab = 'themes';
|
||||
searchTerm = '';
|
||||
setActiveTab('themes');
|
||||
setSearchTerm('');
|
||||
displayTheme = match;
|
||||
}
|
||||
}
|
||||
@@ -175,9 +178,6 @@
|
||||
await fetchThemes();
|
||||
await fetchCurrentThemes();
|
||||
|
||||
darkMode = (await browser.storage.local.get('DarkMode')).DarkMode === 'true';
|
||||
darkMode = $settingsState.DarkMode;
|
||||
|
||||
const pending = consumePendingHighlightThemeId();
|
||||
if (pending) focusThemeById(pending);
|
||||
|
||||
@@ -242,11 +242,9 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-screen h-screen bg-white {darkMode ? 'dark' : ''}">
|
||||
<div class="h-full overflow-y-scroll bg-zinc-200/50 dark:bg-zinc-900 dark:text-white pt-[4.25rem]">
|
||||
<Header {searchTerm} {setSearchTerm} {darkMode} {activeTab} {setActiveTab} />
|
||||
|
||||
<div class={`px-12 h-full ${activeTab === 'backgrounds' ? 'pt-0' : 'pt-6 md:px-24 lg:px-48'}`}>
|
||||
<div class="relative flex h-full min-h-0 flex-col overflow-hidden text-zinc-900 dark:text-white">
|
||||
<main class="min-h-0 flex-1 overflow-y-auto bg-zinc-50/80 dark:bg-zinc-900/40">
|
||||
<div class={activeTab === 'backgrounds' ? 'h-full' : 'px-6 py-6 md:px-8 lg:px-10'}>
|
||||
<!-- Loading State -->
|
||||
{#if loading}
|
||||
<div class="grid grid-cols-1 gap-4 py-12 mx-auto sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -263,7 +261,7 @@
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-6 px-4 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700"
|
||||
class="mt-6 rounded-lg bg-zinc-900 px-4 py-2 font-medium text-white transition-colors duration-200 hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-white"
|
||||
onclick={() => {
|
||||
loading = true;
|
||||
error = null;
|
||||
@@ -312,11 +310,15 @@
|
||||
/>
|
||||
{/if}
|
||||
{:else if activeTab === 'backgrounds'}
|
||||
<Backgrounds {searchTerm} />
|
||||
<Backgrounds
|
||||
{searchTerm}
|
||||
selectedCategory={selectedBackgroundCategory}
|
||||
onCategoriesChange={setBackgroundCategories}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{#if showSignInOverlay}
|
||||
<SignInToFavoriteModal onClose={() => (showSignInOverlay = false)} />
|
||||
|
||||
@@ -75,12 +75,12 @@
|
||||
}
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
margin-right: 48px;
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
line-height: 32px;
|
||||
margin: 0 48px 0 8px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
transform: translateX(300%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ const messageFoldersPlugin = {
|
||||
|
||||
let activeFolderId: string | null = null;
|
||||
let messageListObserver: MutationObserver | null = null;
|
||||
let observedMessageList: Element | null = null;
|
||||
let sidebarObserver: MutationObserver | null = null;
|
||||
let actionsObserver: MutationObserver | null = null;
|
||||
let openDropdown: HTMLElement | null = null;
|
||||
@@ -218,9 +219,6 @@ const messageFoldersPlugin = {
|
||||
}
|
||||
};
|
||||
|
||||
const isMessageInAnyCustomFolder = (messageId: string): boolean =>
|
||||
getAssignedFolderIds(messageId, getAssignments()).length > 0;
|
||||
|
||||
const shouldShowBadgesInList = (): boolean => {
|
||||
return api.settings.showTagsInAllMessages || activeFolderId !== null;
|
||||
};
|
||||
@@ -837,20 +835,9 @@ const messageFoldersPlugin = {
|
||||
const messageItems = getMessageListItems();
|
||||
const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button");
|
||||
if (activeFolderId === null) {
|
||||
if (api.settings.hideFolderedMessagesInAll) {
|
||||
for (const li of messageItems) {
|
||||
const msgId = li.getAttribute("data-message");
|
||||
if (msgId && isMessageInAnyCustomFolder(msgId)) {
|
||||
li.classList.add("bsplus-folder-hidden");
|
||||
} else {
|
||||
li.classList.remove("bsplus-folder-hidden");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const li of messageItems) {
|
||||
li.classList.remove("bsplus-folder-hidden");
|
||||
}
|
||||
}
|
||||
if (moreBtn) (moreBtn as HTMLElement).classList.remove("bsplus-folder-hidden");
|
||||
return;
|
||||
}
|
||||
@@ -868,14 +855,29 @@ const messageFoldersPlugin = {
|
||||
|
||||
const setupMessageListObserver = () => {
|
||||
const messageList = document.querySelector("[class*='MessageList__MessageList___'] ol");
|
||||
if (!messageList || messageListObserver) return;
|
||||
messageListObserver = new MutationObserver(() => {
|
||||
if (!messageList || messageList === observedMessageList) return;
|
||||
messageListObserver?.disconnect();
|
||||
observedMessageList = messageList;
|
||||
messageListObserver = new MutationObserver((mutations) => {
|
||||
const messageRowsChanged = mutations.some((mutation) =>
|
||||
mutation.type === "childList"
|
||||
? mutation.target === messageList
|
||||
: mutation.target instanceof Element &&
|
||||
mutation.target.parentElement === messageList &&
|
||||
mutation.target.matches("li[data-message]"),
|
||||
);
|
||||
if (!messageRowsChanged) return;
|
||||
applyBadges();
|
||||
applyFolderFilter();
|
||||
attachDragListeners();
|
||||
attachContextMenuListeners();
|
||||
});
|
||||
messageListObserver.observe(messageList, { childList: true, subtree: false });
|
||||
messageListObserver.observe(messageList, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "data-message"],
|
||||
});
|
||||
};
|
||||
|
||||
const attachContextMenuListeners = () => {
|
||||
@@ -925,6 +927,7 @@ const messageFoldersPlugin = {
|
||||
renderSidebarFolders();
|
||||
attachNativeSidebarListeners();
|
||||
}
|
||||
setupMessageListObserver();
|
||||
});
|
||||
sidebarObserver.observe(sidebar, { childList: true, subtree: true });
|
||||
}
|
||||
@@ -937,12 +940,6 @@ const messageFoldersPlugin = {
|
||||
applyBadges();
|
||||
}),
|
||||
);
|
||||
unregisters.push(
|
||||
api.settings.onChange("hideFolderedMessagesInAll", () => {
|
||||
applyFolderFilter();
|
||||
}),
|
||||
);
|
||||
|
||||
return () => {
|
||||
for (const u of unregisters) u.unregister();
|
||||
messageListObserver?.disconnect();
|
||||
|
||||
@@ -9,12 +9,6 @@ const settings = defineSettings({
|
||||
description:
|
||||
"When off, folder tags are not shown on the message list until you select a folder.",
|
||||
}),
|
||||
hideFolderedMessagesInAll: booleanSetting({
|
||||
default: true,
|
||||
title: "Hide foldered messages in All Messages",
|
||||
description:
|
||||
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
|
||||
}),
|
||||
});
|
||||
|
||||
export default defineLazyPlugin({
|
||||
|
||||
@@ -27,7 +27,6 @@ export const SYNCABLE_PLUGIN_SETTING_DEFAULTS: Record<
|
||||
"background-music": { volume: 0.5, pauseOnHidden: true },
|
||||
messageFolders: {
|
||||
showTagsInAllMessages: true,
|
||||
hideFolderedMessagesInAll: true,
|
||||
},
|
||||
"enhanced-navigation": { autoScrollOnClick: false },
|
||||
"global-search": {
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
import { unmount } from "svelte";
|
||||
|
||||
let remove: () => void;
|
||||
|
||||
export async function OpenStorePage(): Promise<void> {
|
||||
remove = await renderStore();
|
||||
}
|
||||
|
||||
export async function renderStore() {
|
||||
const [{ default: renderSvelte }, { default: Store }] = await Promise.all([
|
||||
import("@/interface/main"),
|
||||
import("@/interface/pages/store.svelte"),
|
||||
const [{ requestSettingsDestination }, { openSettingsPopup }] =
|
||||
await Promise.all([
|
||||
import("@/seqta/utils/settingsNavigation"),
|
||||
import("@/seqta/utils/setupSettingsButton"),
|
||||
]);
|
||||
|
||||
const container =
|
||||
document.querySelector("#container") ??
|
||||
document.getElementById("content") ??
|
||||
document.body;
|
||||
|
||||
document.getElementById("store")?.remove();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function closeStore() {
|
||||
document.getElementById("store")!.classList.add("hide");
|
||||
|
||||
setTimeout(() => {
|
||||
remove();
|
||||
document.getElementById("store")!.remove();
|
||||
}, 500);
|
||||
requestSettingsDestination({ page: "themes", view: "store" });
|
||||
await openSettingsPopup();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,12 @@ async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
|
||||
const mount = () => renderSvelte(Settings, shadow);
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
requestIdleCallback(mount);
|
||||
await new Promise<void>((resolve) => {
|
||||
requestIdleCallback(() => {
|
||||
mount();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
mount();
|
||||
}
|
||||
|
||||
@@ -11,14 +11,23 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
|
||||
|
||||
extensionPopup.classList.add("hide");
|
||||
if (settingsState.animations) {
|
||||
const panel = extensionPopup.shadowRoot?.querySelector<HTMLElement>(
|
||||
"[data-settings-panel]",
|
||||
);
|
||||
animate(1, 0, {
|
||||
onUpdate: (progress) => {
|
||||
extensionPopup.style.opacity = Math.max(0, progress).toString();
|
||||
},
|
||||
type: "spring",
|
||||
stiffness: 520,
|
||||
damping: 20,
|
||||
duration: 0.18,
|
||||
ease: "easeIn",
|
||||
});
|
||||
if (panel) {
|
||||
animate(
|
||||
panel,
|
||||
{ scale: [1, 0], opacity: [1, 0] },
|
||||
{ duration: 0.18, ease: "easeIn" },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
extensionPopup.style.opacity = "0";
|
||||
}
|
||||
|
||||
@@ -311,10 +311,10 @@ export async function loadEngageHomePage(): Promise<void> {
|
||||
const engageHomeBody = stringToHTML(/* html */ `
|
||||
<div class="home-root" id="engage-home-root">
|
||||
<div class="home-container" id="engage-home-container">
|
||||
<div class="bsplus-rounded shortcut-container">
|
||||
<div class="bsplus-rounded shortcuts" id="shortcuts"></div>
|
||||
<div class="border shortcut-container">
|
||||
<div class="border shortcuts" id="shortcuts"></div>
|
||||
</div>
|
||||
<div class="bsplus-rounded timetable-container">
|
||||
<div class="border timetable-container">
|
||||
<div class="home-subtitle">
|
||||
<div class="engage-timetable-title-cluster">
|
||||
<h2 id="engage-home-lesson-subtitle">Today's Lessons</h2>
|
||||
@@ -331,7 +331,7 @@ export async function loadEngageHomePage(): Promise<void> {
|
||||
</div>
|
||||
<div class="day-container loading" id="engage-day-container"></div>
|
||||
</div>
|
||||
<div class="bsplus-rounded notices-container">
|
||||
<div class="border notices-container">
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<h2 class="home-subtitle">Notices</h2>
|
||||
<input type="date" id="engage-notices-date" />
|
||||
|
||||
@@ -62,10 +62,10 @@ export async function loadHomePage() {
|
||||
|
||||
const skeletonStructure = stringToHTML(/* html */ `
|
||||
<div class="home-container" id="home-container">
|
||||
<div class="bsplus-rounded shortcut-container">
|
||||
<div class="bsplus-rounded shortcuts" id="shortcuts"></div>
|
||||
<div class="border shortcut-container">
|
||||
<div class="border shortcuts" id="shortcuts"></div>
|
||||
</div>
|
||||
<div class="bsplus-rounded timetable-container">
|
||||
<div class="border timetable-container">
|
||||
<div class="home-subtitle">
|
||||
<h2 id="home-lesson-subtitle">Today's Lessons</h2>
|
||||
<div class="timetable-arrows">
|
||||
@@ -80,7 +80,7 @@ export async function loadHomePage() {
|
||||
<div class="day-container loading" id="day-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="bsplus-rounded upcoming-container">
|
||||
<div class="border upcoming-container">
|
||||
<div class="upcoming-title">
|
||||
<h2 class="home-subtitle">Upcoming Assessments</h2>
|
||||
<div class="upcoming-filters" id="upcoming-filters"></div>
|
||||
@@ -88,7 +88,7 @@ export async function loadHomePage() {
|
||||
<div class="upcoming-items loading" id="upcoming-items">
|
||||
</div>
|
||||
</div>
|
||||
<div class="bsplus-rounded notices-container">
|
||||
<div class="border notices-container">
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<h2 class="home-subtitle">Notices</h2>
|
||||
<input type="date" />
|
||||
|
||||
@@ -11,15 +11,9 @@ export function consumePendingHighlightThemeId(): string | null {
|
||||
|
||||
export async function openThemeStoreWithHighlight(themeId: string): Promise<void> {
|
||||
pendingHighlightThemeId = themeId;
|
||||
|
||||
const existing = document.getElementById("store");
|
||||
if (existing) {
|
||||
const { OpenStorePage } = await import("@/seqta/ui/renderStore");
|
||||
await OpenStorePage();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bsplus:highlight-theme", { detail: { themeId } }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { OpenStorePage } = await import("@/seqta/ui/renderStore");
|
||||
await OpenStorePage();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export type SettingsDestination =
|
||||
| { page: "settings"; section?: string }
|
||||
| { page: "themes" | "backgrounds"; view?: "settings" | "store" };
|
||||
|
||||
export const SETTINGS_NAVIGATION_EVENT = "bsplus:navigate-settings";
|
||||
|
||||
let pendingDestination: SettingsDestination | null = null;
|
||||
|
||||
export function requestSettingsDestination(destination: SettingsDestination): void {
|
||||
pendingDestination = destination;
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(SETTINGS_NAVIGATION_EVENT, { detail: destination }),
|
||||
);
|
||||
}
|
||||
|
||||
export function consumeSettingsDestination(): SettingsDestination | null {
|
||||
const destination = pendingDestination;
|
||||
pendingDestination = null;
|
||||
return destination;
|
||||
}
|
||||
@@ -17,7 +17,18 @@ export function setupSettingsButton() {
|
||||
AddedSettings.dataset.bsplusSettingsBound = "1";
|
||||
|
||||
AddedSettings.addEventListener("click", async () => {
|
||||
// Re-query each click — Engage SPA navigations can recreate the host.
|
||||
if (SettingsClicked) {
|
||||
closeExtensionPopup();
|
||||
} else {
|
||||
await openSettingsPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function openSettingsPopup(): Promise<void> {
|
||||
if (SettingsClicked && document.getElementById("ExtensionPopup")) return;
|
||||
|
||||
// Re-query each time — Engage SPA navigations can recreate the host.
|
||||
let extensionPopup = document.getElementById("ExtensionPopup");
|
||||
if (!extensionPopup) {
|
||||
const { addExtensionSettings } = await import("./Adders/AddExtensionSettings");
|
||||
@@ -26,14 +37,13 @@ export function setupSettingsButton() {
|
||||
}
|
||||
if (!extensionPopup) return;
|
||||
|
||||
if (SettingsClicked) {
|
||||
closeExtensionPopup(extensionPopup);
|
||||
} else {
|
||||
await renderSettingsIfNeeded();
|
||||
|
||||
await delay(30);
|
||||
|
||||
extensionPopup.style.transform = "none";
|
||||
const panel = extensionPopup.shadowRoot?.querySelector<HTMLElement>(
|
||||
"[data-settings-panel]",
|
||||
);
|
||||
if (settingsState.animations) {
|
||||
animate(0, 1, {
|
||||
onUpdate: (progress) => {
|
||||
@@ -43,12 +53,21 @@ export function setupSettingsButton() {
|
||||
stiffness: 280,
|
||||
damping: 20,
|
||||
});
|
||||
if (panel) {
|
||||
animate(
|
||||
panel,
|
||||
{ scale: [0, 1], opacity: [0, 1] },
|
||||
{ type: "spring", stiffness: 330, damping: 30 },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
extensionPopup.style.opacity = "1";
|
||||
extensionPopup.style.transition = "opacity 0s linear";
|
||||
if (panel) {
|
||||
panel.style.scale = "1";
|
||||
panel.style.opacity = "1";
|
||||
}
|
||||
}
|
||||
extensionPopup.classList.remove("hide");
|
||||
changeSettingsClicked(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user