schoolbox web extension :)
0
fork

Configure Feed

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

feat(plugins/changeLogo): init

willow 99cc614f 6378de94

+250 -162
+11 -1
src/entrypoints/plugins.content.ts
··· 1 1 import { defineContentScript } from "#imports"; 2 2 import { EXCLUDE_MATCHES } from "@/utils/constants"; 3 + import changeLogo from "./plugins/changeLogo"; 3 4 import homepageSwitcher from "./plugins/homepageSwitcher"; 4 5 import modernIcons from "./plugins/modernIcons"; 5 6 import progressBar from "./plugins/progressBar"; ··· 8 9 import subheader from "./plugins/subheader"; 9 10 import tabTitle from "./plugins/tabTitle"; 10 11 11 - export const plugins = [subheader, scrollSegments, scrollPeriod, progressBar, modernIcons, tabTitle, homepageSwitcher]; 12 + export const plugins = [ 13 + subheader, 14 + scrollSegments, 15 + scrollPeriod, 16 + progressBar, 17 + modernIcons, 18 + tabTitle, 19 + changeLogo, 20 + homepageSwitcher, 21 + ]; 12 22 13 23 export type PluginInstance = (typeof plugins)[number]; 14 24
+32
src/entrypoints/plugins/changeLogo/Menu.svelte
··· 1 + <script lang="ts"> 2 + import Toggle from "@/entrypoints/popup/components/inputs/Toggle.svelte"; 3 + import type { Settings } from "."; 4 + import { logos } from "."; 5 + 6 + let { settings }: { settings: Settings } = $props(); 7 + </script> 8 + 9 + <div class="grid grid-cols-3 gap-4"> 10 + {#await logos then logos} 11 + {#each Object.entries(logos) as [id, logo] (id)} 12 + <button 13 + onclick={() => settings.logo.set({ id })} 14 + class:highlight={settings.logo.state.id === id} 15 + class="flex flex-col rounded-lg border border-(--ctp-accent) p-2"> 16 + <span>{logo.name}</span> 17 + <div class="flex h-full w-full items-center justify-center"> 18 + <img src={logo.url} alt="Logo" class="mt-2 w-16" /> 19 + </div> 20 + </button> 21 + {/each} 22 + {/await} 23 + </div> 24 + 25 + <Toggle 26 + text="Set icon as tab favicon" 27 + update={(toggle) => { 28 + settings.setAsFavicon.set({ toggle }); 29 + }} 30 + checked={settings.setAsFavicon.state.toggle} 31 + size="small" 32 + id="setAsFavicon" />
+139
src/entrypoints/plugins/changeLogo/index.ts
··· 1 + import { browser } from "#imports"; 2 + import { dataAttr, setDataAttr } from "@/utils"; 3 + import { logger } from "@/utils/logger"; 4 + import { Plugin } from "@/utils/plugin"; 5 + import { globalSettings } from "@/utils/storage"; 6 + import type { Toggle } from "@/utils/storage"; 7 + import type { StorageState } from "@/utils/storage/state.svelte"; 8 + import schoolbox from "/schoolbox.svg?raw"; 9 + import { flavors } from "@catppuccin/palette"; 10 + 11 + const ID = "changeLogo"; 12 + let originalFavicon: string | undefined; 13 + export const logos = buildLogos({ 14 + schooltape: { 15 + name: "Schooltape", 16 + url: "schooltape.svg", 17 + }, 18 + "schooltape-rainbow": { 19 + name: "ST Rainbow", 20 + url: "schooltape-ctp.svg", 21 + }, 22 + "schooltape-legacy": { 23 + name: "ST Legacy", 24 + url: "https://schooltape.github.io/schooltape-legacy.svg", 25 + }, 26 + catppuccin: { 27 + name: "Catppuccin", 28 + url: "https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/logos/exports/1544x1544_circle.png", 29 + }, 30 + schoolbox: { 31 + name: "Schoolbox", 32 + raw: schoolbox, 33 + }, 34 + }); 35 + 36 + export type Settings = { 37 + setAsFavicon: StorageState<Toggle>; 38 + logo: StorageState<{ id: keyof Awaited<typeof logos> }>; 39 + }; 40 + 41 + interface LogoInfo { 42 + name: string; 43 + url: string; 44 + } 45 + type ImageSource = { name: string; url: string; raw?: never } | { name: string; url?: never; raw: string }; 46 + 47 + export default new Plugin<Settings>( 48 + { 49 + id: ID, 50 + name: "Change Logo", 51 + description: "Changes the Schoolbox logo to a logo of your choice.", 52 + }, 53 + true, 54 + { 55 + setAsFavicon: { toggle: true }, 56 + logo: { id: "schooltape-rainbow" }, 57 + }, 58 + async (settings) => { 59 + const x = await logos; 60 + const logoId = (await settings.logo.get()).id; 61 + injectLogo(x[logoId]); 62 + if ((await settings.setAsFavicon.get()).toggle) injectFavicon(x[logoId]); 63 + }, 64 + () => { 65 + uninjectLogo(); 66 + uninjectFavicon(); 67 + }, 68 + [".logo"], 69 + ); 70 + 71 + function injectLogo(logo: LogoInfo): void { 72 + logger.info(`injecting logo: ${logo.name}`); 73 + 74 + const style = document.createElement("style"); 75 + setDataAttr(style, "logo"); 76 + 77 + style.textContent = `a.logo > img { content: url("${logo.url}"); max-width: 30%; width: 100px; }`; 78 + document.head.appendChild(style); 79 + } 80 + 81 + function uninjectLogo() { 82 + logger.info("uninjecting logo..."); 83 + for (const el of document.querySelectorAll(dataAttr("logo"))) { 84 + el.parentElement?.removeChild(el); 85 + } 86 + } 87 + 88 + function injectFavicon(logo: LogoInfo) { 89 + logger.info(`injecting favicon: ${logo.name}`); 90 + 91 + let favicon = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null; 92 + 93 + if (!favicon) { 94 + favicon = document.createElement("link") as HTMLLinkElement; 95 + favicon.rel = "icon"; 96 + document.head.appendChild(favicon); 97 + } 98 + 99 + originalFavicon = favicon?.href; 100 + favicon.href = logo.url; 101 + } 102 + 103 + function uninjectFavicon() { 104 + logger.info("uninjecting favicon..."); 105 + const favicon = document.querySelector<HTMLLinkElement>("link[rel~='icon']"); 106 + if (favicon && originalFavicon) favicon.href = originalFavicon; 107 + } 108 + 109 + async function buildLogos(logos: Record<string, ImageSource>): Promise<Record<string, LogoInfo>> { 110 + const output: Record<string, LogoInfo> = {}; 111 + 112 + for (const [key, value] of Object.entries(logos)) { 113 + let url; 114 + 115 + if (value.url) { 116 + if (value.url.startsWith("http")) { 117 + url = value.url; 118 + } else { 119 + // @ts-expect-error unlisted CSS not a PublicPath 120 + url = browser.runtime.getURL(value.url); 121 + } 122 + } else if (value.raw) { 123 + const settings = await globalSettings.get(); 124 + const flavour = settings.themeFlavour; 125 + const accent = settings.themeAccent; 126 + const accentHex = flavors[flavour].colors[accent].hex; 127 + url = `data:image/svg+xml;utf8,${encodeURIComponent(value.raw.replaceAll("currentColor", accentHex))}`; 128 + } 129 + 130 + if (!url) throw new Error(`error getting URL for logo: ${key}`); 131 + 132 + output[key] = { 133 + name: value.name, 134 + url, 135 + }; 136 + } 137 + 138 + return output; 139 + }
+3 -3
src/entrypoints/popup/components/inputs/Toggle.svelte
··· 14 14 let { update, checked, id, size = "big", text = "", description = "", children }: Props = $props(); 15 15 </script> 16 16 17 - <label class="group relative flex cursor-pointer items-center justify-between py-2 text-ctp-text"> 17 + <label class="group text-ctp-text relative flex cursor-pointer items-center justify-between py-2"> 18 18 <h4 class="text-ctp-text">{text}</h4> 19 19 <input 20 20 {id} ··· 29 29 </label> 30 30 31 31 <div 32 - class="flex items-center justify-between text-ctp-overlay1 transition-colors duration-500 ease-in-out group-hover:text-ctp-subtext0"> 32 + class="text-ctp-overlay1 group-hover:text-ctp-subtext0 flex items-center justify-between gap-2 transition-colors duration-500 ease-in-out"> 33 33 <div>{description}</div> 34 - <div>{@render children?.()}</div> 34 + {@render children?.()} 35 35 </div>
+4 -56
src/entrypoints/popup/routes/Themes.svelte
··· 1 1 <script lang="ts"> 2 - import { browser } from "#imports"; 3 - import type { LogoId } from "@/utils/storage"; 4 2 import { globalSettings } from "@/utils/storage"; 5 - import { LOGO_INFO } from "@/utils/constants"; 6 - import { Palette } from "@lucide/svelte"; 3 + import type { Accent, Flavour } from "@/utils/storage"; 7 4 8 5 import Title from "../components/Title.svelte"; 9 - import Modal from "../components/Modal.svelte"; 10 - import Button from "../components/inputs/Button.svelte"; 11 - import Toggle from "../components/inputs/Toggle.svelte"; 12 6 13 - const flavours = ["latte", "frappe", "macchiato", "mocha"]; 7 + const flavours: Flavour[] = ["latte", "frappe", "macchiato", "mocha"]; 14 8 const accents = [ 15 9 "bg-ctp-rosewater", 16 10 "bg-ctp-flamingo", ··· 28 22 "bg-ctp-lavender", 29 23 ]; 30 24 31 - const logos = LOGO_INFO; 32 - let showModal = $state(false); 33 - 34 25 function cleanAccent(accent: string) { 35 26 return accent.replace("bg-ctp-", ""); 36 27 } 37 28 </script> 38 29 39 - <Modal bind:showModal> 40 - {#snippet header()} 41 - <h2 class="mb-4 text-xl">Choose an icon</h2> 42 - {/snippet} 43 - 44 - <div class="grid grid-cols-3 gap-4"> 45 - {#each Object.entries(logos) as [logoId, logo] (logoId)} 46 - <button 47 - onclick={() => { 48 - globalSettings.update({ themeLogo: logoId as LogoId }); 49 - }} 50 - class:highlight={globalSettings.state.themeLogo === logoId} 51 - class="flex flex-col rounded-lg border border-(--ctp-accent) p-2"> 52 - <span>{logo.name}</span> 53 - {#if logo.disable !== true} 54 - <div class="flex h-full w-full items-center justify-center"> 55 - {#if logo.adaptive} 56 - <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any --> 57 - <span class="logo-picker" style="--icon: url({browser.runtime.getURL(logo.url as any)})"></span> 58 - {:else} 59 - <img src={logo.url} alt="Logo" class="mt-2 w-16" /> 60 - {/if} 61 - </div> 62 - {/if} 63 - </button> 64 - {/each} 65 - </div> 66 - 67 - <div class="mt-4"> 68 - <Toggle 69 - update={(toggled) => { 70 - globalSettings.update({ themeLogoAsFavicon: toggled }); 71 - }} 72 - checked={globalSettings.state.themeLogoAsFavicon} 73 - id="setAsFavicon" 74 - size="small" 75 - text="Set icon as tab favicon" /> 76 - </div> 77 - </Modal> 78 - 79 30 <div id="card"> 80 31 <Title 81 32 title="Themes" ··· 84 35 globalSettings.update({ themes: toggled }); 85 36 }} /> 86 37 87 - <div id="flavours" class="my-6 flex rounded-xl py-2 text-ctp-text"> 38 + <div id="flavours" class="text-ctp-text my-6 flex rounded-xl py-2"> 88 39 {#each flavours as flavour (flavour)} 89 40 <button 90 41 class:active={globalSettings.state.themeFlavour === flavour} ··· 105 56 aria-label={cleanAccent(accent)} 106 57 title={cleanAccent(accent)} 107 58 onclick={() => { 108 - globalSettings.update({ themeAccent: cleanAccent(accent) }); 59 + globalSettings.update({ themeAccent: cleanAccent(accent) as Accent }); 109 60 }}></button> 110 61 {/each} 111 62 </div> 112 - 113 - <Button title="Choose icon" id="choose-icon" onclick={() => (showModal = true)} 114 - ><Palette size={22} /> Choose an icon</Button> 115 63 </div>
+4 -8
src/entrypoints/start.content.ts
··· 2 2 import { 3 3 hasChanged, 4 4 injectCatppuccin, 5 - injectLogo, 6 5 injectStylesheet, 7 6 injectUserSnippet, 8 7 onSchoolboxPage, ··· 10 9 uninjectStylesheet, 11 10 uninjectUserSnippet, 12 11 } from "@/utils"; 13 - import { EXCLUDE_MATCHES, LOGO_INFO } from "@/utils/constants"; 14 - import type { LogoId, Settings } from "@/utils/storage"; 12 + import { EXCLUDE_MATCHES } from "@/utils/constants"; 13 + import type { SettingsV2 } from "@/utils/storage"; 15 14 import { globalSettings } from "@/utils/storage"; 16 15 import type { WatchCallback } from "wxt/utils/storage"; 17 16 import cssUrl from "./catppuccin.css?url"; ··· 25 24 // if not on Schoolbox page 26 25 if (!(await onSchoolboxPage())) return; 27 26 28 - const updateThemes: WatchCallback<Settings> = async (newValue, oldValue) => { 27 + const updateThemes: WatchCallback<SettingsV2> = async (newValue, oldValue) => { 29 28 // if global or themes was changed 30 29 if (hasChanged(newValue, oldValue, ["global", "themes", "themeFlavour", "themeAccent"])) { 31 30 if (newValue.global && newValue.themes) { ··· 38 37 } 39 38 }; 40 39 41 - const updateUserSnippets: WatchCallback<Settings> = async (newValue, oldValue) => { 40 + const updateUserSnippets: WatchCallback<SettingsV2> = async (newValue, oldValue) => { 42 41 // if global or userSnippets were changed 43 42 if (hasChanged(newValue, oldValue, ["global", "userSnippets"])) { 44 43 // uninject removed snippets ··· 78 77 injectThemes(); 79 78 injectCatppuccin(); 80 79 } 81 - 82 - // inject logo 83 - injectLogo(LOGO_INFO[settings.themeLogo as LogoId], settings.themeLogoAsFavicon); 84 80 85 81 // inject user snippets 86 82 if (settings.snippets) {
-30
src/utils/constants.ts
··· 1 - import type { LogoId, LogoInfo } from "./storage"; 2 - 3 1 export const EXCLUDE_MATCHES: string[] = ["*://*/learning/quiz/*"]; 4 - export const LOGO_INFO: Record<LogoId, LogoInfo> = { 5 - default: { 6 - name: "Default", 7 - url: "default", 8 - disable: true, 9 - }, 10 - catppuccin: { 11 - name: "Catppuccin", 12 - url: "https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/logos/exports/1544x1544_circle.png", 13 - }, 14 - schoolbox: { 15 - name: "Schoolbox", 16 - url: "schoolbox.svg", 17 - adaptive: true, 18 - }, 19 - schooltape: { 20 - name: "Schooltape", 21 - url: "schooltape.svg", 22 - }, 23 - "schooltape-rainbow": { 24 - name: "ST Rainbow", 25 - url: "schooltape-ctp.svg", 26 - }, 27 - "schooltape-legacy": { 28 - name: "ST Legacy", 29 - url: "https://schooltape.github.io/schooltape-legacy.svg", 30 - }, 31 - };
-46
src/utils/index.ts
··· 1 - import { browser } from "#imports"; 2 1 import { flavorEntries } from "@catppuccin/palette"; 3 2 import { logger } from "./logger"; 4 - import type { LogoInfo } from "./storage"; 5 3 import { globalSettings, schoolboxUrls } from "./storage"; 6 4 7 5 export const dataAttr = (id: string) => `[data-schooltape="${id}"]`; ··· 50 48 51 49 export function uninjectCatppuccin() { 52 50 uninjectInlineStyles("catppuccin"); 53 - } 54 - 55 - export function injectLogo(logo: LogoInfo, setAsFavicon: boolean) { 56 - let url = logo.url; 57 - if (!url.startsWith("http")) { 58 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 - url = browser.runtime.getURL(url as any); 60 - } 61 - logger.info(`injecting logo: ${logo.name}`); 62 - if (logo.disable) { 63 - return; 64 - } 65 - const style = document.createElement("style"); 66 - style.classList.add("schooltape"); 67 - if (logo.adaptive) { 68 - style.textContent = `a.logo > img { display: none !important; } a.logo { display: flex; align-items: center; justify-content: center; }`; 69 - const span = document.createElement("span"); 70 - span.style.mask = `url("${url}") no-repeat center`; 71 - span.style.maskSize = "100% 100%"; 72 - span.style.backgroundColor = "hsl(var(--ctp-accent))"; 73 - span.style.width = "100%"; 74 - span.style.height = "60px"; 75 - span.style.display = "block"; 76 - window.addEventListener("load", () => { 77 - document.querySelectorAll("a.logo").forEach((logo) => { 78 - const clonedSpan = span.cloneNode(true); 79 - logo.append(clonedSpan); 80 - }); 81 - }); 82 - } else { 83 - style.textContent = `a.logo > img { content: url("${url}"); max-width: 30%; width: 100px; }`; 84 - } 85 - document.head.appendChild(style); 86 - 87 - // inject favicon 88 - if (setAsFavicon) { 89 - let favicon = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null; 90 - if (!favicon) { 91 - favicon = document.createElement("link") as HTMLLinkElement; 92 - favicon.rel = "icon"; 93 - document.head.appendChild(favicon); 94 - } 95 - favicon.href = url; 96 - } 97 51 } 98 52 99 53 export function injectStylesheet(url: string, id: string) {
-1
src/utils/plugin.ts
··· 9 9 private injected = false; 10 10 public toggle: StorageState<Toggle>; 11 11 public settings!: T; 12 - public menu: string | undefined; 13 12 14 13 constructor( 15 14 public meta: {
+22 -4
src/utils/storage/global.ts
··· 1 1 import { storage } from "#imports"; 2 2 import { StorageState } from "./state.svelte"; 3 3 import type * as Types from "./types"; 4 + import type { Settings as LogoSettings } from "@/entrypoints/plugins/changeLogo"; 4 5 5 - export const globalSettings = new StorageState<Types.Settings>( 6 - storage.defineItem<Types.Settings>("local:globalSettings", { 6 + export const globalSettings = new StorageState( 7 + storage.defineItem<Types.SettingsV2>("local:globalSettings", { 8 + version: 2, 7 9 fallback: { 8 10 global: true, 9 11 plugins: true, ··· 12 14 13 15 themeFlavour: "mocha", 14 16 themeAccent: "mauve", 15 - themeLogo: "schooltape-rainbow", 16 - themeLogoAsFavicon: false, 17 17 18 18 userSnippets: {}, 19 + }, 20 + migrations: { 21 + 2: async (settings: Types.SettingsV1): Promise<Types.SettingsV2> => { 22 + const { themeLogo, themeLogoAsFavicon, ...rest } = settings; 23 + 24 + // dynamic import to avoid TDZ error 25 + const { plugins } = await import("@/entrypoints/plugins.content"); 26 + const changeLogo = plugins.find((plugin) => plugin.meta.id === "changeLogo"); 27 + 28 + if (changeLogo) { 29 + const { logos } = await import("@/entrypoints/plugins/changeLogo"); 30 + const s = changeLogo.settings as LogoSettings; 31 + if (Object.keys(logos).includes(themeLogo)) s.logo.set({ id: themeLogo }); 32 + s.setAsFavicon.set({ toggle: themeLogoAsFavicon }); 33 + } 34 + 35 + return rest; 36 + }, 19 37 }, 20 38 }), 21 39 );
+35 -13
src/utils/storage/types.ts
··· 1 + import type { logos } from "@/entrypoints/plugins/changeLogo"; 2 + 1 3 // global 2 - export interface Settings { 4 + export interface SettingsV1 { 3 5 global: boolean; 4 6 plugins: boolean; 5 7 themes: boolean; 6 8 snippets: boolean; 7 9 8 - themeFlavour: string; 9 - themeAccent: string; 10 - themeLogo: LogoId; 10 + themeFlavour: Flavour; 11 + themeAccent: Accent; 12 + themeLogo: keyof Awaited<typeof logos>; 11 13 themeLogoAsFavicon: boolean; 12 14 13 15 userSnippets: Record<string, UserSnippet>; 14 16 } 15 17 18 + export interface SettingsV2 { 19 + global: boolean; 20 + plugins: boolean; 21 + themes: boolean; 22 + snippets: boolean; 23 + 24 + themeFlavour: Flavour; 25 + themeAccent: Accent; 26 + 27 + userSnippets: Record<string, UserSnippet>; 28 + } 29 + 30 + export type Flavour = "latte" | "frappe" | "macchiato" | "mocha"; 31 + export type Accent = 32 + | "rosewater" 33 + | "flamingo" 34 + | "pink" 35 + | "mauve" 36 + | "red" 37 + | "maroon" 38 + | "peach" 39 + | "yellow" 40 + | "green" 41 + | "teal" 42 + | "sky" 43 + | "sapphire" 44 + | "blue" 45 + | "lavender"; 46 + 16 47 export interface UpdatedBadges { 17 48 icon: boolean; 18 49 changelog: boolean; ··· 24 55 25 56 export interface SchoolboxUrls { 26 57 urls: string[]; 27 - } 28 - 29 - export type LogoId = "default" | "catppuccin" | "schoolbox" | "schooltape" | "schooltape-rainbow" | "schooltape-legacy"; 30 - 31 - export interface LogoInfo { 32 - name: string; 33 - url: string; 34 - disable?: boolean; // whether the icon should be injected or not 35 - adaptive?: boolean; // whether the icon should follow the accent colour 36 58 } 37 59 38 60 export interface UserSnippet {