fix: svelte settings Sync

This commit is contained in:
sethburkart123
2024-09-04 09:42:07 +10:00
parent e4ba89073c
commit c008b32efa
15 changed files with 220 additions and 233 deletions
+1
View File
@@ -48,6 +48,7 @@ var IsSEQTAPage = false
const hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software')
init()
async function init() {
CheckForMenuList()
const hasSEQTATitle = document.title.includes('SEQTA Learn')
+1 -1
View File
@@ -1,5 +1,5 @@
import browser from 'webextension-polyfill'
import { SettingsState } from "@/types/storage";
import type { SettingsState } from "@/types/storage";
export const openDB = () => {
return new Promise((resolve, reject) => {
@@ -2,15 +2,18 @@ import browser from 'webextension-polyfill';
import type { SettingsState } from '@/types/storage';
type ChangeListener = (newValue: any, oldValue: any) => void;
type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
class StorageManager {
private static instance: StorageManager;
private data: SettingsState;
private listeners: { [key: string]: ChangeListener[] };
private globalListeners: GlobalChangeListener[];
private constructor() {
this.data = {} as SettingsState;
this.listeners = {};
this.globalListeners = [];
this.loadFromStorage();
const handler: ProxyHandler<StorageManager> = {
@@ -57,6 +60,11 @@ class StorageManager {
await instance.loadFromStorage();
return instance;
}
public setKey<K extends keyof SettingsState>(key: K, value: SettingsState[K]): void {
this.data[key] = value;
this.saveToStorage();
}
private async loadFromStorage(): Promise<void> {
const result = await browser.storage.local.get();
@@ -85,6 +93,9 @@ class StorageManager {
listener(newValue, oldValue);
}
}
for (const listener of this.globalListeners) {
listener(newValue, oldValue, key);
}
}
}
});
@@ -101,6 +112,22 @@ class StorageManager {
}
this.listeners[prop].push(listener);
}
/**
* Register a listener for any setting.
* @param listener The listener to call when any setting changes -> takes two arguments, (newValue, oldValue)
*/
public registerGlobal(listener: GlobalChangeListener): void {
this.globalListeners.push(listener);
}
/**
* Get all settings.
* @returns All settings.
*/
public getAll(): SettingsState {
return this.data;
}
}
export const settingsState = StorageManager.getInstance();
+36 -37
View File
@@ -1,64 +1,63 @@
<script>
import { settingsState } from '../state/SettingsState.ts';
<script lang="ts">
import { animate, spring } from 'motion';
import './Switch.css'
import { onMount } from "svelte"
import { delay } from "../../seqta/utils/delay"
import { onMount } from "svelte";
export let setting;
export let onChange = () => {}
let { state, onChange } = $props<{ state: boolean, onChange: (newState: boolean) => void }>();
let handle: HTMLElement | null = null;
const toggleSwitch = () => {
const newIsOn = !$settingsState[setting]
onChange(newIsOn)
}
onChange(!state);
};
$effect(() => {
console.log('state', state);
});
const springParams = {
type: 'spring',
stiffness: 700,
damping: 30,
}
};
let handle;
const animation = (enabled) => {
const animateSwitch = (enabled: boolean) => {
if (handle) {
animate(
handle,
{ x: enabled ? 20 : 0 },
{
x: enabled ? 24 : 0,
},
{
easing: spring({ stiffness: 500, damping: 30 })
easing: spring(springParams),
}
)
);
}
}
};
$: ((enabled) => {
if (handle) {
animate(
handle,
{ x: enabled ? 24 : 0 },
{ easing: spring({ stiffness: 500, damping: 30 }) }
)
}
})($settingsState[setting])
// Trigger animation whenever state changes
$effect(() => animateSwitch(state));
onMount(() => {
// Initialize the position of the switch
animateSwitch(state);
});
</script>
<div
id={setting}
class="flex w-14 p-1 cursor-pointer transition rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch"
data-ison={$settingsState[setting]}
on:click={toggleSwitch}
on:keydown={(e) => e.key === "Enter" && toggleSwitch()}
data-ison={state}
onclick={toggleSwitch}
onkeydown={(e) => e.key === "Enter" && toggleSwitch()}
role="switch"
aria-checked={$settingsState[setting]}
aria-checked={state}
tabindex="0"
>
<div
bind:this={handle}
class="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
/>
</div>
></div>
</div>
<style>
.dark .switch[data-ison="true"],
.switch[data-ison="true"] {
background-color: #30D259;
}
</style>
@@ -1,33 +1,41 @@
<script>
<script lang="ts">
// @ts-expect-error umm idk
import { MotionDiv } from 'svelte-motion';
import { onMount, onDestroy } from 'svelte';
import { writable, derived } from 'svelte/store';
import './TabbedContainer.css';
import type { Component } from 'svelte'
import { onMount } from 'svelte';
export let tabs = [];
let activeTab = writable(0);
const hoveredTab = writable(null);
const position = writable(0);
let tabWidth = 0;
let containerRef;
let { tabs } = $props<{ tabs: { title: string, Content: Component }[] }>();
let activeTab = $state(0);
let hoveredTab = $state<number | null>(null);
let containerRef: HTMLElement | null = null;
let tabWidth = $state(0);
const springTransition = { type: 'spring', stiffness: 250, damping: 25 };
// Calculate tabWidth dynamically based on tabs length
onMount(() => {
const updateTabWidth = () => {
tabWidth = tabs.length > 0 ? 100 / tabs.length : 0;
if (!containerRef) return;
containerRef.style.setProperty('--tab-width', `${tabWidth}%`);
};
const calcXPos = (index: number | null) => {
if (containerRef) {
tabWidth = 100 / tabs.length;
document.documentElement.style.setProperty('--tab-width', `${tabWidth}%`);
calcXPos = (index) => tabWidth * (index !== null ? index : $activeTab) * (containerRef !== null ? containerRef.getBoundingClientRect().width : 0) / 100;
return tabWidth * (index !== null ? index : activeTab) * containerRef.getBoundingClientRect().width / 100;
}
return 0;
};
// Listen for messages
const handleMessage = (event) => {
$effect(() => {
calcXPos(hoveredTab);
});
onMount(() => {
updateTabWidth();
const handleMessage = (event: MessageEvent) => {
if (event.data === "popupClosed") {
activeTab.set(0);
activeTab = 0;
}
};
window.addEventListener("message", handleMessage);
@@ -37,23 +45,28 @@
};
});
let calcXPos = (index) => tabWidth * (index !== null ? index : $activeTab);
/* $effect(() => {
if (tabs.length > 0) {
updateTabWidth();
}
}); */
</script>
<div bind:this={containerRef} class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4">
<div bind:this={containerRef} class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 tab-width-container">
<div class="hidden tab-width"></div>
<div class="relative flex">
<MotionDiv
class="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] tab-width rounded-full opacity-40"
animate={{ x: calcXPos($hoveredTab) }}
class="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] rounded-full opacity-40"
animate={{ x: calcXPos(hoveredTab) }}
style="width: var(--tab-width)"
transition={springTransition}
/>
{#each tabs as { title }, index}
<button
class="relative z-10 flex-1 px-4 py-2"
on:click={() => activeTab.set(index)}
on:mouseenter={() => hoveredTab.set(index)}
on:mouseleave={() => hoveredTab.set(null)}
onclick={() => activeTab = index}
onmouseenter={() => hoveredTab = index}
onmouseleave={() => hoveredTab = null}
>
{title}
</button>
@@ -62,22 +75,22 @@
</div>
<div class="h-full px-4 overflow-y-scroll overflow-x-clip">
<MotionDiv
animate={{ x: `${-$activeTab * 100}%` }}
animate={{ x: `${-activeTab * 100}%` }}
transition={springTransition}
>
<div class="flex">
{#each tabs as { content }, index}
<div class="absolute w-full transition-opacity duration-300 pb-4 {$activeTab === index ? 'opacity-100' : 'opacity-0'}"
style="left: {index * 100}%;">
<svelte:component this={content} />
</div>
{#each tabs as { Content }, index}
<div class="absolute w-full transition-opacity duration-300 pb-4 {activeTab === index ? 'opacity-100' : 'opacity-0'}"
style="left: {index * 100}%;">
<Content />
</div>
{/each}
</div>
</MotionDiv>
</div>
<style>
:root {
--tab-width: 0px;
.tab-width {
width: var(--tab-width);
}
</style>
+5 -4
View File
@@ -1,18 +1,19 @@
// @ts-expect-error - Svelte Hash Router is not typed (yet)
import { routes } from 'svelte-hash-router'
import App from './+layout.svelte';
//import App from './+layout.svelte';
import Settings from './pages/settings.svelte';
import styles from './index.css?inline';
import { mount } from 'svelte';
export default function initSvelteInterface(shadow: ShadowRoot) {
console.log(shadow)
routes.set({
/* routes.set({
'settings': Settings,
'*': Settings
})
}) */
const app = new App({
const app = mount(Settings, {
target: shadow,
});
+5 -8
View File
@@ -11,13 +11,6 @@
};
let standalone = false;
// Define the tabs array
const tabs = [
{ title: 'Settings', content: Settings },
{ title: 'Shortcuts', content: Shortcuts },
{ title: 'Themes', content: Theme },
];
</script>
<div class="relative flex flex-col w-[384px] shadow-2xl gap-2 bg-white {standalone ? '' : 'rounded-xl'} h-[100vh] overflow-clip dark:bg-zinc-800 dark:text-white">
@@ -27,5 +20,9 @@
<button on:click={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700"></button>
</div>
<!-- <Picker /> -->
<TabbedContainer {tabs} />
<TabbedContainer tabs={[
{ title: 'Settings', Content: Settings },
{ title: 'Shortcuts', Content: Shortcuts },
{ title: 'Themes', Content: Theme },
]} />
</div>
@@ -1,66 +1,62 @@
<script lang="ts">
import Switch from "../../components/Switch.svelte"
import Button from "../../components/Button.svelte"
import PickerSwatch from "../../components/PickerSwatch.svelte"
//import PickerSwatch from "../../components/PickerSwatch.svelte"
import Slider from "../../components/Slider.svelte"
import browser from "webextension-polyfill"
import type { SettingsList } from "../../types/SettingsProps"
import { setSettingsValue } from "../../state/SettingsState"
import { createSettingsState } from "../../state/SettingsStore.svelte.ts"
const settingsStore = createSettingsState();
let test = $state(false);
const settings: SettingsList[] = [
{
title: "Transparency Effects",
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
id: 1,
component: Switch,
Component: Switch,
props: {
state: 'transparencyEffects',
onChange: (isOn: boolean) => setSettingsValue('transparencyEffects', isOn)
/* state: $settingsStore.transparencyEffects,
onChange: (isOn: boolean) => settingsStore.setKey('transparencyEffects', isOn) */
state: test,
onChange: (isOn: boolean) => test = isOn
}
},
{
title: "Animated Background",
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
id: 2,
component: Switch,
Component: Switch as any,
props: {
state: 'animatedBackground',
onChange: (isOn: boolean) => setSettingsValue('animatedBackground', isOn)
state: $settingsStore.animatedbk,
onChange: (isOn: boolean) => settingsStore.setKey('animatedbk', isOn)
}
},
{
title: "Animated Background Speed",
description: "Controls the speed of the animated background.",
id: 3,
component: Slider,
Component: Slider,
props: {
state: 'animatedBackgroundSpeed',
onChange: (value: number) => setSettingsValue('animatedBackgroundSpeed', `${value}`)
state: $settingsStore.bksliderinput,
onChange: (value: number) => settingsStore.setKey('bksliderinput', `${value}`)
}
},
{
/* {
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn.",
id: 4,
component: PickerSwatch
},
{
title: "Telemetry",
description: "Enables/disables error collecting.",
id: 5,
component: Switch,
props: {
state: 'telemetry',
onChange: (isOn: boolean) => setSettingsValue('telemetry', isOn)
}
},
Component: PickerSwatch
}, */
{
title: "Edit Sidebar Layout",
description: "Customise the sidebar layout.",
id: 6,
component: Button,
Component: Button,
props: {
onClick: () => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' }),
text: "Edit"
@@ -70,49 +66,48 @@
title: "Notification Collector",
description: "Uncaps the 9+ limit for notifications, showing the real number.",
id: 7,
component: Switch,
Component: Switch,
props: {
state: 'notificationCollector',
onChange: (isOn: boolean) => setSettingsValue('notificationCollector', isOn)
state: $settingsStore.notificationcollector,
onChange: (isOn: boolean) => settingsStore.setKey('notificationcollector', isOn)
}
},
{
title: "Lesson Alerts",
description: "Sends a native browser notification ~5 minutes prior to lessons.",
id: 8,
component: Switch,
Component: Switch,
props: {
state: 'lessonAlerts',
onChange: (isOn: boolean) => setSettingsValue('lessonAlerts', isOn)
state: $settingsStore.lessonalert,
onChange: (isOn: boolean) => settingsStore.setKey('lessonalert', isOn)
}
},
{
title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features",
id: 9,
component: Switch,
Component: Switch,
props: {
state: 'betterSEQTAPlus',
onChange: (isOn: boolean) => setSettingsValue('betterSEQTAPlus', isOn)
state: $settingsStore.onoff,
onChange: (isOn: boolean) => settingsStore.setKey('onoff', isOn)
}
}
];
</script>
<div class="flex flex-col -mt-4 overflow-y-scroll divide-y divide-zinc-100 dark:divide-zinc-700">
{#each settings as { title, description, component: Component, props, id } (id)}
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">{title}</h2>
<p class="text-xs">{description}</p>
<Switch state={$settingsStore.DarkMode} onChange={(isOn: boolean) => settingsStore.setKey('DarkMode', isOn)} />
{#if settings}
{#each settings as { title, description, Component, props, id } (id)}
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">{title}</h2>
<p class="text-xs">{description}</p>
</div>
<div>
<Component {...props} />
</div>
</div>
<div>
{#if props?.state !== undefined}
<svelte:component this={Component} {...props} bind:setting={props.state} />
{:else}
<svelte:component this={Component} {...props} />
{/if}
</div>
</div>
{/each}
{/each}
{/if}
</div>
@@ -1,91 +0,0 @@
import browser from "webextension-polyfill";
import type { MainConfig, SettingsState } from "../types/AppProps";
import { writable } from "svelte/store";
const initialState: SettingsState = {
notificationCollector: false,
lessonAlerts: false,
telemetry: false,
animatedBackground: false,
animatedBackgroundSpeed: '0',
customThemeColor: '',
betterSEQTAPlus: false,
shortcuts: [],
customshortcuts: [],
transparencyEffects: false,
theme: ''
};
const keyToStateMap: { [key: string]: string } = {
notificationcollector: 'notificationCollector',
lessonalert: 'lessonAlerts',
telemetry: 'telemetry',
animatedbk: 'animatedBackground',
bksliderinput: 'animatedBackgroundSpeed',
selectedColor: 'customThemeColor',
onoff: 'betterSEQTAPlus',
shortcuts: 'shortcuts',
customshortcuts: 'customshortcuts',
transparencyEffects: 'transparencyEffects'
};
const stateToKeyMap = Object.fromEntries(
Object.entries(keyToStateMap).map(([key, value]) => [value, key])
);
const storageChangeListener = async (changes: browser.Storage.StorageChange) => {
for (const [key, { newValue }] of Object.entries(changes)) {
const stateKey = keyToStateMap[key] || key;
if (stateKey === 'DarkMode') {
if (newValue) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
}
settingsState.update((prevState) => ({ ...prevState, [stateKey]: newValue }));
}
}
const initialStorageLoad = async (storage: MainConfig) => {
for (const [key, value] of Object.entries(storage)) {
const stateKey = keyToStateMap[key] || key;
if (stateKey === 'DarkMode') {
if (value) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
}
settingsState.update((prevState) => ({ ...prevState, [stateKey]: value }));
}
}
const settingsSubscription = (/* set: (value: SettingsState) => void */) => {
settingsState.subscribe((newState) => {
const stateToSave = Object.fromEntries(
Object.entries(newState).map(([key, value]) => [stateToKeyMap[key] || key, value])
);
browser.storage.local.set(stateToSave);
/* set(newState); */
});
}
export const initializeListeners = async () => {
const result = await browser.storage.local.get() as MainConfig;
await initialStorageLoad(result);
settingsSubscription();
browser.storage.onChanged.addListener(storageChangeListener);
}
export const settingsState = writable(initialState);
export const setSettingsValue = <K extends keyof SettingsState>(key: K, value: SettingsState[K]) => {
settingsState.update((prevState) => ({ ...prevState, [key]: value }));
}
@@ -0,0 +1,45 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { SettingsState } from '@/types/storage';
export function createSettingsState() {
let settings = $state<SettingsState>(settingsState);
const subscribers = new Set<(value: SettingsState) => void>();
// Register a global listener to notify subscribers on any change
settingsState.registerGlobal((newValue, oldValue, key) => {
console.log('Global listener triggered:', { newValue, oldValue, key });
if (newValue !== undefined) {
settings = { ...settings, [key]: newValue };
notifySubscribers(settings);
}
});
function notifySubscribers(newValue: SettingsState) {
console.log('Notifying subscribers with:', newValue);
subscribers.forEach(subscriber => subscriber(newValue));
}
return {
get settings() { return settings; },
set(newSettings: SettingsState) {
settings = newSettings;
notifySubscribers(settings);
},
setKey<K extends keyof SettingsState>(key: K, value: SettingsState[K]) {
settings[key] = value;
settingsState.setKey(key, value);
notifySubscribers(settings);
},
subscribe(callback: (value: SettingsState) => void) {
subscribers.add(callback);
// Immediately call the callback with the current value
callback(settings);
// Return an unsubscribe function
return () => {
subscribers.delete(callback);
};
}
};
}
+2 -2
View File
@@ -1,11 +1,11 @@
import type { SettingsState } from './AppProps';
import { ComponentType } from 'svelte';
import type { Component } from 'svelte';
export interface SettingsList {
title: string;
id: number;
description: string;
component: ComponentType;
Component: Component;
props?: any;
}
export interface SettingsProps {