diff --git a/src/css/injected.scss b/src/css/injected.scss index 3b5e3169..11bc6e5c 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -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; diff --git a/src/interface/components/PlainCloseButton.svelte b/src/interface/components/PlainCloseButton.svelte new file mode 100644 index 00000000..aaa345ed --- /dev/null +++ b/src/interface/components/PlainCloseButton.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/interface/components/SidebarAppearance.svelte b/src/interface/components/SidebarAppearance.svelte index 6333f853..77f40f0f 100644 --- a/src/interface/components/SidebarAppearance.svelte +++ b/src/interface/components/SidebarAppearance.svelte @@ -1,6 +1,7 @@ + +
+ {#if indicatorReady} +
+ {/if} + + {#each groups as group} +
+ {#if group.label} +

+ {group.label} +

+ {/if} + {#each group.items as item (item.id)} + {#if item.divided} +
+ {/if} + onselect(item.id)} + /> + {/each} +
+ {/each} +
diff --git a/src/interface/components/SidebarNavItem.svelte b/src/interface/components/SidebarNavItem.svelte new file mode 100644 index 00000000..24708c55 --- /dev/null +++ b/src/interface/components/SidebarNavItem.svelte @@ -0,0 +1,33 @@ + + + diff --git a/src/interface/components/TabbedContainer.css b/src/interface/components/TabbedContainer.css deleted file mode 100644 index d3b7ea5e..00000000 --- a/src/interface/components/TabbedContainer.css +++ /dev/null @@ -1,3 +0,0 @@ -.tab-width { - width: var(--tab-width); -} diff --git a/src/interface/components/TabbedContainer.svelte b/src/interface/components/TabbedContainer.svelte index 7d871d31..9519f403 100644 --- a/src/interface/components/TabbedContainer.svelte +++ b/src/interface/components/TabbedContainer.svelte @@ -1,6 +1,5 @@ -
- -
-
-

Categories

- -
-
- - -
+
@@ -294,7 +263,6 @@
{/if}
-
{#if settingsState.devMode} @@ -307,4 +275,3 @@

Selected Category: {selectedCategory}

{/if} - diff --git a/src/interface/components/store/CloudHeader.svelte b/src/interface/components/store/CloudHeader.svelte index abae4e29..9a536e9e 100644 --- a/src/interface/components/store/CloudHeader.svelte +++ b/src/interface/components/store/CloudHeader.svelte @@ -69,7 +69,7 @@ diff --git a/src/interface/components/store/CoverSwiper.svelte b/src/interface/components/store/CoverSwiper.svelte index 1f1bb038..01bf1914 100644 --- a/src/interface/components/store/CoverSwiper.svelte +++ b/src/interface/components/store/CoverSwiper.svelte @@ -34,16 +34,16 @@ {#if slides.length > 0} -
+
{#each slides as slide (slide.imageUrl + slide.title + (slide.subtitle ?? ''))}
{ @@ -51,7 +51,7 @@ }} onclick={() => setDisplayTheme(slide.openTheme)} > - + {#if slide.badgeFeatured === true}
{/if} -
+

{slide.title}

{#if slide.subtitle}

{slide.subtitle}

@@ -81,7 +81,6 @@

{slide.openTheme.description}

{/if}
-
{/each}
@@ -111,3 +110,26 @@
{/if} + + diff --git a/src/interface/components/store/Header.svelte b/src/interface/components/store/Header.svelte index bfe322d9..fd9fa05f 100644 --- a/src/interface/components/store/Header.svelte +++ b/src/interface/components/store/Header.svelte @@ -1,77 +1,120 @@ -
-
-
{ if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0"> - Logo - Dark Logo +
+ -
- - - - - -
- -
- 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" /> - - - - - - +
+
+ + {#each tabs as tab (tab.id)} + + {/each}
-
\ No newline at end of file + +
+ {#if showStoreTools} + + + + {/if} + + +
+ + + diff --git a/src/interface/components/store/ThemeCard.svelte b/src/interface/components/store/ThemeCard.svelte index 12f9d288..bae0d58d 100644 --- a/src/interface/components/store/ThemeCard.svelte +++ b/src/interface/components/store/ThemeCard.svelte @@ -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; } @@ -86,8 +70,7 @@ onclick={handleCardClick} >
{#if theme.featured === true}
@@ -102,45 +85,34 @@
{/if} - -
- - {#if menuOpen} - - {/if} -
+ + +
{theme.name} {#if theme.author} @@ -161,7 +133,6 @@
-
{#if gridRotatorUrls.length === 0} {:else if !allowSlideAutoplay || gridRotatorUrls.length === 1} @@ -199,3 +170,22 @@ {/if}
+ + diff --git a/src/interface/components/store/ThemeGrid.svelte b/src/interface/components/store/ThemeGrid.svelte index 5b219fe8..0fbe398d 100644 --- a/src/interface/components/store/ThemeGrid.svelte +++ b/src/interface/components/store/ThemeGrid.svelte @@ -30,7 +30,7 @@
-
+
{#each filteredThemes as theme (theme.id)} -
+
{'\uecb3'}
Got a Theme Idea? @@ -64,4 +64,4 @@
{/if} -
\ No newline at end of file +
diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index 8723139e..62ae76de 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -12,7 +12,10 @@ const themeManager = ThemeManager.getInstance(); let themes = $state(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>({}); @@ -128,7 +131,6 @@ const openStorePage = async () => { const { OpenStorePage } = await import('@/seqta/ui/renderStore') - closeExtensionPopup() await OpenStorePage() } @@ -166,7 +168,7 @@
-

Themes

{#if themes} {#each themes.themes as theme (theme.id)} @@ -267,25 +268,54 @@
{/if} - {#if themes && themes.themes.length > 0} -
+ {#if themes && themes.themes.length === 0 && !tempTheme} +
+

+ No themes installed +

+

+ Install a theme from the store or create one of your own. +

+
+ + +
+
{/if} - + {#if showNavigation && themes && themes.themes.length > 0} +
- + + + + {/if}
diff --git a/src/interface/index.ts b/src/interface/index.ts index 9e893eb7..459bb698 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -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"; @@ -37,4 +36,4 @@ if (!mountPoint) { initVerboseLogging(); renderSvelte(Settings, mountPoint, { standalone: true }); })(); -} \ No newline at end of file +} diff --git a/src/interface/main.ts b/src/interface/main.ts index 3cd8a164..874d2395 100644 --- a/src/interface/main.ts +++ b/src/interface/main.ts @@ -16,6 +16,14 @@ export default function renderSvelte( props: Record = {}, 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; } diff --git a/src/interface/pages/settings.svelte b/src/interface/pages/settings.svelte index dd81488f..2463d347 100644 --- a/src/interface/pages/settings.svelte +++ b/src/interface/pages/settings.svelte @@ -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("settings"); let activeSection = $state("general"); - let navTrackEl = $state(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( - `[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("theme-settings"); + let activeBackgroundView = $state("background-settings"); + let selectedBackgroundCategory = $state("All"); + let backgroundCategories = $state([]); + 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(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).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 @@ }); -{#snippet navButton(item: NavItem)} - -{/snippet} - {#snippet settingsShell()}
- -
- - - BetterSEQTA+ - - - - -
- {#each pages as page (page.id)} - - {/each} -
- - {#if !standalone} -
- {#if ghReleaseUpdateEnabled} -
- {#if ghReleaseUpdate?.available} - - {/if} -

- {#if ghReleaseChannelLabel} - {ghReleaseChannelLabel} — do not upload to stores. - {:else} - GitHub build — do not upload to stores. - {/if} -

-
- {/if} - - - - -
- {/if} -
+ (storeSearchTerm = term)} + {activePage} + setActivePage={(page) => (activePage = page)} + showStoreTools={isStoreView} + onLogoClick={handleDevModeToggle} + onClose={handleClose} + />
@@ -378,79 +370,47 @@ : 'w-[260px] px-4 py-5'}" aria-label="Settings categories" > -
- {#if activePage === "settings" && indicatorReady} -
+ + +
+ {#if ghReleaseUpdateEnabled} +
+ {#if ghReleaseUpdate?.available} + + {/if} +

+ {ghReleaseChannelLabel ?? "GitHub build"} — do not upload to stores. +

+
{/if} - {#if activePage === "settings"} -
-

- User Settings -

- {#each userNav as item (item.id)} - {@render navButton(item)} - {/each} -
-
-

- App Settings -

- {#each appNav as item (item.id)} - {@render navButton(item)} - {/each} -
- {:else if activePage === "shortcuts"} -
-

- Shortcuts -

- -
- {:else} -
-

- Themes -

- -
- {/if} -
+ {#each utilityActions as action (action.label)} + + {/each} - + +
-
-
-

- {sectionTitle} -

+ {#if isStoreView} +
+ { + activePage = tab; + if (tab === "themes") activeThemeView = "theme-store"; + else activeBackgroundView = "background-store"; + }} + setSearchTerm={(term) => (storeSearchTerm = term)} + setBackgroundCategories={(categories) => (backgroundCategories = categories)} + />
-
- {#if activePage === "settings"} - - {:else if activePage === "shortcuts"} - - {:else} - - {/if} + {:else} +
+
+

+ {sectionTitle} +

+
+
+ {#if activePage === "settings"} + {#if activeSection === "shortcuts"} + + {:else} + + {/if} + {:else if activePage === "themes"} + + {:else} + + {/if} +
-
+ {/if} +
+
+{/snippet} + +{#snippet compactSettings()} +
+
+ + + BetterSEQTA+ + + + +
+ +
+
{/snippet} @@ -500,7 +529,7 @@ ? 'dark' : ''}" > - {@render settingsShell()} + {@render compactSettings()}
{:else}
{@render settingsShell()}
diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 9201ac22..a7ec2710 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -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}
- {#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 @@
{/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 @@
{/if} - {#if activeSection === "general"} + {#if showsSection("general")}
@@ -590,7 +584,7 @@ {/if} {/each} - {#if activeSection === "advanced"} + {#if showsSection("advanced")} {@render Setting({ title: "BetterSEQTA+", description: "Enables BetterSEQTA+ features", diff --git a/src/interface/pages/settings/theme.svelte b/src/interface/pages/settings/theme.svelte index 12d765e3..74be1d73 100644 --- a/src/interface/pages/settings/theme.svelte +++ b/src/interface/pages/settings/theme.svelte @@ -2,6 +2,10 @@ import BackgroundSelector from "@/interface/components/themes/BackgroundSelector.svelte" 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(null); @@ -9,15 +13,22 @@ let clearTheme = $derived(selectedBackground !== null); let editMode = $state(false); + let showBackgrounds = $derived(section !== "themes"); + let showThemes = $derived(section !== "backgrounds");
{#if !standalone.standalone} - + {#if showBackgrounds} + + {/if}
- - + {#if showBackgrounds} + + {/if} + {#if showThemes} + + {/if}
{:else}
diff --git a/src/interface/pages/store.svelte b/src/interface/pages/store.svelte index 1a626b69..6713d253 100644 --- a/src/interface/pages/store.svelte +++ b/src/interface/pages/store.svelte @@ -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([]); /** 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(null); let currentThemes = $state([]); - let activeTab = $state('themes'); let error = $state(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,13 +242,11 @@ }); -
-
-
- -
- - {#if loading} +
+
+
+ + {#if loading}
{#each Array(6) as _, i (i)} @@ -263,7 +261,7 @@

-
+ {/if} +
+ {#if showSignInOverlay} (showSignInOverlay = false)} /> {/if} -
\ No newline at end of file +
diff --git a/src/plugins/built-in/globalSearch/src/core/styles.css b/src/plugins/built-in/globalSearch/src/core/styles.css index c49489a6..e442b4ab 100644 --- a/src/plugins/built-in/globalSearch/src/core/styles.css +++ b/src/plugins/built-in/globalSearch/src/core/styles.css @@ -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%); } } @@ -289,4 +289,4 @@ .dark .search-progress-text.is-active.is-rough { color: #fca5a5; -} \ No newline at end of file +} diff --git a/src/plugins/built-in/messageFolders/index.ts b/src/plugins/built-in/messageFolders/index.ts index 3c61dce0..4c6fa969 100644 --- a/src/plugins/built-in/messageFolders/index.ts +++ b/src/plugins/built-in/messageFolders/index.ts @@ -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,19 +835,8 @@ 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"); - } + 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(); diff --git a/src/plugins/built-in/messageFolders/lazy.ts b/src/plugins/built-in/messageFolders/lazy.ts index afe3c1c4..292cb920 100644 --- a/src/plugins/built-in/messageFolders/lazy.ts +++ b/src/plugins/built-in/messageFolders/lazy.ts @@ -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({ diff --git a/src/plugins/syncablePluginDefaults.ts b/src/plugins/syncablePluginDefaults.ts index 8c73576f..e438e1fa 100644 --- a/src/plugins/syncablePluginDefaults.ts +++ b/src/plugins/syncablePluginDefaults.ts @@ -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": { diff --git a/src/seqta/ui/renderStore.ts b/src/seqta/ui/renderStore.ts index 9d2d7b04..b7ce53b3 100644 --- a/src/seqta/ui/renderStore.ts +++ b/src/seqta/ui/renderStore.ts @@ -1,39 +1,9 @@ -import { unmount } from "svelte"; - -let remove: () => void; - export async function OpenStorePage(): Promise { - remove = await renderStore(); -} - -export async function renderStore() { - const [{ default: renderSvelte }, { default: Store }] = await Promise.all([ - import("@/interface/main"), - import("@/interface/pages/store.svelte"), - ]); - - 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); + const [{ requestSettingsDestination }, { openSettingsPopup }] = + await Promise.all([ + import("@/seqta/utils/settingsNavigation"), + import("@/seqta/utils/setupSettingsButton"), + ]); + requestSettingsDestination({ page: "themes", view: "store" }); + await openSettingsPopup(); } diff --git a/src/seqta/utils/Adders/AddExtensionSettings.ts b/src/seqta/utils/Adders/AddExtensionSettings.ts index a73e54ee..5436b63e 100644 --- a/src/seqta/utils/Adders/AddExtensionSettings.ts +++ b/src/seqta/utils/Adders/AddExtensionSettings.ts @@ -52,7 +52,12 @@ async function loadSettingsUi(extensionPopup: HTMLElement): Promise { const mount = () => renderSvelte(Settings, shadow); if ("requestIdleCallback" in window) { - requestIdleCallback(mount); + await new Promise((resolve) => { + requestIdleCallback(() => { + mount(); + resolve(); + }); + }); } else { mount(); } diff --git a/src/seqta/utils/Closers/closeExtensionPopup.ts b/src/seqta/utils/Closers/closeExtensionPopup.ts index c8d16a38..20d67f22 100644 --- a/src/seqta/utils/Closers/closeExtensionPopup.ts +++ b/src/seqta/utils/Closers/closeExtensionPopup.ts @@ -11,14 +11,23 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { extensionPopup.classList.add("hide"); if (settingsState.animations) { + const panel = extensionPopup.shadowRoot?.querySelector( + "[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"; } diff --git a/src/seqta/utils/Loaders/LoadEngageHomePage.ts b/src/seqta/utils/Loaders/LoadEngageHomePage.ts index db523db6..1f91643a 100644 --- a/src/seqta/utils/Loaders/LoadEngageHomePage.ts +++ b/src/seqta/utils/Loaders/LoadEngageHomePage.ts @@ -311,10 +311,10 @@ export async function loadEngageHomePage(): Promise { const engageHomeBody = stringToHTML(/* html */ `
-
-
+
+
-
+

Today's Lessons

@@ -331,7 +331,7 @@ export async function loadEngageHomePage(): Promise {
-
+

Notices

diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index f0de74d1..1c777b2b 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -62,10 +62,10 @@ export async function loadHomePage() { const skeletonStructure = stringToHTML(/* html */ `
-
-
+
+
-
+

Today's Lessons

@@ -80,7 +80,7 @@ export async function loadHomePage() {
-
+

Upcoming Assessments

@@ -88,7 +88,7 @@ export async function loadHomePage() {
-
+

Notices

diff --git a/src/seqta/utils/openThemeStoreWithHighlight.ts b/src/seqta/utils/openThemeStoreWithHighlight.ts index 7597d257..00b343a4 100644 --- a/src/seqta/utils/openThemeStoreWithHighlight.ts +++ b/src/seqta/utils/openThemeStoreWithHighlight.ts @@ -11,15 +11,9 @@ export function consumePendingHighlightThemeId(): string | null { export async function openThemeStoreWithHighlight(themeId: string): Promise { pendingHighlightThemeId = themeId; - - const existing = document.getElementById("store"); - if (existing) { - window.dispatchEvent( - new CustomEvent("bsplus:highlight-theme", { detail: { themeId } }), - ); - return; - } - const { OpenStorePage } = await import("@/seqta/ui/renderStore"); await OpenStorePage(); + window.dispatchEvent( + new CustomEvent("bsplus:highlight-theme", { detail: { themeId } }), + ); } diff --git a/src/seqta/utils/settingsNavigation.ts b/src/seqta/utils/settingsNavigation.ts new file mode 100644 index 00000000..8290d9ab --- /dev/null +++ b/src/seqta/utils/settingsNavigation.ts @@ -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; +} diff --git a/src/seqta/utils/setupSettingsButton.ts b/src/seqta/utils/setupSettingsButton.ts index 6f87eb52..0cabfee0 100644 --- a/src/seqta/utils/setupSettingsButton.ts +++ b/src/seqta/utils/setupSettingsButton.ts @@ -17,38 +17,57 @@ export function setupSettingsButton() { AddedSettings.dataset.bsplusSettingsBound = "1"; AddedSettings.addEventListener("click", async () => { - // Re-query each click — Engage SPA navigations can recreate the host. - let extensionPopup = document.getElementById("ExtensionPopup"); - if (!extensionPopup) { - const { addExtensionSettings } = await import("./Adders/AddExtensionSettings"); - addExtensionSettings(); - extensionPopup = document.getElementById("ExtensionPopup"); - } - if (!extensionPopup) return; - if (SettingsClicked) { - closeExtensionPopup(extensionPopup); + closeExtensionPopup(); } else { - await renderSettingsIfNeeded(); - - await delay(30); - - extensionPopup.style.transform = "none"; - if (settingsState.animations) { - animate(0, 1, { - onUpdate: (progress) => { - extensionPopup.style.opacity = progress.toString(); - }, - type: "spring", - stiffness: 280, - damping: 20, - }); - } else { - extensionPopup.style.opacity = "1"; - extensionPopup.style.transition = "opacity 0s linear"; - } - extensionPopup.classList.remove("hide"); - changeSettingsClicked(true); + await openSettingsPopup(); } }); } + +export async function openSettingsPopup(): Promise { + 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"); + addExtensionSettings(); + extensionPopup = document.getElementById("ExtensionPopup"); + } + if (!extensionPopup) return; + + await renderSettingsIfNeeded(); + await delay(30); + + extensionPopup.style.transform = "none"; + const panel = extensionPopup.shadowRoot?.querySelector( + "[data-settings-panel]", + ); + if (settingsState.animations) { + animate(0, 1, { + onUpdate: (progress) => { + extensionPopup.style.opacity = progress.toString(); + }, + type: "spring", + 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); +}