mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: new settings popup
This commit is contained in:
@@ -274,6 +274,14 @@ select option {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 35px !important;
|
width: 35px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Engage toolbar: keep settings/theme buttons above React chrome */
|
||||||
|
.connectedNotificationsWrapper .addedButton,
|
||||||
|
.connectedNotificationsWrapper #AddedSettings,
|
||||||
|
.connectedNotificationsWrapper #LightDarkModeButton {
|
||||||
|
z-index: 50 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
[style="background-color: rgb(255, 255, 255);"] {
|
[style="background-color: rgb(255, 255, 255);"] {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -14,11 +14,12 @@
|
|||||||
.outside-container {
|
.outside-container {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
right: 10px;
|
inset: 0;
|
||||||
top: 80px;
|
width: 100%;
|
||||||
height: 590px;
|
height: 100%;
|
||||||
z-index: 100;
|
/* Above Engage React chrome / MUI layers; below rare fullscreen media overlays */
|
||||||
|
z-index: 200000;
|
||||||
transition-duration: 100ms;
|
transition-duration: 100ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +32,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ExtensionPopup {
|
#ExtensionPopup {
|
||||||
border-radius: 1rem;
|
border-radius: 0;
|
||||||
filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.6));
|
filter: none;
|
||||||
transform-origin: 70% 0;
|
/* Avoid transform on the host — it makes shadow `position: fixed` children
|
||||||
will-change: opacity, transform;
|
size to the host instead of the viewport on Engage stacking contexts. */
|
||||||
transform: translateZ(0); // promotes GPU rendering
|
transform-origin: center center;
|
||||||
transition:
|
will-change: opacity;
|
||||||
opacity 0.05s,
|
transition: opacity 0.05s;
|
||||||
transform 0.05s;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,8 @@
|
|||||||
|
|
||||||
const openStorePage = async () => {
|
const openStorePage = async () => {
|
||||||
const { OpenStorePage } = await import('@/seqta/ui/renderStore')
|
const { OpenStorePage } = await import('@/seqta/ui/renderStore')
|
||||||
OpenStorePage()
|
closeExtensionPopup()
|
||||||
|
await OpenStorePage()
|
||||||
}
|
}
|
||||||
|
|
||||||
const openThemeCreator = async (themeId?: string) => {
|
const openThemeCreator = async (themeId?: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TabbedContainer from "../components/TabbedContainer.svelte";
|
|
||||||
import Settings from "./settings/general.svelte";
|
import Settings from "./settings/general.svelte";
|
||||||
import Shortcuts from "./settings/shortcuts.svelte";
|
import Shortcuts from "./settings/shortcuts.svelte";
|
||||||
import Theme from "./settings/theme.svelte";
|
import Theme from "./settings/theme.svelte";
|
||||||
@@ -25,20 +24,64 @@
|
|||||||
isGhReleaseUpdateCheckEnabled,
|
isGhReleaseUpdateCheckEnabled,
|
||||||
type GhReleaseUpdateInfo,
|
type GhReleaseUpdateInfo,
|
||||||
} from "@/utils/githubReleaseUpdate";
|
} from "@/utils/githubReleaseUpdate";
|
||||||
|
import { getAllPluginSettings } from "@/plugins";
|
||||||
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
|
|
||||||
|
type PageId = "settings" | "shortcuts" | "themes";
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
let devModeSequence = "";
|
let devModeSequence = "";
|
||||||
let settingsActiveTab = $state(0);
|
let activePage = $state<PageId>("settings");
|
||||||
|
let activeSection = $state("general");
|
||||||
let showDisclaimerModal = $state(false);
|
let showDisclaimerModal = $state(false);
|
||||||
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
|
let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
|
||||||
let disclaimerTitle = $state("Confirm");
|
let disclaimerTitle = $state("Confirm");
|
||||||
let disclaimerMessage = $state("");
|
let disclaimerMessage = $state("");
|
||||||
const ghReleaseUpdateEnabled = isGhReleaseUpdateCheckEnabled();
|
const ghReleaseUpdateEnabled = isGhReleaseUpdateCheckEnabled();
|
||||||
const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel();
|
const ghReleaseChannelLabel = getInstalledGhReleaseChannelLabel();
|
||||||
let ghReleaseUpdate = $state<GhReleaseUpdateInfo | null>(null);
|
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 pluginNavItems = getAllPluginSettings()
|
||||||
|
.filter((plugin) => !(isSeqtaEngageExperience() && plugin.pluginId === "global-search"))
|
||||||
|
.filter(
|
||||||
|
(plugin) =>
|
||||||
|
(plugin as { disableToggle?: boolean }).disableToggle ||
|
||||||
|
Object.keys(plugin.settings ?? {}).length > 0,
|
||||||
|
)
|
||||||
|
.map((plugin) => ({
|
||||||
|
id: `plugin:${plugin.pluginId}`,
|
||||||
|
label: plugin.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userNav: NavItem[] = [
|
||||||
|
{ id: "account", label: "My Account" },
|
||||||
|
{ id: "general", label: "General" },
|
||||||
|
{ id: "appearance", label: "Appearance" },
|
||||||
|
{ id: "home", label: "Home" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const appNav: NavItem[] = [...pluginNavItems, { id: "advanced", label: "Advanced" }];
|
||||||
|
|
||||||
|
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 openGhRelease = () => {
|
const openGhRelease = () => {
|
||||||
const url = ghReleaseUpdate?.url
|
const url =
|
||||||
?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
|
ghReleaseUpdate?.url ?? "https://github.com/BetterSEQTA/BetterSEQTA-Plus/releases";
|
||||||
if (ghReleaseUpdate?.available) {
|
if (ghReleaseUpdate?.available) {
|
||||||
dismissNightlyUpdate();
|
dismissNightlyUpdate();
|
||||||
}
|
}
|
||||||
@@ -118,6 +161,21 @@
|
|||||||
showCloudPanel = false;
|
showCloudPanel = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!standalone) {
|
||||||
|
closeExtensionPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPage = (page: PageId) => {
|
||||||
|
activePage = page;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSection = (id: string) => {
|
||||||
|
activeSection = id;
|
||||||
|
activePage = "settings";
|
||||||
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
settingsPopup.addListener(closePopupsOnSettingsClose);
|
settingsPopup.addListener(closePopupsOnSettingsClose);
|
||||||
|
|
||||||
@@ -130,6 +188,17 @@
|
|||||||
ghReleaseUpdate = info;
|
ghReleaseUpdate = info;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape" && !standalone) {
|
||||||
|
closeExtensionPopup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -137,120 +206,259 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#snippet navButton(item: NavItem)}
|
||||||
class="relative w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode
|
<button
|
||||||
? 'dark'
|
type="button"
|
||||||
: ''} {standalone ? 'h-[600px]' : 'h-full rounded-xl'} overflow-clip"
|
onclick={() => selectSection(item.id)}
|
||||||
>
|
class="w-full px-3 py-2 text-left text-base rounded-lg transition-all duration-200
|
||||||
<div
|
{activePage === 'settings' && activeSection === item.id
|
||||||
class="flex relative flex-col gap-2 h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white"
|
? 'bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white font-medium'
|
||||||
|
: 'text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-700/40 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 {standalone
|
||||||
|
? ''
|
||||||
|
: 'rounded-xl shadow-2xl border border-zinc-200/60 dark:border-zinc-700/60'}"
|
||||||
|
>
|
||||||
|
<!-- Top bar: logo + page selectors + actions -->
|
||||||
<div
|
<div
|
||||||
class="grid shrink-0 place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"
|
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_no_noninteractive_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<img
|
<img
|
||||||
src={browser.runtime.getURL(
|
src={browser.runtime.getURL("resources/icons/betterseqta-dark-full.png")}
|
||||||
"resources/icons/betterseqta-dark-full.png",
|
class="h-9 w-auto dark:hidden shrink-0 cursor-pointer"
|
||||||
)}
|
alt="BetterSEQTA+"
|
||||||
class="w-4/5 dark:hidden"
|
|
||||||
alt="Light logo"
|
|
||||||
onclick={handleDevModeToggle}
|
onclick={handleDevModeToggle}
|
||||||
/>
|
/>
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<img
|
<img
|
||||||
src={browser.runtime.getURL(
|
src={browser.runtime.getURL("resources/icons/betterseqta-light-full.png")}
|
||||||
"resources/icons/betterseqta-light-full.png",
|
class="hidden h-9 w-auto dark:block shrink-0 cursor-pointer"
|
||||||
)}
|
alt="BetterSEQTA+"
|
||||||
class="hidden w-4/5 dark:block"
|
|
||||||
alt="Dark logo"
|
|
||||||
onclick={handleDevModeToggle}
|
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 text-base 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}
|
{#if !standalone}
|
||||||
<div class="flex absolute top-1 right-1 gap-1 items-start">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
{#if ghReleaseUpdateEnabled}
|
{#if ghReleaseUpdateEnabled}
|
||||||
<div class="flex flex-col items-end gap-0.5 max-w-[9rem] mr-0.5">
|
<div class="flex flex-col items-end gap-0.5 max-w-[8.5rem] mr-0.5">
|
||||||
{#if ghReleaseUpdate?.available}
|
{#if ghReleaseUpdate?.available}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={openGhRelease}
|
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"
|
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"
|
title="Open GitHub release"
|
||||||
>
|
>
|
||||||
Update available — {ghReleaseUpdate.label}
|
Update — {ghReleaseUpdate.label}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
|
<p class="text-[9px] leading-tight text-right text-zinc-500 dark:text-zinc-400">
|
||||||
{#if ghReleaseChannelLabel}
|
{#if ghReleaseChannelLabel}
|
||||||
{ghReleaseChannelLabel} — do not upload to extension stores.
|
{ghReleaseChannelLabel} — do not upload to stores.
|
||||||
{:else}
|
{:else}
|
||||||
GitHub release build — do not upload to extension stores.
|
GitHub build — do not upload to stores.
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex gap-1 items-center">
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={openAbout}
|
onclick={openAbout}
|
||||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
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"
|
||||||
>
|
>
|
||||||
{"\ueb73"}
|

|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={openChangelog}
|
onclick={openChangelog}
|
||||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
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"
|
||||||
>
|
>
|
||||||
{"\ue929"}
|

|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onclick={openPrivacyStatement}
|
onclick={openPrivacyStatement}
|
||||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
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"
|
aria-label="Privacy Statement"
|
||||||
>
|
>
|
||||||
{"\uecba"}
|

|
||||||
|
</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>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
<!-- Body: left nav + content -->
|
||||||
<TabbedContainer
|
<div class="flex flex-1 min-h-0 overflow-hidden">
|
||||||
bind:activeTab={settingsActiveTab}
|
<nav
|
||||||
tabs={[
|
class="flex flex-col shrink-0 gap-5 overflow-y-auto no-scrollbar border-r border-zinc-200/60 dark:border-zinc-700/50 bg-zinc-50/80 dark:bg-zinc-900/40 {standalone
|
||||||
{
|
? 'w-[140px] px-2 py-3'
|
||||||
title: "Settings",
|
: 'w-[260px] px-4 py-5'}"
|
||||||
Content: Settings,
|
aria-label="Settings categories"
|
||||||
props: { showColourPicker: openColourPicker, showFontPicker: openFontPicker, showDisclaimer, showCloudPanel: openCloudPanel },
|
>
|
||||||
},
|
{#if activePage === "settings"}
|
||||||
{ title: "Shortcuts", Content: Shortcuts },
|
<div class="flex flex-col gap-1">
|
||||||
{ title: "Themes", Content: Theme },
|
<p
|
||||||
]}
|
class="px-3 mb-1.5 text-xs 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="px-3 mb-1.5 text-xs 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-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||||
|
>
|
||||||
|
Shortcuts
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Shortcuts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<p
|
||||||
|
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
|
||||||
|
>
|
||||||
|
Themes
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Themes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<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-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">
|
||||||
|
{sectionTitle}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar">
|
||||||
|
{#if activePage === "settings"}
|
||||||
|
<Settings
|
||||||
|
showColourPicker={openColourPicker}
|
||||||
|
showFontPicker={openFontPicker}
|
||||||
|
{showDisclaimer}
|
||||||
|
showCloudPanel={openCloudPanel}
|
||||||
|
{activeSection}
|
||||||
|
/>
|
||||||
|
{:else if activePage === "shortcuts"}
|
||||||
|
<Shortcuts />
|
||||||
|
{:else}
|
||||||
|
<Theme />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#if showColourPicker && ColourPickerComponent}
|
{#if standalone}
|
||||||
<ColourPickerComponent
|
<div
|
||||||
hidePicker={() => {
|
class="relative w-[384px] h-[600px] no-scrollbar shadow-2xl overflow-clip {$settingsState.DarkMode
|
||||||
showColourPicker = false;
|
? 'dark'
|
||||||
}}
|
: ''}"
|
||||||
/>
|
>
|
||||||
{/if}
|
{@render settingsShell()}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center p-4 sm:p-6 {$settingsState.DarkMode
|
||||||
|
? 'dark'
|
||||||
|
: ''}"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="BetterSEQTA+ settings"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute inset-0 bg-black/60 backdrop-blur-sm transition-colors duration-200"
|
||||||
|
aria-label="Close settings"
|
||||||
|
onclick={handleClose}
|
||||||
|
></button>
|
||||||
|
|
||||||
{#if showCloudPanel}
|
<div
|
||||||
<CloudPanel
|
class="relative z-10 w-[min(1180px,96vw)] h-[min(860px,92vh)] no-scrollbar overflow-clip"
|
||||||
hidePanel={() => {
|
>
|
||||||
showCloudPanel = false;
|
{@render settingsShell()}
|
||||||
}}
|
</div>
|
||||||
/>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
{#if showColourPicker && ColourPickerComponent}
|
||||||
|
<ColourPickerComponent
|
||||||
|
hidePicker={() => {
|
||||||
|
showColourPicker = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showCloudPanel}
|
||||||
|
<CloudPanel
|
||||||
|
hidePanel={() => {
|
||||||
|
showCloudPanel = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showFontPicker}
|
{#if showFontPicker}
|
||||||
<FontPickerModal
|
<FontPickerModal
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
|
|
||||||
import { getAllPluginSettings } from "@/plugins"
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"
|
||||||
|
|
||||||
|
const isEngage = isSeqtaEngageExperience();
|
||||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||||
|
|
||||||
// Union type representing all possible settings
|
// Union type representing all possible settings
|
||||||
@@ -133,13 +135,20 @@
|
|||||||
void loadPluginSettings();
|
void loadPluginSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel } = $props<{
|
const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel, activeSection = "general" } = $props<{
|
||||||
showColourPicker: () => void;
|
showColourPicker: () => void;
|
||||||
showFontPicker: () => void;
|
showFontPicker: () => void;
|
||||||
showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void;
|
showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void;
|
||||||
showCloudPanel: () => void;
|
showCloudPanel: () => void;
|
||||||
|
activeSection?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const activePluginId = $derived(
|
||||||
|
activeSection.startsWith("plugin:")
|
||||||
|
? activeSection.slice("plugin:".length)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
async function exportCloudSettingsJsonToFile() {
|
async function exportCloudSettingsJsonToFile() {
|
||||||
const payload = await getSnapshotForUpload();
|
const payload = await getSnapshotForUpload();
|
||||||
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
const blob = new Blob([JSON.stringify(payload, null, 2)], {
|
||||||
@@ -155,153 +164,244 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-5">
|
||||||
<h2 class="text-sm font-bold">{title}</h2>
|
<h2 class="text-base font-bold">{title}</h2>
|
||||||
<p class="text-xs">{description}</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="shrink-0">
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
|
||||||
{#each [
|
{#if activeSection === "account"}
|
||||||
{
|
{@render Setting({
|
||||||
title: "Connect Mobile App",
|
title: "Connect Mobile App",
|
||||||
description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn",
|
description: "Link your SEQTA session to DesQTA — the modern desktop and mobile app for SEQTA Learn",
|
||||||
id: 0,
|
id: 0,
|
||||||
Component: ConnectMobileApp,
|
Component: ConnectMobileApp,
|
||||||
props: {}
|
props: {}
|
||||||
},
|
})}
|
||||||
{
|
|
||||||
title: "Edit Sidebar Layout",
|
<div class="border-none">
|
||||||
description: "Reorder pages on the sidebar",
|
<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">
|
||||||
id: 5,
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
Component: Button,
|
<div class="pr-4">
|
||||||
props: {
|
<h2 class="text-base font-bold">BetterSEQTA Cloud</h2>
|
||||||
onClick: () => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' }),
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Account & sync</p>
|
||||||
text: "Edit"
|
</div>
|
||||||
}
|
<div>
|
||||||
},
|
<CloudHeader alwaysShowUserName onClick={showCloudPanel} />
|
||||||
{
|
</div>
|
||||||
title: "Custom Theme Colour",
|
</div>
|
||||||
description: "Customise the overall theme colour of SEQTA Learn",
|
{#if cloudState.isLoggedIn}
|
||||||
id: 4,
|
<div class="px-3 pb-3">
|
||||||
Component: PickerSwatch,
|
<CloudSettingsSync showDisclaimer={(onConfirm, onCancel) => showDisclaimer(onConfirm, onCancel, "Restore from cloud?", "This will replace your local settings with the cloud backup. Continue?")} />
|
||||||
props: {
|
</div>
|
||||||
onClick: showColourPicker
|
{/if}
|
||||||
}
|
</div>
|
||||||
},
|
</div>
|
||||||
{
|
{/if}
|
||||||
title: "Interface Font",
|
|
||||||
description: "Choose the typeface used across SEQTA Learn",
|
{#if activeSection === "general"}
|
||||||
id: 16,
|
{#each [
|
||||||
Component: Button,
|
...(!isEngage
|
||||||
props: {
|
? [
|
||||||
onClick: showFontPicker,
|
{
|
||||||
text: "Change"
|
title: "Edit Sidebar Layout",
|
||||||
}
|
description: "Reorder pages on the sidebar",
|
||||||
},
|
id: 5,
|
||||||
{
|
Component: Button,
|
||||||
title: "Icon Only Sidebar",
|
props: {
|
||||||
description: "Show only icons in the sidebar for a compact layout",
|
onClick: () => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' }),
|
||||||
id: 14,
|
text: "Edit"
|
||||||
Component: Switch,
|
}
|
||||||
props: {
|
},
|
||||||
state: $settingsState.iconOnlySidebar ?? false,
|
{
|
||||||
onChange: (isOn: boolean) => settingsState.iconOnlySidebar = isOn
|
title: "Icon Only Sidebar",
|
||||||
}
|
description: "Show only icons in the sidebar for a compact layout",
|
||||||
},
|
id: 14,
|
||||||
{
|
Component: Switch,
|
||||||
title: "Animations",
|
props: {
|
||||||
description: "Enable animations on certain pages",
|
state: $settingsState.iconOnlySidebar ?? false,
|
||||||
id: 6,
|
onChange: (isOn: boolean) => settingsState.iconOnlySidebar = isOn
|
||||||
Component: Switch,
|
}
|
||||||
props: {
|
},
|
||||||
state: $settingsState.animations,
|
]
|
||||||
onChange: (isOn: boolean) => settingsState.animations = isOn
|
: []),
|
||||||
}
|
{
|
||||||
},
|
title: "Animations",
|
||||||
{
|
description: "Enable animations on certain pages",
|
||||||
title: "12 Hour Time",
|
id: 6,
|
||||||
description: "Prefer 12 hour time format for SEQTA",
|
Component: Switch,
|
||||||
id: 9,
|
props: {
|
||||||
Component: Switch,
|
state: $settingsState.animations,
|
||||||
props: {
|
onChange: (isOn: boolean) => settingsState.animations = isOn
|
||||||
state: $settingsState.timeFormat === "12",
|
}
|
||||||
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",
|
|
||||||
id: 10,
|
|
||||||
Component: Select,
|
|
||||||
props: {
|
|
||||||
value: $settingsState.defaultPage ?? "home",
|
|
||||||
onChange: (value: string) => (settingsState.defaultPage = value),
|
|
||||||
options: [
|
|
||||||
{ value: "home", label: "Home" },
|
|
||||||
{ value: "dashboard", label: "Dashboard" },
|
|
||||||
{ value: "timetable", label: "Timetable" },
|
|
||||||
{ value: "welcome", label: "Welcome" },
|
|
||||||
{ value: "messages", label: "Messages" },
|
|
||||||
{ value: "documents", label: "Documents" },
|
|
||||||
{ value: "reports", label: "Reports" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
title: "12 Hour Time",
|
||||||
title: "News Feed Source",
|
description: "Prefer 12 hour time format for SEQTA",
|
||||||
description: "Choose the sources for your news feed",
|
id: 9,
|
||||||
id: 11,
|
Component: Switch,
|
||||||
Component: Select,
|
props: {
|
||||||
props: {
|
state: $settingsState.timeFormat === "12",
|
||||||
value: $settingsState.newsSource,
|
onChange: (isOn: boolean) => settingsState.timeFormat = isOn ? "12" : "24"
|
||||||
onChange: (value: string) => settingsState.newsSource = value,
|
}
|
||||||
options: [
|
},
|
||||||
{ value: "australia", label: "Australia" },
|
{
|
||||||
{ value: "usa", label: "USA" },
|
title: "Transparency Effects",
|
||||||
{ value: "uk", label: "UK" },
|
description: "Enable transparency effects on certain elements, such as blur (May impact battery life)",
|
||||||
{ value: "taiwan", label: "Taiwan" },
|
id: 1,
|
||||||
{ value: "hong_kong", label: "Hong Kong" },
|
Component: Switch,
|
||||||
{ value: "panama", label: "Panama" },
|
props: {
|
||||||
{ value: "canada", label: "Canada" },
|
state: $settingsState.transparencyEffects,
|
||||||
{ value: "singapore", label: "Singapore" },
|
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
|
||||||
{ value: "japan", label: "Japan" },
|
}
|
||||||
{ value: "netherlands", label: "Netherlands" }
|
},
|
||||||
]
|
{
|
||||||
|
title: "Default Page",
|
||||||
|
description: "Choose which page loads first when you open SEQTA",
|
||||||
|
id: 10,
|
||||||
|
Component: Select,
|
||||||
|
props: {
|
||||||
|
value: $settingsState.defaultPage ?? "home",
|
||||||
|
onChange: (value: string) => (settingsState.defaultPage = value),
|
||||||
|
options: isEngage
|
||||||
|
? [
|
||||||
|
{ value: "home", label: "Home" },
|
||||||
|
{ value: "dashboard", label: "Dashboard" },
|
||||||
|
{ value: "timetable", label: "Timetable" },
|
||||||
|
{ value: "messages", label: "Messages" },
|
||||||
|
{ value: "documents", label: "Documents" },
|
||||||
|
{ value: "reports", label: "Reports" },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ value: "home", label: "Home" },
|
||||||
|
{ value: "dashboard", label: "Dashboard" },
|
||||||
|
{ value: "timetable", label: "Timetable" },
|
||||||
|
{ value: "welcome", label: "Welcome" },
|
||||||
|
{ value: "messages", label: "Messages" },
|
||||||
|
{ value: "documents", label: "Documents" },
|
||||||
|
{ value: "reports", label: "Reports" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(!isEngage
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "News Feed Source",
|
||||||
|
description: "Choose the sources for your news feed",
|
||||||
|
id: 11,
|
||||||
|
Component: Select,
|
||||||
|
props: {
|
||||||
|
value: $settingsState.newsSource,
|
||||||
|
onChange: (value: string) => settingsState.newsSource = value,
|
||||||
|
options: [
|
||||||
|
{ value: "australia", label: "Australia" },
|
||||||
|
{ value: "usa", label: "USA" },
|
||||||
|
{ value: "uk", label: "UK" },
|
||||||
|
{ value: "taiwan", label: "Taiwan" },
|
||||||
|
{ value: "hong_kong", label: "Hong Kong" },
|
||||||
|
{ value: "panama", label: "Panama" },
|
||||||
|
{ value: "canada", label: "Canada" },
|
||||||
|
{ value: "singapore", label: "Singapore" },
|
||||||
|
{ value: "japan", label: "Japan" },
|
||||||
|
{ value: "netherlands", label: "Netherlands" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
] as option (option.id)}
|
||||||
|
{@render Setting(option)}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
}
|
{#if activeSection === "appearance"}
|
||||||
}
|
{#each [
|
||||||
] as option}
|
{
|
||||||
{@render Setting(option)}
|
title: "Custom Theme Colour",
|
||||||
{/each}
|
description: "Customise the overall theme colour of SEQTA Learn",
|
||||||
|
id: 4,
|
||||||
|
Component: PickerSwatch,
|
||||||
|
props: {
|
||||||
|
onClick: showColourPicker
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Interface Font",
|
||||||
|
description: "Choose the typeface used across SEQTA Learn",
|
||||||
|
id: 16,
|
||||||
|
Component: Button,
|
||||||
|
props: {
|
||||||
|
onClick: showFontPicker,
|
||||||
|
text: "Change"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
] as option (option.id)}
|
||||||
|
{@render Setting(option)}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<div class="pr-4">
|
||||||
|
<h2 class="text-base font-bold">Adaptive Theme Colour</h2>
|
||||||
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.adaptiveThemeColour ?? false}
|
||||||
|
onChange={(isOn: boolean) => settingsState.adaptiveThemeColour = isOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $settingsState.adaptiveThemeColour}
|
||||||
|
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
|
<div class="pr-4">
|
||||||
|
<h2 class="text-base font-bold">Soft Gradient</h2>
|
||||||
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Use a soft gradient instead of a solid colour when viewing a class</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.adaptiveThemeGradient ?? false}
|
||||||
|
onChange={(isOn: boolean) => settingsState.adaptiveThemeGradient = isOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
|
<div class="pr-4">
|
||||||
|
<h2 class="text-base font-bold">Smooth colour transition</h2>
|
||||||
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Ease between class/subject colours when navigating instead of switching instantly</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.adaptiveThemeColourTransition ?? true}
|
||||||
|
onChange={(isOn: boolean) => settingsState.adaptiveThemeColourTransition = isOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if activeSection === "home"}
|
||||||
<div class="border-none">
|
<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="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-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Home Page Assessments</h2>
|
<h2 class="text-base font-bold">Home Page Assessments</h2>
|
||||||
<p class="text-xs">Limit upcoming assessments shown on the home page by subject</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Limit upcoming assessments shown on the home page by subject</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Include Past Assessments</h2>
|
<h2 class="text-base font-bold">Include Past Assessments</h2>
|
||||||
<p class="text-xs">Show past-due assessments from the upcoming list, matching the Assessments page</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show past-due assessments from the upcoming list, matching the Assessments page</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -310,10 +410,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Maximum Subjects</h2>
|
<h2 class="text-base font-bold">Maximum Subjects</h2>
|
||||||
<p class="text-xs">Number of subjects to include, ordered by soonest due date</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Number of subjects to include, ordered by soonest due date</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={String($settingsState.homeUpcomingSubjectsMax ?? 5)}
|
value={String($settingsState.homeUpcomingSubjectsMax ?? 5)}
|
||||||
@@ -328,10 +428,10 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Maximum Assessments per Subject</h2>
|
<h2 class="text-base font-bold">Maximum Assessments per Subject</h2>
|
||||||
<p class="text-xs">Assessments shown for each included subject</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Assessments shown for each included subject</p>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={String($settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0)}
|
value={String($settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0)}
|
||||||
@@ -348,58 +448,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="border-none">
|
{#each pluginSettings as plugin (plugin.pluginId)}
|
||||||
<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">
|
{#if activePluginId === plugin.pluginId}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
|
||||||
<div class="pr-4">
|
|
||||||
<h2 class="text-sm font-bold">Adaptive Theme Colour</h2>
|
|
||||||
<p class="text-xs">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
state={$settingsState.adaptiveThemeColour ?? false}
|
|
||||||
onChange={(isOn: boolean) => settingsState.adaptiveThemeColour = isOn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if $settingsState.adaptiveThemeColour}
|
|
||||||
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
|
||||||
<div class="pr-4">
|
|
||||||
<h2 class="text-sm font-bold">Soft Gradient</h2>
|
|
||||||
<p class="text-xs">Use a soft gradient instead of a solid colour when viewing a class</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
state={$settingsState.adaptiveThemeGradient ?? false}
|
|
||||||
onChange={(isOn: boolean) => settingsState.adaptiveThemeGradient = isOn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center px-4 py-3 pl-6 border-t border-zinc-100 dark:border-zinc-700/50">
|
|
||||||
<div class="pr-4">
|
|
||||||
<h2 class="text-sm font-bold">Smooth colour transition</h2>
|
|
||||||
<p class="text-xs">Ease between class/subject colours when navigating instead of switching instantly</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
state={$settingsState.adaptiveThemeColourTransition ?? true}
|
|
||||||
onChange={(isOn: boolean) => settingsState.adaptiveThemeColourTransition = isOn}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#each pluginSettings as plugin}
|
|
||||||
<div class="border-none">
|
<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 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}">
|
<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 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}">
|
||||||
<!-- Always show enable toggle if disableToggle is true -->
|
<!-- Always show enable toggle if disableToggle is true -->
|
||||||
{#if (plugin as any).disableToggle}
|
{#if (plugin as any).disableToggle}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="flex gap-2 items-center text-sm font-bold">
|
<h2 class="flex gap-2 items-center text-base font-bold">
|
||||||
Enable {plugin.name}
|
Enable {plugin.name}
|
||||||
{#if plugin.beta}
|
{#if plugin.beta}
|
||||||
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30">
|
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30">
|
||||||
@@ -407,7 +466,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs">{plugin.description}</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">{plugin.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -432,13 +491,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !((plugin as any).disableToggle) || (pluginSettingsValues[plugin.pluginId]?.enabled ?? true)}
|
{#if !((plugin as any).disableToggle) || (pluginSettingsValues[plugin.pluginId]?.enabled ?? true)}
|
||||||
{#each Object.entries(plugin.settings) as [key, setting]}
|
{#each Object.entries(plugin.settings) as [key, setting] (key)}
|
||||||
<!-- Skip the 'enabled' setting and hide cloud-only settings when not signed in -->
|
<!-- Skip the 'enabled' setting and hide cloud-only settings when not signed in -->
|
||||||
{#if key !== 'enabled' && !(key === 'useCloudPfp' && !cloudState.isLoggedIn)}
|
{#if key !== 'enabled' && !(key === 'useCloudPfp' && !cloudState.isLoggedIn)}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">{setting.title || key}</h2>
|
<h2 class="text-base font-bold">{setting.title || key}</h2>
|
||||||
<p class="text-xs">{setting.description || ''}</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">{setting.description || ''}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{#if setting.type === 'boolean'}
|
{#if setting.type === 'boolean'}
|
||||||
@@ -507,29 +566,10 @@
|
|||||||
})}
|
})}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="border-none">
|
{#if activeSection === "advanced"}
|
||||||
<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-4 py-3">
|
|
||||||
<div class="pr-4">
|
|
||||||
<h2 class="text-sm font-bold">BetterSEQTA Cloud</h2>
|
|
||||||
<p class="text-xs">Account & sync</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CloudHeader alwaysShowUserName onClick={showCloudPanel} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if cloudState.isLoggedIn}
|
|
||||||
<div class="px-3 pb-3">
|
|
||||||
<CloudSettingsSync showDisclaimer={(onConfirm, onCancel) => showDisclaimer(onConfirm, onCancel, "Restore from cloud?", "This will replace your local settings with the cloud backup. Continue?")} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-1 border-none"></div>
|
|
||||||
|
|
||||||
{@render Setting({
|
{@render Setting({
|
||||||
title: "BetterSEQTA+",
|
title: "BetterSEQTA+",
|
||||||
description: "Enables BetterSEQTA+ features",
|
description: "Enables BetterSEQTA+ features",
|
||||||
@@ -543,19 +583,19 @@
|
|||||||
|
|
||||||
{#if $settingsState.devMode}
|
{#if $settingsState.devMode}
|
||||||
<div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
|
<div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 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-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Developer Mode</h2>
|
<h2 class="text-base font-bold">Developer Mode</h2>
|
||||||
<p class="text-xs">Enables developer mode, allowing you to test new features and changes.</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Enables developer mode, allowing you to test new features and changes.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
|
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Verbose logging</h2>
|
<h2 class="text-base font-bold">Verbose logging</h2>
|
||||||
<p class="text-xs">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -564,10 +604,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
<h2 class="text-base font-bold">Sensitive Hider</h2>
|
||||||
<p class="text-xs">Replace sensitive content with mock data</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Replace sensitive content with mock data</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -576,10 +616,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Mock Notices</h2>
|
<h2 class="text-base font-bold">Mock Notices</h2>
|
||||||
<p class="text-xs">Use fake notice data on homepage instead of real data</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Use fake notice data on homepage instead of real data</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -588,10 +628,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Show Privacy Notification</h2>
|
<h2 class="text-base font-bold">Show Privacy Notification</h2>
|
||||||
<p class="text-xs">Show the privacy notification popup on next page load</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show the privacy notification popup on next page load</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@@ -607,10 +647,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Show Theme of the Month</h2>
|
<h2 class="text-base font-bold">Show Theme of the Month</h2>
|
||||||
<p class="text-xs">Fetch and show the current month's popup now (ignores dismissed state)</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Fetch and show the current month's popup now (ignores dismissed state)</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@@ -623,10 +663,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-5 py-4">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Export cloud settings JSON</h2>
|
<h2 class="text-base font-bold">Export cloud settings JSON</h2>
|
||||||
<p class="text-xs">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={exportCloudSettingsJsonToFile} text="Export to file" />
|
<Button onClick={exportCloudSettingsJsonToFile} text="Export to file" />
|
||||||
@@ -635,8 +675,8 @@
|
|||||||
<div class="flex flex-col gap-2 px-4 py-3">
|
<div class="flex flex-col gap-2 px-4 py-3">
|
||||||
<div class="flex justify-between items-start gap-3">
|
<div class="flex justify-between items-start gap-3">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">API Base URL (session only)</h2>
|
<h2 class="text-base font-bold">API Base URL (session only)</h2>
|
||||||
<p class="text-xs">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p>
|
||||||
{#if devApiBaseActive}
|
{#if devApiBaseActive}
|
||||||
<p class="text-xs mt-1 text-amber-600 dark:text-amber-400">
|
<p class="text-xs mt-1 text-amber-600 dark:text-amber-400">
|
||||||
Override active: <span class="font-mono">{devApiBaseActive}</span>
|
Override active: <span class="font-mono">{devApiBaseActive}</span>
|
||||||
@@ -659,8 +699,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 px-4 py-3">
|
<div class="flex flex-col gap-2 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-sm font-bold">GitHub latest version override</h2>
|
<h2 class="text-base font-bold">GitHub latest version override</h2>
|
||||||
<p class="text-xs">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
|
<p class="text-sm text-zinc-600 dark:text-zinc-300">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -674,4 +714,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -832,12 +832,17 @@ export function AppendElementsToDisabledPage() {
|
|||||||
margin: 6px;
|
margin: 6px;
|
||||||
}
|
}
|
||||||
.outside-container {
|
.outside-container {
|
||||||
top: 48px !important;
|
position: fixed !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
z-index: 200000 !important;
|
||||||
}
|
}
|
||||||
#ExtensionPopup {
|
#ExtensionPopup {
|
||||||
border-radius: 1rem;
|
border-radius: 0;
|
||||||
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6);
|
box-shadow: none;
|
||||||
transform-origin: 70% 0;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.append(settingsStyle);
|
document.head.append(settingsStyle);
|
||||||
|
|||||||
@@ -440,22 +440,56 @@ async function addEngageUserInfo() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupEngageSettingsButton() {
|
async function mountEngageToolbarControls(parent?: Element) {
|
||||||
try {
|
// React can remount the notifications chrome — recreate missing controls.
|
||||||
const notificationsWrapper = await waitForElm(
|
if (!document.getElementById("LightDarkModeButton")) {
|
||||||
"#content > div.connectedNotificationsWrapper > div",
|
|
||||||
);
|
|
||||||
const parent = notificationsWrapper.parentElement!;
|
|
||||||
await addDarkLightToggle(parent);
|
await addDarkLightToggle(parent);
|
||||||
await createSettingsButton(parent);
|
|
||||||
setupSettingsButton();
|
|
||||||
attachNotificationsPanelAnimation();
|
|
||||||
} catch {
|
|
||||||
await addDarkLightToggle();
|
|
||||||
await createSettingsButton();
|
|
||||||
setupSettingsButton();
|
|
||||||
attachNotificationsPanelAnimation();
|
|
||||||
}
|
}
|
||||||
|
if (!document.getElementById("AddedSettings")) {
|
||||||
|
await createSettingsButton(parent);
|
||||||
|
}
|
||||||
|
setupSettingsButton();
|
||||||
|
attachNotificationsPanelAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupEngageSettingsButton() {
|
||||||
|
let mounting = false;
|
||||||
|
|
||||||
|
const tryMount = async () => {
|
||||||
|
if (mounting || document.getElementById("AddedSettings")) return;
|
||||||
|
mounting = true;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const notificationsWrapper = await waitForElm(
|
||||||
|
"#content > div.connectedNotificationsWrapper > div",
|
||||||
|
true,
|
||||||
|
100,
|
||||||
|
40,
|
||||||
|
);
|
||||||
|
const parent = notificationsWrapper.parentElement!;
|
||||||
|
await mountEngageToolbarControls(parent);
|
||||||
|
} catch {
|
||||||
|
await mountEngageToolbarControls();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
mounting = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await tryMount();
|
||||||
|
|
||||||
|
// Re-inject if Engage's React shell wipes the toolbar controls.
|
||||||
|
const content = document.getElementById("content");
|
||||||
|
if (!content || (content as HTMLElement).dataset.bsplusEngageSettingsWatch === "1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(content as HTMLElement).dataset.bsplusEngageSettingsWatch = "1";
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (document.getElementById("AddedSettings")) return;
|
||||||
|
void tryMount();
|
||||||
|
});
|
||||||
|
observer.observe(content, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetLightDarkModeString() {
|
function GetLightDarkModeString() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import debounce from "@/seqta/utils/debounce";
|
import debounce from "@/seqta/utils/debounce";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically resizes the popup to fit the screen, checks on resize but is debounced to prevent intense utilisation.
|
* Keeps the settings overlay covering the viewport.
|
||||||
*/
|
*/
|
||||||
export class SettingsResizer {
|
export class SettingsResizer {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -17,10 +17,10 @@ export class SettingsResizer {
|
|||||||
const iframePopup = document.getElementById("ExtensionPopup");
|
const iframePopup = document.getElementById("ExtensionPopup");
|
||||||
if (!iframePopup) return;
|
if (!iframePopup) return;
|
||||||
|
|
||||||
const viewportHeight = window.innerHeight;
|
iframePopup.style.inset = "0";
|
||||||
const rawIdeal = viewportHeight - 80 - 15; // room below top chrome
|
iframePopup.style.top = "0";
|
||||||
const idealHeight = Math.min(Math.max(rawIdeal, 280), 600);
|
iframePopup.style.right = "0";
|
||||||
|
iframePopup.style.width = "100%";
|
||||||
iframePopup.style.height = `${idealHeight}px`;
|
iframePopup.style.height = "100%";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export async function renderStore() {
|
|||||||
import("@/interface/pages/store.svelte"),
|
import("@/interface/pages/store.svelte"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const container = document.querySelector("#container");
|
const container =
|
||||||
if (!container) {
|
document.querySelector("#container") ??
|
||||||
throw new Error("Container not found");
|
document.getElementById("content") ??
|
||||||
}
|
document.body;
|
||||||
|
|
||||||
document.getElementById("store")?.remove();
|
document.getElementById("store")?.remove();
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ function extensionOutsideClickHandler(extensionPopup: HTMLElement) {
|
|||||||
if (!SettingsClicked) return;
|
if (!SettingsClicked) return;
|
||||||
|
|
||||||
if (!(event.target as HTMLElement).closest("#AddedSettings")) {
|
if (!(event.target as HTMLElement).closest("#AddedSettings")) {
|
||||||
|
// Clicks inside the shadow tree retarget to the host — keep open.
|
||||||
if (event.target == extensionPopup) return;
|
if (event.target == extensionPopup) return;
|
||||||
changeSettingsClicked(closeExtensionPopup());
|
changeSettingsClicked(closeExtensionPopup());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mount the settings host on `document.body` so `position: fixed` covers the
|
||||||
|
* viewport on both SEQTA Learn and SEQTA Engage (Engage often lacks `#container`
|
||||||
|
* or wraps the app in stacking contexts that clip in-app overlays).
|
||||||
|
*/
|
||||||
export function addExtensionSettings() {
|
export function addExtensionSettings() {
|
||||||
if (document.getElementById("ExtensionPopup")) return;
|
if (document.getElementById("ExtensionPopup")) return;
|
||||||
|
|
||||||
@@ -26,15 +32,12 @@ export function addExtensionSettings() {
|
|||||||
extensionPopup.classList.add("outside-container", "hide");
|
extensionPopup.classList.add("outside-container", "hide");
|
||||||
extensionPopup.id = "ExtensionPopup";
|
extensionPopup.id = "ExtensionPopup";
|
||||||
|
|
||||||
const extensionContainer =
|
document.body.appendChild(extensionPopup);
|
||||||
document.querySelector("#container") ?? document.getElementById("container");
|
|
||||||
const mountParent = extensionContainer ?? document.body;
|
|
||||||
mountParent.appendChild(extensionPopup);
|
|
||||||
|
|
||||||
new SettingsResizer();
|
new SettingsResizer();
|
||||||
|
|
||||||
const handler = extensionOutsideClickHandler(extensionPopup);
|
const handler = extensionOutsideClickHandler(extensionPopup);
|
||||||
(extensionContainer ?? document.body).addEventListener("click", handler, false);
|
document.body.addEventListener("click", handler, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
|
async function loadSettingsUi(extensionPopup: HTMLElement): Promise<void> {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
|
|||||||
animate(1, 0, {
|
animate(1, 0, {
|
||||||
onUpdate: (progress) => {
|
onUpdate: (progress) => {
|
||||||
extensionPopup.style.opacity = Math.max(0, progress).toString();
|
extensionPopup.style.opacity = Math.max(0, progress).toString();
|
||||||
extensionPopup.style.transform = `scale(${Math.max(0, progress)})`;
|
|
||||||
},
|
},
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 520,
|
stiffness: 520,
|
||||||
@@ -22,7 +21,6 @@ export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
extensionPopup.style.opacity = "0";
|
extensionPopup.style.opacity = "0";
|
||||||
extensionPopup.style.transform = "scale(0)";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsPopup.triggerClose();
|
settingsPopup.triggerClose();
|
||||||
|
|||||||
@@ -10,22 +10,34 @@ import { delay } from "./delay";
|
|||||||
|
|
||||||
export function setupSettingsButton() {
|
export function setupSettingsButton() {
|
||||||
const AddedSettings = document.getElementById("AddedSettings");
|
const AddedSettings = document.getElementById("AddedSettings");
|
||||||
const extensionPopup = document.getElementById("ExtensionPopup");
|
if (!AddedSettings) return;
|
||||||
if (!AddedSettings || !extensionPopup) return;
|
|
||||||
|
// Avoid stacking duplicate listeners if Engage remounts the toolbar.
|
||||||
|
if (AddedSettings.dataset.bsplusSettingsBound === "1") return;
|
||||||
|
AddedSettings.dataset.bsplusSettingsBound = "1";
|
||||||
|
|
||||||
AddedSettings.addEventListener("click", async () => {
|
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) {
|
if (SettingsClicked) {
|
||||||
closeExtensionPopup(extensionPopup as HTMLElement);
|
closeExtensionPopup(extensionPopup);
|
||||||
} else {
|
} else {
|
||||||
await renderSettingsIfNeeded();
|
await renderSettingsIfNeeded();
|
||||||
|
|
||||||
await delay(30);
|
await delay(30);
|
||||||
|
|
||||||
|
extensionPopup.style.transform = "none";
|
||||||
if (settingsState.animations) {
|
if (settingsState.animations) {
|
||||||
animate(0, 1, {
|
animate(0, 1, {
|
||||||
onUpdate: (progress) => {
|
onUpdate: (progress) => {
|
||||||
extensionPopup.style.opacity = progress.toString();
|
extensionPopup.style.opacity = progress.toString();
|
||||||
extensionPopup.style.transform = `scale(${progress})`;
|
|
||||||
},
|
},
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 280,
|
stiffness: 280,
|
||||||
@@ -33,9 +45,7 @@ export function setupSettingsButton() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
extensionPopup.style.opacity = "1";
|
extensionPopup.style.opacity = "1";
|
||||||
extensionPopup.style.transform = "scale(1)";
|
extensionPopup.style.transition = "opacity 0s linear";
|
||||||
extensionPopup.style.transition =
|
|
||||||
"opacity 0s linear, transform 0s linear";
|
|
||||||
}
|
}
|
||||||
extensionPopup.classList.remove("hide");
|
extensionPopup.classList.remove("hide");
|
||||||
changeSettingsClicked(true);
|
changeSettingsClicked(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user