schoolbox web extension :)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: storage state wrapper (#235)

* feat: initial prototype

* refactor: storage state wrapper

* refactor: wrap everything

* refactor: finish wrapping

* fix: plugins and snippets

* refactor: remove unused import

* refactor: remove unused utility functions

* fix: custom snippets not working

* refactor: await set value

---------

Co-authored-by: willow <42willow@pm.me>

authored by

BC-548
willow
and committed by
GitHub
ca9d685e ab507a53

+271 -311
+2 -2
src/entrypoints/background.ts
··· 37 37 }); 38 38 39 39 // watch for global toggle 40 - globalSettings.watch(async (newSettings, oldSettings) => { 40 + globalSettings.storage.watch(async (newSettings, oldSettings) => { 41 41 if (newSettings.global !== oldSettings.global) { 42 42 logger.info(`[background] Global toggle changed to ${newSettings.global}`); 43 43 // update icon ··· 120 120 } 121 121 122 122 async function updateIcon() { 123 - const global = (await globalSettings.getValue()).global; 123 + const global = (await globalSettings.storage.getValue()).global; 124 124 let iconSuffix = "-disabled"; 125 125 if (global) { 126 126 iconSuffix = "";
+3 -3
src/entrypoints/end.content.ts
··· 3 3 runAt: "document_end", 4 4 excludeMatches: EXCLUDE_MATCHES, 5 5 async main() { 6 - const settings = await globalSettings.getValue(); 7 - const urls = await schoolboxUrls.getValue(); 6 + const settings = await globalSettings.storage.getValue(); 7 + const urls = await schoolboxUrls.storage.getValue(); 8 8 9 9 if (!settings.global) return; 10 10 const footer = document.querySelector("#footer > ul"); ··· 20 20 logger.info("[end.content.ts] URL not in settings, adding..."); 21 21 if (!urls.includes(window.location.origin)) { 22 22 urls.push(window.location.origin); 23 - await schoolboxUrls.setValue(urls); 23 + await schoolboxUrls.storage.setValue(urls); 24 24 // TODO: hot reload 25 25 window.location.reload(); 26 26 }
+13 -14
src/entrypoints/popup/App.svelte
··· 19 19 }; 20 20 let flavour = $state(""); 21 21 let accent = ""; 22 - let accentRgb = ""; 23 - let settings = globalSettings.fallback; 24 - let refresh = $state(needsRefresh.fallback); 22 + let settings = globalSettings.storage.fallback; 23 + let refresh = $state(needsRefresh.storage.fallback); 25 24 26 25 async function refreshSchoolboxURLs() { 27 26 logger.info("[App.svelte] Refreshing all Schoolbox URLs"); 28 - const urls = (await schoolboxUrls.getValue()).map((url) => url.replace(/^https:\/\//, "*://") + "/*"); 27 + const urls = (await schoolboxUrls.storage.getValue()).map((url) => url.replace(/^https:\/\//, "*://") + "/*"); 29 28 const tabs = await browser.tabs.query({ url: urls }); 30 29 tabs.forEach((tab) => { 31 - browser.tabs.reload(tab.id); 30 + if (tab.id) { 31 + browser.tabs.reload(tab.id); 32 + } 32 33 }); 33 34 } 34 35 35 36 async function onBannerClick() { 36 37 refresh = false; 37 - needsRefresh.setValue(refresh); 38 + needsRefresh.storage.setValue(refresh); 38 39 refreshSchoolboxURLs(); 39 40 } 40 41 41 42 function getAccentRgb(accent: string, flavour: string) { 42 - console.log(accent, flavour); 43 - console.log(flavors); 44 - console.log(flavors[flavour].colors); 43 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 44 let x = (flavors as any)[flavour].colors[accent].rgb; 46 45 return `rgb(${x.r}, ${x.g}, ${x.b})`; 47 46 } ··· 50 49 let refreshUnwatch: () => void; 51 50 52 51 onMount(async () => { 53 - settings = await globalSettings.getValue(); 54 - refresh = await needsRefresh.getValue(); 52 + settings = await globalSettings.storage.getValue(); 53 + refresh = await needsRefresh.storage.getValue(); 55 54 accent = settings.themeAccent; 56 55 flavour = settings.themeFlavour; 57 56 document.documentElement.style.setProperty("--ctp-accent", getAccentRgb(accent, flavour)); 58 57 59 - settingsUnwatch = globalSettings.watch((newValue) => { 58 + settingsUnwatch = globalSettings.storage.watch((newValue) => { 60 59 settings = newValue; 61 60 flavour = newValue.themeFlavour; 62 61 accent = newValue.themeAccent; 63 62 64 63 document.documentElement.style.setProperty("--ctp-accent", getAccentRgb(accent, flavour)); 65 64 refresh = true; 66 - needsRefresh.setValue(refresh); 65 + needsRefresh.storage.setValue(refresh); 67 66 }); 68 - refreshUnwatch = needsRefresh.watch((newValue) => { 67 + refreshUnwatch = needsRefresh.storage.watch((newValue) => { 69 68 refresh = newValue; 70 69 }); 71 70 });
+11 -18
src/entrypoints/popup/routes/Home.svelte
··· 1 1 <script lang="ts"> 2 2 import Footer from "../components/Footer.svelte"; 3 - import { onMount } from "svelte"; 4 3 5 - let settings = $state(globalSettings.fallback); 6 - 7 - onMount(async () => { 8 - settings = await globalSettings.getValue(); 9 - }); 10 - 11 - // $inspect(settings); 12 - 13 - async function globalToggle() { 14 - settings.global = !settings.global; 15 - await globalSettings.setValue(settings); 16 - } 4 + let label = $derived(globalSettings.state.global ? "enabled" : "disabled"); 5 + let classList = $derived( 6 + globalSettings.state.global 7 + ? "bg-ctp-green hover:bg-(--ctp-accent) active:bg-ctp-red/75" 8 + : "bg-ctp-red hover:bg-(--ctp-accent) active:bg-ctp-green/75", 9 + ); 17 10 </script> 18 11 19 12 <div id="card"> 20 13 <h1 class="mb-6">Schooltape</h1> 21 14 22 15 <button 23 - class={settings.global 24 - ? "bg-ctp-green hover:bg-(--ctp-accent) active:bg-ctp-red/75" 25 - : "bg-ctp-red hover:bg-(--ctp-accent) active:bg-ctp-green/75"} 16 + class={classList} 26 17 id="toggle" 27 - onclick={globalToggle} 28 - >{settings.global ? "enabled" : "disabled"} 18 + onclick={() => { 19 + globalSettings.set({ global: !globalSettings.get().global }); 20 + }} 21 + >{label} 29 22 </button> 30 23 </div> 31 24
+14 -26
src/entrypoints/popup/routes/Plugins.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 3 2 import Title from "../components/Title.svelte"; 4 3 import Slider from "../components/inputs/Slider.svelte"; 5 - 6 - let populatedPlugins: PopulatedItem<PluginId>[] = $state([]); 7 - let pluginsToggle: boolean = $state(true); 8 - 9 - onMount(async () => { 10 - populatedPlugins = await populateItems(plugins, PLUGIN_INFO); 11 - pluginsToggle = await globalSettings.getValue().then((settings) => settings.plugins); 12 - }); 13 - 14 - async function handleToggleChange(event: CustomEvent) { 15 - let settings = await globalSettings.getValue(); 16 - settings.plugins = event.detail.checked; 17 - await globalSettings.setValue(settings); 18 - } 19 - 20 - async function togglePlugin(pluginId: PluginId, toggled: boolean): Promise<void> { 21 - await toggleItem(plugins, pluginId, toggled); 22 - } 23 4 </script> 24 5 25 6 <div id="card"> 26 - <Title title="Plugins" bind:checked={pluginsToggle} on:change={handleToggleChange} /> 7 + <Title 8 + title="Plugins" 9 + bind:checked={globalSettings.state.plugins} 10 + on:change={(event: CustomEvent) => { 11 + globalSettings.set({ plugins: event.detail.checked }); 12 + }} /> 27 13 28 14 <div class="plugins-container"> 29 - {#each populatedPlugins as plugin} 15 + {#each Object.entries(plugins) as [id, plugin] (id)} 30 16 <div class="my-4 group"> 31 17 <Slider 32 - id={plugin.id} 33 - bind:checked={plugin.toggle} 34 - on:change={() => togglePlugin(plugin.id, plugin.toggle)} 35 - text={plugin.name} 36 - description={plugin.description} 18 + {id} 19 + bind:checked={plugin.state.toggle} 20 + on:change={(event: CustomEvent) => { 21 + plugin.set({ toggle: event.detail.checked }); 22 + }} 23 + text={plugin.info?.name} 24 + description={plugin.info?.description} 37 25 size="small" /> 38 26 </div> 39 27 {/each}
+27 -43
src/entrypoints/popup/routes/Snippets.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 3 2 import Title from "../components/Title.svelte"; 4 3 import Slider from "../components/inputs/Slider.svelte"; 5 4 import TextInput from "../components/inputs/TextInput.svelte"; 6 5 7 - let populatedSnippets: PopulatedItem<SnippetId>[] = $state([]); 8 - let settings = $state(globalSettings.fallback); 9 - 10 6 let snippetURL = $state(""); 11 7 12 - onMount(async () => { 13 - populatedSnippets = await populateItems(snippets, SNIPPET_INFO); 14 - settings = await globalSettings.getValue(); 15 - }); 16 - 17 - async function handleToggleChange(event: CustomEvent) { 18 - let settings = await globalSettings.getValue(); 19 - settings.snippets = event.detail.checked; 20 - await globalSettings.setValue(settings); 21 - } 22 - 23 8 async function addUserSnippet() { 24 9 if (!snippetURL.startsWith("http") && !snippetURL.includes("gist.github.com")) { 25 10 alert("Invalid URL. Please enter a valid Gist URL."); ··· 36 21 let sections = snippetURL.split("/"); 37 22 let key = sections[sections.length - 1].split(".")[0]; 38 23 24 + let settings = await globalSettings.storage.getValue(); 39 25 settings.userSnippets[key] = { 40 26 author: sections[3], 41 27 name: getMatch(data, /\/\*\s*name:\s*(.*?)\s*\*\//) || key, ··· 43 29 url: snippetURL, 44 30 toggle: true, 45 31 }; 46 - await globalSettings.setValue(settings); 47 - } 48 - 49 - async function toggleSnippet(snippetId: SnippetId, toggled: boolean) { 50 - await toggleItem(snippets, snippetId, toggled); 51 - } 52 - 53 - async function toggleUserSnippet(snippetId: string, toggled: boolean) { 54 - settings.userSnippets[snippetId].toggle = toggled; 55 - await globalSettings.setValue(settings); 56 - } 57 - 58 - async function removeUserSnippet(snippetId: string) { 59 - delete settings.userSnippets[snippetId]; 60 - settings.userSnippets = settings.userSnippets; // force reactivity 61 - await globalSettings.setValue(settings); 32 + await globalSettings.storage.setValue(settings); 62 33 } 63 34 </script> 64 35 65 36 <div id="card"> 66 - <Title title="Snippets" bind:checked={settings.snippets} on:change={handleToggleChange} /> 37 + <Title 38 + title="Snippets" 39 + bind:checked={globalSettings.state.snippets} 40 + on:change={(event: CustomEvent) => { 41 + globalSettings.set({ snippets: event.detail.checked }); 42 + }} /> 67 43 68 44 <div class="snippets-container w-full"> 69 - {#each populatedSnippets as snippet} 45 + {#each Object.entries(snippets) as [id, snippet] (id)} 70 46 <div class="my-4 group w-full"> 71 47 <Slider 72 - id={snippet.id} 73 - bind:checked={snippet.toggle} 74 - on:change={() => toggleSnippet(snippet.id, snippet.toggle)} 75 - text={snippet.name} 76 - description={snippet.description} 48 + {id} 49 + bind:checked={snippet.state.toggle} 50 + on:change={(event: CustomEvent) => { 51 + snippet.set({ toggle: event.detail.checked }); 52 + }} 53 + text={snippet.info?.name} 54 + description={snippet.info?.description} 77 55 size="small" /> 78 56 </div> 79 57 {/each} ··· 93 71 </div> 94 72 95 73 <div class="user-snippets-container w-full"> 96 - {#each Object.entries(settings.userSnippets) as [key, snippet] (key)} 74 + {#each Object.entries(globalSettings.state.userSnippets) as [id, snippet] (id)} 97 75 <div class="my-4 group w-full"> 98 76 <Slider 99 - id={key} 77 + {id} 100 78 bind:checked={snippet.toggle} 101 - on:change={() => toggleUserSnippet(key, snippet.toggle)} 79 + on:change={async (event: CustomEvent) => { 80 + let settings = await globalSettings.storage.getValue(); 81 + settings.userSnippets[id].toggle = event.detail.checked; 82 + await globalSettings.storage.setValue(settings); 83 + }} 102 84 text={snippet.name} 103 85 description={snippet.description} 104 86 size="small" /> 105 87 <button 106 88 class="xsmall hover:bg-ctp-red hover:text-ctp-mantle" 107 - onclick={() => { 108 - removeUserSnippet(key); 89 + onclick={async () => { 90 + let settings = await globalSettings.storage.getValue(); 91 + delete settings.userSnippets[id]; 92 + await globalSettings.storage.setValue(settings); 109 93 }}>Remove</button> 110 94 <a href={snippet.url} target="_blank" 111 95 ><button class="xsmall hover:bg-(--ctp-accent) hover:text-ctp-mantle">Gist</button></a>
+18 -34
src/entrypoints/popup/routes/Themes.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from "svelte"; 3 2 import Title from "../components/Title.svelte"; 4 3 import Modal from "../components/Modal.svelte"; 5 4 import IconBtn from "../components/inputs/IconBtn.svelte"; ··· 24 23 ]; 25 24 26 25 const logos = LOGO_INFO; 27 - let settings = $state(globalSettings.fallback); 28 26 let showModal = $state(false); 29 27 30 - onMount(async () => { 31 - settings = await globalSettings.getValue(); 32 - }); 33 - 34 - async function flavourClicked(flavour: string) { 35 - settings.themeFlavour = flavour; 36 - await globalSettings.setValue($state.snapshot(settings)); 37 - } 38 - 39 28 function cleanAccent(accent: string) { 40 29 return accent.replace("bg-ctp-", ""); 41 30 } 42 - 43 - async function accentClicked(accent: string) { 44 - settings.themeAccent = cleanAccent(accent); 45 - await globalSettings.setValue($state.snapshot(settings)); 46 - } 47 - 48 - async function logoClicked(logoId: string) { 49 - settings.themeLogo = logoId as LogoId; 50 - await globalSettings.setValue($state.snapshot(settings)); 51 - } 52 - 53 - async function handleToggleChange(event: CustomEvent) { 54 - let settings = await globalSettings.getValue(); 55 - settings.themes = event.detail.checked; 56 - await globalSettings.setValue($state.snapshot(settings)); 57 - } 58 31 </script> 59 32 60 33 <Modal bind:showModal> ··· 65 38 <div class="grid grid-cols-3 gap-4"> 66 39 {#each Object.entries(logos) as [logoId, logo] (logoId)} 67 40 <button 68 - onclick={() => logoClicked(logoId)} 69 - class:highlight={settings.themeLogo === logoId} 41 + onclick={() => { 42 + globalSettings.set({ themeLogo: logoId as LogoId }); 43 + }} 44 + class:highlight={globalSettings.state.themeLogo === logoId} 70 45 class="border border-(--ctp-accent) p-2 flex flex-col items-center justify-between rounded-lg"> 71 46 <span>{logo.name}</span> 72 47 {#if logo.disable !== true} ··· 82 57 </Modal> 83 58 84 59 <div id="card"> 85 - <Title title="Themes" bind:checked={settings.themes} on:change={handleToggleChange} /> 60 + <Title 61 + title="Themes" 62 + bind:checked={globalSettings.state.themes} 63 + on:change={(event: CustomEvent) => { 64 + globalSettings.set({ themes: event.detail.checked }); 65 + }} /> 86 66 87 67 <div id="flavours" class="flex my-6 py-2 rounded-xl text-ctp-text"> 88 68 {#each flavours as flavour (flavour)} 89 69 <button 90 - class:active={settings.themeFlavour === flavour} 70 + class:active={globalSettings.state.themeFlavour === flavour} 91 71 class:navbutton-left={flavour === "latte"} 92 72 class:navbutton-right={flavour === "mocha"} 93 73 class:navbutton-center={flavour === "macchiato" || flavour === "frappe"} 94 - onclick={() => flavourClicked(flavour)}>{flavour}</button> 74 + onclick={() => { 75 + globalSettings.set({ themeFlavour: flavour }); 76 + }}>{flavour}</button> 95 77 {/each} 96 78 </div> 97 79 ··· 99 81 {#each accents as accent (accent)} 100 82 <button 101 83 class={accent} 102 - class:current={settings.themeAccent === cleanAccent(accent)} 84 + class:current={globalSettings.state.themeAccent === cleanAccent(accent)} 103 85 aria-label={cleanAccent(accent)} 104 86 title={cleanAccent(accent)} 105 - onclick={() => accentClicked(accent)}></button> 87 + onclick={() => { 88 + globalSettings.set({ themeAccent: cleanAccent(accent) }); 89 + }}></button> 106 90 {/each} 107 91 </div> 108 92
+2 -2
src/entrypoints/start.content.ts
··· 6 6 runAt: "document_start", 7 7 excludeMatches: EXCLUDE_MATCHES, 8 8 async main() { 9 - const settings = await globalSettings.getValue(); 10 - const urls = await schoolboxUrls.getValue(); 9 + const settings = await globalSettings.storage.getValue(); 10 + const urls = await schoolboxUrls.storage.getValue(); 11 11 12 12 if (settings.global && urls.includes(window.location.origin)) { 13 13 // inject themes
-48
src/utils/constants.ts
··· 30 30 url: "https://schooltape.github.io/schooltape-legacy.svg", 31 31 }, 32 32 }; 33 - 34 - // Plugins 35 - export const PLUGIN_INFO: Record<Types.PluginId, Types.PluginInfo> = { 36 - subheader: { 37 - name: "Subheader Revamp", 38 - description: "Adds a clock and current period info to the subheader", 39 - }, 40 - scrollSegments: { 41 - name: "Scroll Segments", 42 - description: "Segments the Schoolbox page into scrollable sections", 43 - }, 44 - progressBar: { 45 - name: "Progress Bar", 46 - description: "Displays a progress bar below the timetable to show the time of the day", 47 - }, 48 - scrollPeriod: { 49 - name: "Scroll Period", 50 - description: "Scrolls to the current period on the timetable", 51 - }, 52 - modernIcons: { 53 - name: "Modern Icons", 54 - description: "Modernise the icons across Schoolbox", 55 - }, 56 - tabTitle: { 57 - name: "Better Tab Titles", 58 - description: "Improves the tab titles for easier navigation", 59 - }, 60 - homepageSwitcher: { 61 - name: "Homepage Switcher", 62 - description: "The logo will switch to existing Schoolbox homepage when available", 63 - }, 64 - }; 65 - 66 - // Snippets 67 - export const SNIPPET_INFO: Record<Types.SnippetId, Types.SnippetInfo> = { 68 - hidePfp: { 69 - name: "Hide PFP", 70 - description: "Hide your profile picture across Schoolbox.", 71 - }, 72 - hidePwaPrompt: { 73 - name: "Hide PWA Prompt", 74 - description: "Hides the prompt in the notifications menu to install Schoolbox as a PWA and enable notifications.", 75 - }, 76 - censor: { 77 - name: "Censor", 78 - description: "Censors all text and images. This is intended for development purposes.", 79 - }, 80 - };
+7 -7
src/utils/plugin.ts
··· 3 3 injectLogic: (pluginId: PluginId) => void, 4 4 elementsToWaitFor: string[] = [], 5 5 ) { 6 - const plugin = await plugins[pluginId].getValue(); 6 + const plugin = await plugins[pluginId].storage.getValue(); 7 7 8 - logger.info(`${PLUGIN_INFO[pluginId].name}: ${plugin.toggle ? "enabled" : "disabled"}`); 8 + logger.info(`${plugins[pluginId].info?.name}: ${plugin.toggle ? "enabled" : "disabled"}`); 9 9 10 - const settings = await globalSettings.getValue(); 11 - const urls = await schoolboxUrls.getValue(); 10 + const settings = await globalSettings.storage.getValue(); 11 + const urls = await schoolboxUrls.storage.getValue(); 12 12 13 13 if (plugin && typeof window !== "undefined" && urls.includes(window.location.origin)) { 14 14 if (settings.global && settings.plugins && plugin.toggle) { ··· 19 19 const allElementsPresent = elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 20 20 if (allElementsPresent) { 21 21 observer.disconnect(); 22 - logger.info(`all elements present, injecting plugin: ${PLUGIN_INFO[pluginId].name}`); 22 + logger.info(`all elements present, injecting plugin: ${plugins[pluginId].info?.name}`); 23 23 injectLogic(pluginId); 24 24 } 25 25 }); ··· 30 30 const allElementsPresent = elementsToWaitFor.every((selector) => document.querySelector(selector) !== null); 31 31 if (allElementsPresent) { 32 32 observer.disconnect(); 33 - logger.info(`all elements already present, injecting plugin: ${PLUGIN_INFO[pluginId].name}`); 33 + logger.info(`all elements already present, injecting plugin: ${plugins[pluginId].info?.name}`); 34 34 injectLogic(pluginId); 35 35 } 36 36 } else { 37 37 // no elements to wait for 38 - logger.info(`injecting plugin: ${PLUGIN_INFO[pluginId].name}`); 38 + logger.info(`injecting plugin: ${plugins[pluginId].info?.name}`); 39 39 injectLogic(pluginId); 40 40 } 41 41 };
+5 -5
src/utils/snippet.ts
··· 1 1 export async function defineStSnippet(snippetId: SnippetId, styleText: string) { 2 - const snippet = await snippets[snippetId].getValue(); 2 + const snippet = await snippets[snippetId].storage.getValue(); 3 3 4 - logger.info(`${SNIPPET_INFO[snippetId].name}: ${snippet.toggle ? "enabled" : "disabled"}`); 4 + logger.info(`${snippets[snippetId].info?.name}: ${snippet.toggle ? "enabled" : "disabled"}`); 5 5 6 - const settings = await globalSettings.getValue(); 7 - const urls = await schoolboxUrls.getValue(); 6 + const settings = await globalSettings.storage.getValue(); 7 + const urls = await schoolboxUrls.storage.getValue(); 8 8 9 9 if (snippet && typeof window !== "undefined" && urls.includes(window.location.origin)) { 10 10 if (settings.global && settings.snippets && snippet.toggle) { 11 11 // inject 12 - logger.info(`Injecting snippet: ${SNIPPET_INFO[snippetId].name}`); 12 + logger.info(`Injecting snippet: ${snippets[snippetId].info?.name}`); 13 13 injectStyles(styleText); 14 14 } 15 15 }
+35
src/utils/state.svelte.ts
··· 1 + import { WxtStorageItem } from "#imports"; 2 + 3 + export class StorageState<T, I = {}> { 4 + public state; 5 + 6 + constructor( 7 + public storage: WxtStorageItem<T, {}>, 8 + public info?: I, 9 + ) { 10 + this.info = info; 11 + this.storage = storage; 12 + this.state = $state(this.storage.fallback); 13 + 14 + this.storage.getValue().then(this.update); 15 + this.storage.watch(this.update); 16 + } 17 + 18 + private update = (newState: T | null) => { 19 + this.state = newState ?? this.storage.fallback; 20 + }; 21 + 22 + async set(updates: Partial<T>) { 23 + const newState = { 24 + ...($state.snapshot(this.state) as T), 25 + ...updates, 26 + }; 27 + 28 + await this.storage.setValue(newState); 29 + } 30 + 31 + get() { 32 + this.storage.getValue().then(this.update); 33 + return $state.snapshot(this.state) as T; 34 + } 35 + }
+131 -65
src/utils/storage.ts
··· 1 - import { WxtStorageItem } from "#imports"; 2 1 import * as Types from "./types"; 2 + import { StorageState } from "./state.svelte"; 3 3 4 4 // Global 5 - export const globalSettings = storage.defineItem<Types.Settings>("local:globalSettings", { 6 - version: 1, 7 - fallback: { 8 - global: true, 9 - plugins: true, 10 - themes: true, 11 - snippets: true, 5 + export const globalSettings = new StorageState<Types.Settings>( 6 + storage.defineItem<Types.Settings>("local:globalSettings", { 7 + version: 1, 8 + fallback: { 9 + global: true, 10 + plugins: true, 11 + themes: true, 12 + snippets: true, 12 13 13 - themeFlavour: "mocha", 14 - themeAccent: "mauve", 15 - themeLogo: "schooltape-rainbow", 14 + themeFlavour: "mocha", 15 + themeAccent: "mauve", 16 + themeLogo: "schooltape-rainbow", 16 17 17 - userSnippets: {}, 18 - }, 19 - }); 20 - export const needsRefresh = storage.defineItem<boolean>("local:needsRefresh", { 21 - fallback: false, 22 - }); 23 - export const schoolboxUrls = storage.defineItem<string[]>("local:urls", { 24 - fallback: ["https://help.schoolbox.com.au"], 25 - }); 18 + userSnippets: {}, 19 + }, 20 + }), 21 + ); 22 + export const needsRefresh = new StorageState( 23 + storage.defineItem<boolean>("local:needsRefresh", { 24 + fallback: false, 25 + }), 26 + ); 27 + export const schoolboxUrls = new StorageState( 28 + storage.defineItem<string[]>("local:urls", { 29 + fallback: ["https://help.schoolbox.com.au"], 30 + }), 31 + ); 26 32 27 33 // Plugins 28 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 29 - export const plugins: Record<Types.PluginId, WxtStorageItem<Types.PluginGeneric, any>> = { 30 - subheader: storage.defineItem<Types.PluginGeneric>("local:plugin-subheader", { 31 - fallback: { 32 - toggle: true, 34 + export const plugins: Record<Types.PluginId, StorageState<Types.PluginGeneric, Types.PluginInfo>> = { 35 + subheader: new StorageState( 36 + storage.defineItem<Types.PluginGeneric>("local:plugin-subheader", { 37 + fallback: { 38 + toggle: true, 39 + }, 40 + }), 41 + { 42 + name: "Subheader Revamp", 43 + description: "Adds a clock and current period info to the subheader", 33 44 }, 34 - }), 35 - scrollSegments: storage.defineItem<Types.PluginGeneric>("local:plugin-scrollSegments", { 36 - fallback: { 37 - toggle: true, 45 + ), 46 + scrollSegments: new StorageState( 47 + storage.defineItem<Types.PluginGeneric>("local:plugin-scrollSegments", { 48 + fallback: { 49 + toggle: true, 50 + }, 51 + }), 52 + { 53 + name: "Scroll Segments", 54 + description: "Segments the Schoolbox page into scrollable sections", 38 55 }, 39 - }), 40 - scrollPeriod: storage.defineItem<Types.PluginGeneric>("local:plugin-scrollPeriod", { 41 - fallback: { 42 - toggle: true, 56 + ), 57 + scrollPeriod: new StorageState( 58 + storage.defineItem<Types.PluginGeneric>("local:plugin-scrollPeriod", { 59 + fallback: { 60 + toggle: true, 61 + }, 62 + }), 63 + { 64 + name: "Scroll Period", 65 + description: "Scrolls to the current period on the timetable", 43 66 }, 44 - }), 45 - progressBar: storage.defineItem<Types.PluginGeneric>("local:plugin-progressBar", { 46 - fallback: { 47 - toggle: true, 67 + ), 68 + progressBar: new StorageState( 69 + storage.defineItem<Types.PluginGeneric>("local:plugin-progressBar", { 70 + fallback: { 71 + toggle: true, 72 + }, 73 + }), 74 + { 75 + name: "Progress Bar", 76 + description: "Displays a progress bar below the timetable to show the time of the day", 48 77 }, 49 - }), 50 - modernIcons: storage.defineItem<Types.PluginGeneric>("local:plugin-modernIcons", { 51 - fallback: { 52 - toggle: true, 78 + ), 79 + modernIcons: new StorageState( 80 + storage.defineItem<Types.PluginGeneric>("local:plugin-modernIcons", { 81 + fallback: { 82 + toggle: true, 83 + }, 84 + }), 85 + { 86 + name: "Modern Icons", 87 + description: "Modernise the icons across Schoolbox", 53 88 }, 54 - }), 55 - tabTitle: storage.defineItem<Types.TabTitle>("local:plugin-tabTitle", { 56 - fallback: { 57 - toggle: true, 58 - showSubjectPrefix: true, 89 + ), 90 + tabTitle: new StorageState( 91 + storage.defineItem<Types.PluginGeneric>("local:plugin-tabTitle", { 92 + fallback: { 93 + toggle: true, 94 + settings: { 95 + showSubjectPrefix: true, 96 + }, 97 + }, 98 + }), 99 + { 100 + name: "Better Tab Titles", 101 + description: "Improves the tab titles for easier navigation", 59 102 }, 60 - }), 61 - homepageSwitcher: storage.defineItem<Types.PluginGeneric>("local:plugin-homepageSwitcher", { 62 - fallback: { 63 - toggle: true, 103 + ), 104 + homepageSwitcher: new StorageState( 105 + storage.defineItem<Types.PluginGeneric>("local:plugin-homepageSwitcher", { 106 + fallback: { 107 + toggle: true, 108 + }, 109 + }), 110 + { 111 + name: "Homepage Switcher", 112 + description: "The logo will switch to existing Schoolbox homepage when available", 64 113 }, 65 - }), 114 + ), 66 115 }; 67 116 68 117 // Snippets 69 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 70 - export const snippets: Record<Types.SnippetId, WxtStorageItem<Types.SnippetGeneric, any>> = { 71 - hidePfp: storage.defineItem<Types.SnippetGeneric>("local:snippet-hidePfp", { 72 - fallback: { 73 - toggle: true, 118 + export const snippets: Record<Types.SnippetId, StorageState<Types.SnippetGeneric, Types.SnippetInfo>> = { 119 + hidePfp: new StorageState( 120 + storage.defineItem<Types.SnippetGeneric>("local:snippet-hidePfp", { 121 + fallback: { 122 + toggle: true, 123 + }, 124 + }), 125 + { 126 + name: "Hide PFP", 127 + description: "Hide your profile picture across Schoolbox.", 74 128 }, 75 - }), 76 - hidePwaPrompt: storage.defineItem<Types.SnippetGeneric>("local:snippet-hidePwaPrompt", { 77 - fallback: { 78 - toggle: true, 129 + ), 130 + hidePwaPrompt: new StorageState( 131 + storage.defineItem<Types.SnippetGeneric>("local:snippet-hidePwaPrompt", { 132 + fallback: { 133 + toggle: true, 134 + }, 135 + }), 136 + { 137 + name: "Hide PWA Prompt", 138 + description: "Hides the prompt in the notifications menu to install Schoolbox as a PWA and enable notifications.", 79 139 }, 80 - }), 81 - censor: storage.defineItem<Types.SnippetGeneric>("local:snippet-censor", { 82 - fallback: { 83 - toggle: false, 140 + ), 141 + censor: new StorageState( 142 + storage.defineItem<Types.SnippetGeneric>("local:snippet-censor", { 143 + fallback: { 144 + toggle: false, 145 + }, 146 + }), 147 + { 148 + name: "Censor", 149 + description: "Censors all text and images. This is intended for development purposes.", 84 150 }, 85 - }), 151 + ), 86 152 };
+3 -1
src/utils/types.ts
··· 53 53 | "homepageSwitcher"; 54 54 55 55 export interface PluginInfo extends ItemInfo {} 56 - export interface PluginGeneric extends ItemGeneric {} 56 + export interface PluginGeneric extends ItemGeneric { 57 + settings?: any; // temporary until plugin options is implemented 58 + } 57 59 58 60 export interface TabTitle extends ItemGeneric { 59 61 showSubjectPrefix: boolean;
-43
src/utils/utils.ts
··· 1 1 import { flavorEntries } from "@catppuccin/palette"; 2 - import { WxtStorageItem } from "#imports"; 3 - 4 - export async function populateItems<T extends ItemId>( 5 - storage: Record<T, WxtStorageItem<ItemGeneric, any>>, 6 - info: Record<T, ItemInfo>, 7 - ): Promise<PopulatedItem<T>[]> { 8 - const populatedItems: PopulatedItem<T>[] = []; 9 - 10 - for (const itemId of Object.keys(storage) as T[]) { 11 - const item = storage[itemId].fallback; 12 - const itemInfo = info[itemId]; 13 - 14 - const populatedItem: PopulatedItem<T> = { 15 - id: itemId, 16 - ...item, 17 - ...itemInfo, 18 - toggle: false, 19 - }; 20 - 21 - const storedItem = await storage[itemId].getValue(); 22 - populatedItem.toggle = storedItem.toggle; 23 - 24 - populatedItems.push(populatedItem); 25 - } 26 - 27 - return populatedItems; 28 - } 29 - 30 - export async function toggleItem<T extends ItemId>( 31 - storage: Record<T, WxtStorageItem<ItemGeneric, any>>, 32 - itemId: T, 33 - toggled: boolean, 34 - ): Promise<void> { 35 - const item = await storage[itemId].getValue(); 36 - if (item) { 37 - item.toggle = toggled; 38 - await storage[itemId].setValue(item); 39 - await needsRefresh.setValue(true); 40 - logger.info(`Toggled ${itemId} to ${toggled}`); 41 - } else { 42 - logger.error(`Failed to toggle ${itemId}, not found in storage`); 43 - } 44 - } 45 2 46 3 export function injectStyles(styleText: string) { 47 4 logger.info(`[content-utils] Injecting styles: ${styleText}`);