mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
Merge pull request #452 from BetterSEQTA/deep-reform
fix: harden extension security and plugin reliability
This commit is contained in:
@@ -92,7 +92,7 @@
|
||||
bind:this={background}
|
||||
class="flex absolute top-0 left-0 z-50 justify-center items-center w-full h-full cursor-pointer bg-black/50"
|
||||
onclick={handleBackgroundClick}
|
||||
onkeydown={(e) => { if (e.key === "Enter") handleBackgroundClick; }}
|
||||
onkeydown={(e) => { if (e.key === "Enter") handleBackgroundClick(e as unknown as MouseEvent) }}
|
||||
>
|
||||
<div
|
||||
bind:this={content}
|
||||
|
||||
@@ -22,15 +22,13 @@
|
||||
});
|
||||
|
||||
async function upload() {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (!token) return;
|
||||
if (!cloudState.isLoggedIn) return;
|
||||
busy = true;
|
||||
statusError = null;
|
||||
statusMessage = null;
|
||||
try {
|
||||
const res = (await browser.runtime.sendMessage({
|
||||
type: "cloudSettingsUpload",
|
||||
token,
|
||||
})) as { success?: boolean; error?: string };
|
||||
if (res?.success) {
|
||||
statusMessage = "Settings uploaded.";
|
||||
@@ -49,15 +47,13 @@
|
||||
}
|
||||
|
||||
async function confirmDownload() {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (!token) return;
|
||||
if (!cloudState.isLoggedIn) return;
|
||||
busy = true;
|
||||
statusError = null;
|
||||
statusMessage = null;
|
||||
try {
|
||||
const res = (await browser.runtime.sendMessage({
|
||||
type: "cloudSettingsDownload",
|
||||
token,
|
||||
})) as { success?: boolean; error?: string; notFound?: boolean };
|
||||
if (res?.success) {
|
||||
statusMessage = "Settings restored.";
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
let editor = $state<HTMLDivElement | null>(null)
|
||||
let view: EditorView | null = null;
|
||||
let unsubSettings: (() => void) | undefined;
|
||||
let editorTheme = new Compartment();
|
||||
let { value, onChange, className } = $props<{value: string, onChange: (value: string) => void, className?: string}>()
|
||||
|
||||
@@ -73,7 +74,7 @@
|
||||
view = createEditorView(state, editor as HTMLElement);
|
||||
}
|
||||
|
||||
settingsState.subscribe((settings) => {
|
||||
unsubSettings = settingsState.subscribe((settings) => {
|
||||
if (view) {
|
||||
view.dispatch({
|
||||
effects: editorTheme.reconfigure(
|
||||
@@ -85,6 +86,7 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubSettings?.();
|
||||
if (view) {
|
||||
view.destroy();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import ColourPicker from './ColourPicker.tsx';
|
||||
import ReactAdapter from './utils/ReactAdapter.svelte';
|
||||
import type { Component } from 'svelte'
|
||||
import { animate } from 'motion';
|
||||
import { delay } from '@/seqta/utils/delay.ts'
|
||||
|
||||
@@ -15,6 +14,19 @@
|
||||
|
||||
let background = $state<HTMLDivElement | null>(null);
|
||||
let content = $state<HTMLDivElement | null>(null);
|
||||
let ReactAdapter = $state<Component | null>(null);
|
||||
let ColourPickerEl = $state<unknown>(null);
|
||||
let pickerReady = $state(false);
|
||||
|
||||
const loadPicker = async () => {
|
||||
const [adapterMod, pickerMod] = await Promise.all([
|
||||
import('./utils/ReactAdapter.svelte'),
|
||||
import('./ColourPicker.tsx'),
|
||||
]);
|
||||
ReactAdapter = adapterMod.default;
|
||||
ColourPickerEl = pickerMod.default;
|
||||
pickerReady = true;
|
||||
};
|
||||
|
||||
const closePicker = async () => {
|
||||
if (standalone) return;
|
||||
@@ -37,28 +49,30 @@
|
||||
);
|
||||
|
||||
await delay(400);
|
||||
hidePicker();
|
||||
hidePicker?.();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (standalone) return;
|
||||
if (!background || !content) return;
|
||||
void loadPicker().then(() => {
|
||||
if (standalone) return;
|
||||
if (!background || !content) return;
|
||||
|
||||
animate(
|
||||
background,
|
||||
{ opacity: [0, 1] },
|
||||
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
|
||||
);
|
||||
animate(
|
||||
background,
|
||||
{ opacity: [0, 1] },
|
||||
{ duration: 0.3, ease: [0.4, 0, 0.2, 1] }
|
||||
);
|
||||
|
||||
animate(
|
||||
content,
|
||||
{ scale: [0.4, 1], opacity: [0, 1] },
|
||||
{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30
|
||||
}
|
||||
);
|
||||
animate(
|
||||
content,
|
||||
{ scale: [0.4, 1], opacity: [0, 1] },
|
||||
{
|
||||
type: 'spring',
|
||||
stiffness: 400,
|
||||
damping: 30
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const handleEscapeKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -82,7 +96,9 @@
|
||||
|
||||
{#if standalone}
|
||||
<div class="h-auto overflow-clip rounded-xl">
|
||||
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||
{#if pickerReady && ReactAdapter && ColourPickerEl}
|
||||
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPickerEl} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
@@ -90,13 +106,15 @@
|
||||
bind:this={background}
|
||||
class="flex absolute top-0 left-0 z-50 justify-center items-center w-full h-full shadow-2xl cursor-pointer bg-black/20 border border-[#DDDDDD]/30 dark:border-[#38373D]/30"
|
||||
onclick={handleBackgroundClick}
|
||||
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter') handleBackgroundClick(e as unknown as MouseEvent) }}
|
||||
>
|
||||
<div
|
||||
bind:this={content}
|
||||
class="p-4 h-auto bg-white rounded-xl border shadow-lg cursor-auto dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||
>
|
||||
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
|
||||
{#if pickerReady && ReactAdapter && ColourPickerEl}
|
||||
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPickerEl} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
const startRecording = () => {
|
||||
isRecording = true;
|
||||
recordedKeys.clear();
|
||||
recordedKeys = new Set();
|
||||
inputElement?.focus();
|
||||
};
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
if (recordedKeys.has('esc')) {
|
||||
onChange('');
|
||||
isRecording = false;
|
||||
recordedKeys.clear();
|
||||
recordedKeys = new Set();
|
||||
inputElement?.blur();
|
||||
return;
|
||||
}
|
||||
@@ -113,10 +113,16 @@
|
||||
}
|
||||
|
||||
isRecording = false;
|
||||
recordedKeys.clear();
|
||||
recordedKeys = new Set();
|
||||
inputElement?.blur();
|
||||
};
|
||||
|
||||
const addRecordedKey = (key: string) => {
|
||||
const next = new Set(recordedKeys);
|
||||
next.add(key);
|
||||
recordedKeys = next;
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!isRecording) return;
|
||||
|
||||
@@ -126,14 +132,14 @@
|
||||
const key = formatKeyForHotkey(e.key);
|
||||
|
||||
// Add modifiers
|
||||
if (e.ctrlKey) recordedKeys.add('ctrl');
|
||||
if (e.metaKey) recordedKeys.add('cmd');
|
||||
if (e.altKey) recordedKeys.add('alt');
|
||||
if (e.shiftKey) recordedKeys.add('shift');
|
||||
if (e.ctrlKey) addRecordedKey('ctrl');
|
||||
if (e.metaKey) addRecordedKey('cmd');
|
||||
if (e.altKey) addRecordedKey('alt');
|
||||
if (e.shiftKey) addRecordedKey('shift');
|
||||
|
||||
// Add the main key (ignore modifier keys themselves)
|
||||
if (!['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
|
||||
recordedKeys.add(key);
|
||||
addRecordedKey(key);
|
||||
}
|
||||
|
||||
// Auto-stop recording if we have a main key
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
let { state, onChange, min = 0, max = 100, step = 1 } = $props<{
|
||||
let { state = $bindable(), onChange, min = 0, max = 100, step = 1 } = $props<{
|
||||
state: number,
|
||||
onChange: (value: number) => void,
|
||||
min?: number,
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container">
|
||||
<div class="flex flex-col h-full min-h-0">
|
||||
<div class="top-0 z-10 shrink-0 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container" role="tablist">
|
||||
<div bind:this={containerRef} class="flex relative">
|
||||
<MotionDiv
|
||||
class="absolute top-0 left-0 z-0 h-full bg-gradient-to-tr dark:from-[#38373D]/80 dark:to-[#38373D] from-[#DDDDDD]/80 to-[#DDDDDD] rounded-full opacity-40 tab-width"
|
||||
@@ -48,6 +48,8 @@
|
||||
/>
|
||||
{#each tabs as { title }, index}
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={activeTab === index}
|
||||
class="relative z-10 flex-1 px-4 py-2 focus-visible:outline-none"
|
||||
onclick={() => activeTab = index}
|
||||
>
|
||||
@@ -56,21 +58,17 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden px-4 h-full">
|
||||
<MotionDiv
|
||||
class="h-full"
|
||||
animate={{ x: `${-activeTab * 100}%` }}
|
||||
transition={springTransition}
|
||||
>
|
||||
<div class="flex">
|
||||
{#each tabs as { Content, props }, index}
|
||||
<div class="absolute focus:outline-none w-full pt-2 transition-opacity duration-300 overflow-y-scroll no-scrollbar pb-2 h-full tab {activeTab === index ? 'opacity-100 active' : 'opacity-0'}"
|
||||
style="left: {index * 100}%;">
|
||||
<div style="left: {index * 100}%;" class="fixed top-0 w-full h-8 bg-gradient-to-b to-transparent pointer-events-none z-[100] from-white dark:from-zinc-800 dark:to-transparent"></div>
|
||||
<Content {...props} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</MotionDiv>
|
||||
<div class="overflow-hidden px-4 flex-1 min-h-0">
|
||||
{#each tabs as { Content, props }, index (index)}
|
||||
{#if activeTab === index}
|
||||
<div
|
||||
role="tabpanel"
|
||||
class="focus:outline-none w-full h-full min-h-0 pt-2 overflow-y-auto no-scrollbar pb-6 tab active"
|
||||
>
|
||||
<div class="sticky top-0 w-full h-8 bg-gradient-to-b to-transparent pointer-events-none z-[100] from-white dark:from-zinc-800 dark:to-transparent"></div>
|
||||
<Content {...props} />
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import logo from '@/resources/icons/betterseqta-dark-full.png';
|
||||
import logoDark from '@/resources/icons/betterseqta-light-full.png';
|
||||
import { closeStore } from '@/seqta/ui/renderStore'
|
||||
import browser from 'webextension-polyfill';
|
||||
import CloudHeader from './CloudHeader.svelte';
|
||||
|
||||
const handleCloseStore = () => {
|
||||
void import('@/seqta/ui/renderStore').then((module) => module.closeStore());
|
||||
};
|
||||
|
||||
// Props
|
||||
let { searchTerm, setSearchTerm, darkMode, activeTab, setActiveTab } = $props<{
|
||||
searchTerm: string,
|
||||
@@ -64,7 +67,7 @@
|
||||
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
onclick={closeStore}
|
||||
onclick={handleCloseStore}
|
||||
class="p-1 px-3"
|
||||
>
|
||||
<span class="text-2xl font-IconFamily"></span>
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
|
||||
<div
|
||||
onclick={onClick}
|
||||
onkeydown={onClick}
|
||||
tabindex="-1"
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') onClick() }}
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="relative w-16 h-16 cursor-pointer rounded-xl transition ring-3 dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}"
|
||||
>
|
||||
{#if isEditMode}
|
||||
<div
|
||||
tabindex="-1"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onclick={onDelete}
|
||||
onkeydown={onDelete}
|
||||
onclick={(e) => { e.stopPropagation(); onDelete() }}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); onDelete() } }}
|
||||
>
|
||||
<div class="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
|
||||
@@ -174,18 +174,19 @@
|
||||
if (parentElement) {
|
||||
observer = new MutationObserver(checkActiveClass);
|
||||
observer.observe(parentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
backgroundUpdates.removeListener(syncBackgrounds);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
backgroundUpdates.removeListener(syncBackgrounds);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
observer?.disconnect();
|
||||
backgrounds.forEach((bg) => {
|
||||
if (bg.url) URL.revokeObjectURL(bg.url);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import browser from 'webextension-polyfill'
|
||||
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
||||
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
||||
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
@@ -21,11 +19,14 @@
|
||||
let prevLoggedIn = $state(false);
|
||||
let showSignInModal = $state(false);
|
||||
|
||||
cloudAuth.subscribe((s) => {
|
||||
const now = s.isLoggedIn;
|
||||
if (now && !prevLoggedIn && themes) void fetchThemes();
|
||||
prevLoggedIn = now;
|
||||
cloudLoggedIn = now;
|
||||
$effect(() => {
|
||||
const unsub = cloudAuth.subscribe((s) => {
|
||||
const now = s.isLoggedIn;
|
||||
if (now && !prevLoggedIn && themes) void fetchThemes();
|
||||
prevLoggedIn = now;
|
||||
cloudLoggedIn = now;
|
||||
});
|
||||
return unsub;
|
||||
});
|
||||
|
||||
const handleThemeClick = async (theme: CustomTheme, e: MouseEvent) => {
|
||||
@@ -102,17 +103,14 @@
|
||||
selectedTheme: themeManager.getSelectedThemeId() || '',
|
||||
}
|
||||
if (themes && cloudLoggedIn) {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (token) {
|
||||
const status: Record<string, boolean> = {};
|
||||
await Promise.all(
|
||||
themes.themes.map(async (t) => {
|
||||
try {
|
||||
const res = (await browser.runtime.sendMessage({
|
||||
type: 'fetchThemeDetails',
|
||||
themeId: t.id,
|
||||
token,
|
||||
})) as { success?: boolean; data?: { theme?: { is_favorited?: boolean } } };
|
||||
const status: Record<string, boolean> = {};
|
||||
await Promise.all(
|
||||
themes.themes.map(async (t) => {
|
||||
try {
|
||||
const res = (await browser.runtime.sendMessage({
|
||||
type: 'fetchThemeDetails',
|
||||
themeId: t.id,
|
||||
})) as { success?: boolean; data?: { theme?: { is_favorited?: boolean } } };
|
||||
if (res?.success && res?.data?.theme) {
|
||||
status[t.id] = !!res.data.theme.is_favorited;
|
||||
}
|
||||
@@ -122,25 +120,32 @@
|
||||
})
|
||||
);
|
||||
favoriteStatus = status;
|
||||
}
|
||||
} else {
|
||||
favoriteStatus = {};
|
||||
}
|
||||
}
|
||||
|
||||
const openStorePage = async () => {
|
||||
const { OpenStorePage } = await import('@/seqta/ui/renderStore')
|
||||
OpenStorePage()
|
||||
}
|
||||
|
||||
const openThemeCreator = async (themeId?: string) => {
|
||||
const { OpenThemeCreator } = await import('@/plugins/built-in/themes/ThemeCreator')
|
||||
OpenThemeCreator(themeId)
|
||||
closeExtensionPopup()
|
||||
}
|
||||
|
||||
const handleToggleFavorite = async (theme: CustomTheme, e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!cloudLoggedIn) {
|
||||
showSignInModal = true;
|
||||
return;
|
||||
}
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (!token) return;
|
||||
const isFavorite = !favoriteStatus[theme.id];
|
||||
const result = (await browser.runtime.sendMessage({
|
||||
type: 'cloudFavorite',
|
||||
themeId: theme.id,
|
||||
token,
|
||||
action: isFavorite ? 'favorite' : 'unfavorite',
|
||||
})) as { success?: boolean };
|
||||
if (result?.success) {
|
||||
@@ -216,8 +221,8 @@
|
||||
</div>
|
||||
<div
|
||||
class="absolute z-20 flex w-8 h-8 p-2 text-white transition-all rounded-full delay-[20ms] opacity-0 top-1/4 right-2 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-1/2 -translate-y-1/2"
|
||||
onclick={(event) => { event.stopPropagation(); OpenThemeCreator(theme.id); closeExtensionPopup() }}
|
||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') OpenThemeCreator(theme.id); closeExtensionPopup() }}
|
||||
onclick={(event) => { event.stopPropagation(); void openThemeCreator(theme.id) }}
|
||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') void openThemeCreator(theme.id) }}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
@@ -265,7 +270,7 @@
|
||||
{/if}
|
||||
|
||||
<button
|
||||
onclick={() => OpenStorePage()}
|
||||
onclick={() => void openStorePage()}
|
||||
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
@@ -273,7 +278,7 @@
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
|
||||
onclick={() => void openThemeCreator()}
|
||||
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<script lang="ts">
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
const e = React.createElement;
|
||||
let container: HTMLDivElement;
|
||||
let adapterProps = $props();
|
||||
let container = $state<HTMLDivElement | null>(null);
|
||||
|
||||
onMount(() => {
|
||||
const { el, children, class: _, ...props } = $$props;
|
||||
$effect(() => {
|
||||
if (!container) return;
|
||||
|
||||
const { el, children, class: className, ...rest } = adapterProps;
|
||||
try {
|
||||
ReactDOM.render(e(el, props, children), container);
|
||||
ReactDOM.render(e(el, rest, children), container);
|
||||
} catch (err) {
|
||||
console.warn(`react-adapter failed to mount.`, { err });
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (!container) return;
|
||||
try {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
} catch (err) {
|
||||
@@ -24,4 +28,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container} class={$$props.class}></div>
|
||||
<div bind:this={container} class={adapterProps.class}></div>
|
||||
|
||||
@@ -1,53 +1 @@
|
||||
type SettingsPopupCallback = () => void;
|
||||
|
||||
/**
|
||||
* This is a singleton that triggers an update when the settings popup is closed.
|
||||
* This is used to close the colour picker.
|
||||
* Usage:
|
||||
* settingsPopup.addListener(() => {
|
||||
* console.log('Settings popup closed');
|
||||
* });
|
||||
*/
|
||||
class SettingsPopup {
|
||||
private static instance: SettingsPopup;
|
||||
private listeners: Set<SettingsPopupCallback> = new Set();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): SettingsPopup {
|
||||
if (!SettingsPopup.instance) {
|
||||
SettingsPopup.instance = new SettingsPopup();
|
||||
}
|
||||
return SettingsPopup.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback function to be invoked when the settings popup is closed.
|
||||
*
|
||||
* @param {SettingsPopupCallback} callback The function to call when the settings popup closes.
|
||||
* This callback takes no arguments and returns void.
|
||||
*/
|
||||
public addListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a previously added callback function.
|
||||
* After calling this method, the provided callback will no longer be invoked when the settings popup closes.
|
||||
*
|
||||
* @param {SettingsPopupCallback} callback The callback function to remove from the listeners.
|
||||
*/
|
||||
public removeListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes all registered listener callbacks.
|
||||
* This method should be called when the settings popup is closed to notify all subscribed components or services.
|
||||
*/
|
||||
public triggerClose(): void {
|
||||
this.listeners.forEach((callback) => callback());
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsPopup = SettingsPopup.getInstance();
|
||||
export { settingsPopup } from "@/seqta/utils/settingsPopup";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
|
||||
@@ -14,11 +14,11 @@
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
||||
//import { OpenMinecraftServerPopup } from "@/seqta/utils/Openers/OpenMinecraftServerPopup";
|
||||
|
||||
import ColourPicker from "../components/ColourPicker.svelte";
|
||||
import type { Component } from "svelte";
|
||||
import FontPickerModal from "../components/FontPickerModal.svelte";
|
||||
import CloudPanel from "../components/CloudPanel.svelte";
|
||||
import DisclaimerModal from "../components/DisclaimerModal.svelte";
|
||||
import { settingsPopup } from "../hooks/SettingsPopup";
|
||||
import { settingsPopup } from "@/seqta/utils/settingsPopup";
|
||||
import {
|
||||
checkGithubReleaseUpdate,
|
||||
dismissNightlyUpdate,
|
||||
@@ -64,7 +64,12 @@
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
const openColourPicker = () => {
|
||||
let ColourPickerComponent = $state<Component | null>(null);
|
||||
|
||||
const openColourPicker = async () => {
|
||||
if (!ColourPickerComponent) {
|
||||
ColourPickerComponent = (await import("../components/ColourPicker.svelte")).default;
|
||||
}
|
||||
showColourPicker = true;
|
||||
};
|
||||
|
||||
@@ -108,12 +113,14 @@
|
||||
showDisclaimerModal = true;
|
||||
};
|
||||
|
||||
const closePopupsOnSettingsClose = () => {
|
||||
showColourPicker = false;
|
||||
showFontPicker = false;
|
||||
showCloudPanel = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
settingsPopup.addListener(() => {
|
||||
showColourPicker = false;
|
||||
showFontPicker = false;
|
||||
showCloudPanel = false;
|
||||
});
|
||||
settingsPopup.addListener(closePopupsOnSettingsClose);
|
||||
|
||||
if (standalone) {
|
||||
StandaloneStore.setStandalone(true);
|
||||
@@ -125,6 +132,10 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
settingsPopup.removeListener(closePopupsOnSettingsClose);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -133,10 +144,10 @@
|
||||
: ''} {standalone ? 'h-[600px]' : 'h-full rounded-xl'} overflow-clip"
|
||||
>
|
||||
<div
|
||||
class="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white"
|
||||
class="flex relative flex-col gap-2 h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white"
|
||||
>
|
||||
<div
|
||||
class="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"
|
||||
class="grid shrink-0 place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
@@ -343,22 +354,24 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<TabbedContainer
|
||||
bind:activeTab={settingsActiveTab}
|
||||
tabs={[
|
||||
{
|
||||
title: "Settings",
|
||||
Content: Settings,
|
||||
props: { showColourPicker: openColourPicker, showFontPicker: openFontPicker, showDisclaimer, showCloudPanel: openCloudPanel },
|
||||
},
|
||||
{ title: "Shortcuts", Content: Shortcuts },
|
||||
{ title: "Themes", Content: Theme },
|
||||
]}
|
||||
/>
|
||||
<div class="flex-1 min-h-0 overflow-hidden">
|
||||
<TabbedContainer
|
||||
bind:activeTab={settingsActiveTab}
|
||||
tabs={[
|
||||
{
|
||||
title: "Settings",
|
||||
Content: Settings,
|
||||
props: { showColourPicker: openColourPicker, showFontPicker: openFontPicker, showDisclaimer, showCloudPanel: openCloudPanel },
|
||||
},
|
||||
{ title: "Shortcuts", Content: Shortcuts },
|
||||
{ title: "Themes", Content: Theme },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showColourPicker}
|
||||
<ColourPicker
|
||||
{#if showColourPicker && ColourPickerComponent}
|
||||
<ColourPickerComponent
|
||||
hidePicker={() => {
|
||||
showColourPicker = false;
|
||||
}}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||
import { getSnapshotForUpload } from "@/seqta/utils/cloudSettingsSync"
|
||||
import { getStoredOverride, setApiBase } from "@/seqta/utils/DevApiBase"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let devApiBaseInput = $state<string>(getStoredOverride() ?? "")
|
||||
let devApiBaseActive = $state<string | null>(getStoredOverride())
|
||||
@@ -128,9 +129,9 @@
|
||||
await browser.storage.local.set({ [storageKey]: currentSettings });
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadPluginSettings();
|
||||
})
|
||||
onMount(() => {
|
||||
void loadPluginSettings();
|
||||
});
|
||||
|
||||
const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel } = $props<{
|
||||
showColourPicker: () => void;
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
let cloudLoggedIn = $state(cloudAuth.state.isLoggedIn);
|
||||
|
||||
cloudAuth.subscribe((s) => { cloudLoggedIn = s.isLoggedIn; });
|
||||
$effect(() => {
|
||||
const unsub = cloudAuth.subscribe((s) => { cloudLoggedIn = s.isLoggedIn; });
|
||||
return unsub;
|
||||
});
|
||||
|
||||
// State variables
|
||||
let searchTerm = $state('');
|
||||
@@ -86,13 +89,11 @@
|
||||
}
|
||||
|
||||
const toggleFavorite = async (theme: Theme) => {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
if (!token) return;
|
||||
if (!cloudLoggedIn) return;
|
||||
const isFavorite = !theme.is_favorited;
|
||||
const result = (await browser.runtime.sendMessage({
|
||||
type: 'cloudFavorite',
|
||||
themeId: theme.id,
|
||||
token,
|
||||
action: isFavorite ? 'favorite' : 'unfavorite',
|
||||
})) as { success?: boolean };
|
||||
if (result?.success) {
|
||||
@@ -119,14 +120,12 @@
|
||||
error = null;
|
||||
}
|
||||
try {
|
||||
const token = await cloudAuth.getStoredToken();
|
||||
const data = await sendMessageWithTimeout<{
|
||||
success?: boolean;
|
||||
data?: { themes: unknown[] };
|
||||
error?: string;
|
||||
}>({
|
||||
type: 'fetchThemes',
|
||||
token: token ?? undefined,
|
||||
});
|
||||
if (!data?.success || !Array.isArray(data?.data?.themes)) {
|
||||
throw new Error(data?.error || 'Failed to fetch themes');
|
||||
|
||||
Reference in New Issue
Block a user