feat: optimisations

This commit is contained in:
2026-07-19 18:08:52 +09:30
parent 10db972ef4
commit 824cedca2f
63 changed files with 1098 additions and 1719 deletions
@@ -1,9 +1,3 @@
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm";
@@ -25,34 +19,12 @@ interface weightingsStorage {
weightingOverrides: Record<string, string>;
}
const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage",
}),
});
class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
@Setting(settings.lettergrade)
lettergrade!: boolean;
}
const instance = new AssessmentsAveragePluginClass();
let overrideListenerController: AbortController | null = null;
let wrapperColourObserver: MutationObserver | null = null;
let wrapperColourObserverTimeout: ReturnType<typeof setTimeout> | null = null;
const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
id: "assessments-average",
name: "Assessment Averages",
description: "Adds an average grade to the Assessments page",
version: "1.0.0",
disableToggle: true,
settings: instance.settings,
run: async (api) => {
const assessmentsAveragePlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await initStorage(api);
clearStuck(api);
@@ -0,0 +1,20 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage",
}),
});
export default defineLazyPlugin({
id: "assessments-average",
name: "Assessment Averages",
description: "Adds an average grade to the Assessments page",
version: "1.0.0",
disableToggle: true,
settings,
loader: () => import("./index"),
});
@@ -1,3 +1,5 @@
import debounce from "@/seqta/utils/debounce";
const RUBRIC_SELECTOR =
"[class*='AssessableCriterion__rubric___'][class*='Rubric__Rubric___'], [class*='Rubric__Rubric___'][class*='AssessableCriterion__rubric___']";
const ENHANCED_ATTR = "data-betterseqta-rubric-copy";
@@ -365,13 +367,13 @@ function enhanceRubrics(root: ParentNode = document) {
root.querySelectorAll<HTMLElement>(RUBRIC_SELECTOR).forEach(enhanceRubric);
}
const debouncedEnhanceRubrics = debounce(enhanceRubrics, 50);
function watchRubrics(root: ParentNode) {
observer?.disconnect();
enhanceRubrics(root);
observer = new MutationObserver(() => {
enhanceRubrics(root);
});
observer = new MutationObserver(() => debouncedEnhanceRubrics(root));
observer.observe(root, { childList: true, subtree: true });
}
@@ -15,8 +15,6 @@ import { extractWeightFromCoversheetText } from "./extractWeightFromCoversheetTe
export { extractWeightFromCoversheetText };
ensurePdfjsWorker();
export const WEIGHTING_SCHEMA_VERSION = 1;
export interface WeightingEntry {
@@ -655,6 +653,7 @@ export async function extractPDFText(url: string): Promise<string> {
throw new Error("PDF response is empty");
}
ensurePdfjsWorker();
const pdf = await pdfjs.getDocument({
data: arrayBuffer,
useSystemFonts: true,
@@ -53,7 +53,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
"Adds an overview option to the assessments page that organizes assessments by status",
version: "1.0.0",
settings: {},
disableToggle: false,
disableToggle: true,
styles,
run: async () => {
@@ -304,7 +304,6 @@ const enhancedNavigationPlugin: Plugin<typeof settings> = {
injectStyles();
window.addEventListener("resize", positionArrows);
window.addEventListener("scroll", positionArrows, true);
const navObservers: MutationObserver[] = [];
const courseObservers: MutationObserver[] = [];
@@ -355,7 +354,6 @@ const enhancedNavigationPlugin: Plugin<typeof settings> = {
return () => {
window.removeEventListener("resize", positionArrows);
window.removeEventListener("scroll", positionArrows, true);
courseMount.unregister();
navObservers.forEach((observer) => observer.disconnect());
courseObservers.forEach((observer) => observer.disconnect());
@@ -0,0 +1,23 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
const settings = defineSettings({
autoScrollOnClick: booleanSetting({
default: false,
title: "Auto-scroll navigator on click",
description:
"When you click a lesson directly in the side panel, automatically scroll it to the centre. The prev/next arrows always centre the selected lesson regardless of this setting.",
}),
});
export default defineLazyPlugin({
id: "enhanced-navigation",
name: "Enhanced Navigation",
description:
"Keeps the course navigator focused on the current lesson and adds prev/next lesson arrows.",
version: "1.0.0",
disableToggle: true,
settings,
beta: false,
loader: () => import("./index"),
});
@@ -10,10 +10,11 @@
import Calculator from './Calculator.svelte';
import { actionMap } from '../indexing/actions';
import type { IndexItem } from '../indexing/types';
import debounce from 'lodash/debounce';
import debounce from '@/seqta/utils/debounce';
import { renderComponentMap } from '../indexing/renderComponents';
import HighlightedText from '../utils/HighlightedText.svelte';
import { matchesHotkey } from '../utils/hotkeyUtils';
import { warmUpVectorSearchOnInteraction } from '../search/vector/vectorSearch';
import browser from 'webextension-polyfill';
const {
@@ -93,6 +94,7 @@
keydownHandler = (e: KeyboardEvent) => {
if (matchesHotkey(e, currentSearchHotkey)) {
e.preventDefault();
warmUpVectorSearchOnInteraction();
commandPalleteOpen = true;
tick().then(() => searchbar?.focus());
}
@@ -146,6 +148,7 @@
// @ts-ignore - Intentionally adding to window
window.setCommandPalleteOpen = (open: boolean) => {
if (open) warmUpVectorSearchOnInteraction();
commandPalleteOpen = open;
};
@@ -5,10 +5,8 @@ import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer";
import { installResetIndexMessageListener } from "../indexing/resetIndexes";
import { isIndexingPaused } from "../indexing/indexingPause";
import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from "embeddia";
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
import { checkAndHandleUpdate } from "../utils/versionCheck";
import {
getStoredPassiveItems,
@@ -68,20 +66,10 @@ const globalSearchPlugin: Plugin<{}> = {
console.error("Failed to create IndexedDB:", error);
}
initVectorSearch();
setTimeout(async () => {
try {
const { isVectorSearchSupported } = await import("../utils/browserDetection");
if (isVectorSearchSupported()) VectorWorkerManager.getInstance();
} catch (error) {
console.warn("[Global Search] Vector worker warm-up failed:", error);
}
}, 1000);
// @ts-ignore
window.globalSearchDebug = {
resetWorker: () => VectorWorkerManager.getInstance().resetWorker(),
resetWorker: async () =>
(await import("../indexing/worker/vectorWorkerManager")).VectorWorkerManager.getInstance().resetWorker(),
passiveItems: getStoredPassiveItems,
runSelfTests: async () =>
(await import("../indexing/selfTests")).runGlobalSearchSelfTests(),
@@ -1,6 +1,6 @@
import SearchBar from "../components/SearchBar.svelte";
import { unmount } from "svelte";
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
import { warmUpVectorSearchOnInteraction } from "../search/vector/vectorSearch";
import { formatHotkeyForDisplay, isValidHotkey } from "../utils/hotkeyUtils";
import browser from "webextension-polyfill";
@@ -253,17 +253,18 @@ export async function mountSearchBar(
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
searchButton.addEventListener("click", () => {
warmUpVectorSearchOnInteraction();
// @ts-ignore - Intentionally adding to window
window.setCommandPalleteOpen(true);
});
try {
const { default: renderSvelte } = await import("@/interface/renderInShadow");
const { default: renderSvelte } = await import("@/interface/main");
appRef.current = renderSvelte(SearchBar, searchRootShadow, {
transparencyEffects: api.settings.transparencyEffects,
showRecentFirst: api.settings.showRecentFirst,
searchHotkey: currentHotkey,
});
}, "content");
} catch (error) {
console.error("Error rendering Svelte component:", error);
}
@@ -314,8 +315,10 @@ export function cleanupSearchBar(appRef: {
searchRoot.remove();
}
// Clean up vector worker
VectorWorkerManager.getInstance().terminate();
// Clean up vector worker when it was started (indexing or search interaction)
void import("../indexing/worker/vectorWorkerManager").then(({ VectorWorkerManager }) => {
VectorWorkerManager.getInstance().terminate();
}).catch(() => {});
if (appRef.storageChangeHandler) {
browser.storage.onChanged.removeListener(appRef.storageChangeHandler);
@@ -8,6 +8,13 @@ import { verboseDebug } from "@/utils/verboseLog";
let vectorIndex: EmbeddingIndex | null = null;
let initializationAttempted = false;
let initializationFailed = false;
let interactionWarmupStarted = false;
export function warmUpVectorSearchOnInteraction(): void {
if (interactionWarmupStarted) return;
interactionWarmupStarted = true;
void initVectorSearch();
}
export async function initVectorSearch() {
if (initializationFailed || !isVectorSearchSupported()) {
@@ -1,4 +1,10 @@
import { create, all, typeOf as mathTypeOf, format as mathFormat } from 'mathjs';
import {
create, absDependencies, addDependencies, cosDependencies, divideDependencies, eDependencies,
evaluateDependencies, formatDependencies, log10Dependencies, logDependencies, modDependencies,
multiplyDependencies, piDependencies, powDependencies, sinDependencies, sqrtDependencies,
subtractDependencies, tanDependencies, toDependencies, typeOfDependencies, unaryMinusDependencies,
unaryPlusDependencies, unitDependencies,
} from 'mathjs';
import { unitFullNames } from './unitMap';
export interface CalculatorResult {
@@ -10,38 +16,34 @@ export interface CalculatorResult {
error?: string;
}
/** Hard cap on calculator input length to limit parse/eval cost. */
export const CALCULATOR_MAX_INPUT_LENGTH = 128;
/**
* Functions safe to replace with stubs. Do not block type constructors
* (`complex`, `typed`, `fraction`, `bignumber`, `sparse`) or parse pipeline
* (`parse`, `compile`, `parser`) — mathjs needs those internally and
* `evaluate()` depends on them.
*/
const BLOCKED_MATH_FUNCTIONS = [
'import',
'createUnit',
'random',
'pickRandom',
'chain',
'help',
] as const;
const CALCULATOR_MATH_CONFIG = Object.assign(
{},
evaluateDependencies, formatDependencies, unitDependencies, typeOfDependencies,
addDependencies, subtractDependencies, multiplyDependencies, divideDependencies,
powDependencies, modDependencies, unaryMinusDependencies, unaryPlusDependencies,
absDependencies, sqrtDependencies, logDependencies, log10Dependencies,
sinDependencies, cosDependencies, tanDependencies, toDependencies,
piDependencies, eDependencies,
);
const BLOCKED_MATH_FUNCTIONS = ['import', 'createUnit', 'random', 'pickRandom', 'chain', 'help'] as const;
function createSandboxedMath() {
const sandbox = create(all);
const blockFn = () => {
throw new Error('Function not allowed');
};
const sandbox = create(CALCULATOR_MATH_CONFIG);
const blockFn = () => { throw new Error('Function not allowed'); };
const blocked: Record<string, () => never> = {};
for (const name of BLOCKED_MATH_FUNCTIONS) {
blocked[name] = blockFn;
}
for (const name of BLOCKED_MATH_FUNCTIONS) blocked[name] = blockFn;
sandbox.import(blocked, { override: true });
return sandbox;
}
const calculatorMath = createSandboxedMath();
const FORMAT_OPTS = { precision: 14, lowerExp: -15, upperExp: 15 } as const;
const emptyResult = (error?: string): CalculatorResult => ({
result: null, isValid: false, isPartial: false, inputUnit: '', outputUnit: '', error,
});
function detectUnit(expression: string): string {
try {
@@ -50,161 +52,63 @@ function detectUnit(expression: string): string {
const unitStr = unit.formatUnits();
return unitFullNames[unitStr] || unitStr;
}
} catch (e) {
// Not a unit or invalid expression
}
} catch {}
return '';
}
function isLikelyMathExpression(input: string): boolean {
const trimmed = input.trim();
// Must contain at least one digit or mathematical operator
if (!/[\d+\-*/^()=.]/.test(trimmed)) {
return false;
}
// Check for common non-math words that shouldn't trigger calculator
if (!/[\d+\-*/^()=.]/.test(trimmed)) return false;
const nonMathWords = ['abs', 'function', 'class', 'const', 'let', 'var', 'if', 'else', 'while', 'for', 'return', 'import', 'export'];
const words = trimmed.toLowerCase().split(/\s+/);
// If it's just a single non-math word, skip it
if (words.length === 1 && nonMathWords.includes(words[0]) && !/\d/.test(trimmed)) {
return false;
}
// Must have some mathematical content
const mathPattern = /(\d+\.?\d*|\+|\-|\*|\/|\^|\(|\)|sin|cos|tan|log|sqrt|pi|e|=)/i;
return mathPattern.test(trimmed);
if (words.length === 1 && nonMathWords.includes(words[0]) && !/\d/.test(trimmed)) return false;
return /(\d+\.?\d*|\+|\-|\*|\/|\^|\(|\)|sin|cos|tan|log|sqrt|pi|e|=)/i.test(trimmed);
}
function tryCompleteExpression(expression: string): string | null {
const trimmed = expression.trim();
// Common patterns for incomplete expressions
const incompletePatterns = [
/[\+\-\*\/\^]\s*$/, // ends with operator
/\(\s*$/, // ends with opening parenthesis
/[\+\-\*\/\^]\s*\(/, // operator followed by opening parenthesis
];
for (const pattern of incompletePatterns) {
if (pattern.test(trimmed)) {
// Try to evaluate what we have so far by removing the incomplete part
let partial = trimmed.replace(/[\+\-\*\/\^]\s*$/, '').trim();
// Handle cases like "4 + 3 *" -> evaluate "4 + 3"
if (partial && !partial.match(/[\+\-\*\/\^]\s*$/)) {
try {
const result = calculatorMath.evaluate(partial);
if (typeof result === 'number' && !isNaN(result)) {
return calculatorMath.format(result, { precision: 14, lowerExp: -15, upperExp: 15 });
}
} catch (e) {
// Continue to other attempts
}
for (const pattern of [/[\+\-\*\/\^]\s*$/, /\(\s*$/, /[\+\-\*\/\^]\s*\(/]) {
if (!pattern.test(trimmed)) continue;
const partial = trimmed.replace(/[\+\-\*\/\^]\s*$/, '').trim();
if (!partial || partial.match(/[\+\-\*\/\^]\s*$/)) continue;
try {
const result = calculatorMath.evaluate(partial);
if (typeof result === 'number' && !isNaN(result)) {
return calculatorMath.format(result, FORMAT_OPTS);
}
}
} catch {}
}
return null;
}
export function calculateExpression(input: string): CalculatorResult {
const trimmed = input.trim();
// Early exit for empty or very short inputs
if (!trimmed || (trimmed.length <= 2 && !/\d/.test(trimmed))) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
}
if (!trimmed || (trimmed.length <= 2 && !/\d/.test(trimmed))) return emptyResult();
if (trimmed.length > CALCULATOR_MAX_INPUT_LENGTH) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
error: `Expression too long (max ${CALCULATOR_MAX_INPUT_LENGTH} characters)`,
};
return emptyResult(`Expression too long (max ${CALCULATOR_MAX_INPUT_LENGTH} characters)`);
}
// Check if this looks like a math expression at all
if (!isLikelyMathExpression(trimmed)) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
}
if (!isLikelyMathExpression(trimmed)) return emptyResult();
try {
// First try to evaluate the expression as-is
const evaluated = calculatorMath.evaluate(trimmed.replace('**', '^'));
if (evaluated !== undefined) {
let result: string;
let inputUnit = '';
let outputUnit = '';
if (mathTypeOf(evaluated) === 'Unit') {
// Handle unit conversion results
result = calculatorMath.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
inputUnit = detectUnit(trimmed);
outputUnit = detectUnit(result);
} else if (typeof evaluated === 'number') {
// Handle regular numbers
result = mathFormat(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
} else {
result = mathFormat(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
}
const result = calculatorMath.format(evaluated, FORMAT_OPTS);
const isUnit = calculatorMath.typeOf(evaluated) === 'Unit';
return {
result,
isValid: true,
isPartial: false,
inputUnit,
outputUnit,
inputUnit: isUnit ? detectUnit(trimmed) : '',
outputUnit: isUnit ? detectUnit(result) : '',
};
}
} catch (error) {
// Try to handle incomplete expressions
const partialResult = tryCompleteExpression(trimmed);
if (partialResult) {
return {
result: partialResult,
isValid: true,
isPartial: true,
inputUnit: '',
outputUnit: '',
};
return { result: partialResult, isValid: true, isPartial: true, inputUnit: '', outputUnit: '' };
}
// If it still looks like math but failed, return the error
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
error: error instanceof Error ? error.message : 'Invalid expression',
};
return emptyResult(error instanceof Error ? error.message : 'Invalid expression');
}
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
return emptyResult();
}
+33 -107
View File
@@ -1,22 +1,5 @@
import type { Plugin } from "../../core/types";
import { booleanSetting } from "@/plugins/core/settingsHelpers";
import { waitForElm } from "@/seqta/utils/waitForElm";
import styles from "./styles.css?inline";
const messageFoldersSettings = {
showTagsInAllMessages: booleanSetting({
default: true,
title: "Show folder tags in All Messages",
description:
"When off, folder tags are not shown on the message list until you select a folder.",
}),
hideFolderedMessagesInAll: booleanSetting({
default: true,
title: "Hide foldered messages in All Messages",
description:
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
}),
} as const;
interface Folder {
id: string;
@@ -25,33 +8,31 @@ interface Folder {
emoji: string;
}
interface MessageFoldersStorage {
folders: Folder[];
messageAssignments: Record<string, string[]>;
}
const FOLDER_COLORS = [
"#3b82f6", "#ef4444", "#22c55e", "#f59e0b",
"#8b5cf6", "#ec4899", "#14b8a6", "#f97316",
];
const folderHeroicon = (inner: string) =>
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
const FOLDER_HEROICONS = [
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
folderHeroicon('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'),
folderHeroicon('<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>'),
folderHeroicon('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>'),
folderHeroicon('<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>'),
folderHeroicon('<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>'),
folderHeroicon('<path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/>'),
folderHeroicon('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>'),
folderHeroicon('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>'),
folderHeroicon('<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>'),
folderHeroicon('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>'),
folderHeroicon('<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'),
folderHeroicon('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
folderHeroicon('<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>'),
folderHeroicon('<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>'),
folderHeroicon('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'),
folderHeroicon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
];
const FOLDER_ICON_SVG = `<svg style="width:24px;height:24px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
@@ -177,20 +158,8 @@ function createFolderBadge(
return badge;
}
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
id: "messageFolders",
name: "Message Folders",
description: "Organize direct messages into custom folders",
version: "2.0.0",
settings: messageFoldersSettings,
disableToggle: true,
defaultEnabled: true,
run: async (api) => {
const styleEl = document.createElement("style");
styleEl.textContent = styles;
document.head.appendChild(styleEl);
const messageFoldersPlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await api.storage.loaded;
if (!api.storage.folders) api.storage.folders = [];
@@ -202,7 +171,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
let actionsObserver: MutationObserver | null = null;
let openDropdown: HTMLElement | null = null;
let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null;
let foldedSection: HTMLElement | null = null;
const unregisters: Array<{ unregister: () => void }> = [];
const getFolders = (): Folder[] =>
@@ -217,23 +185,11 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
api.storage.messageAssignments = { ...assignments };
};
const getMessageFolderIds = (messageId: string): string[] => {
const assignments = getAssignments();
const ids: string[] = [];
for (const [folderId, msgIds] of Object.entries(assignments)) {
if (msgIds.includes(messageId)) ids.push(folderId);
}
return ids;
};
const assignMessageToFolder = (messageId: string, folderId: string, add: boolean) => {
const assignMessageToFolder = (messageId: string, folderId: string) => {
const assignments = getAssignments();
if (!assignments[folderId]) assignments[folderId] = [];
const idx = assignments[folderId].indexOf(messageId);
if (add && idx < 0) {
if (!assignments[folderId].includes(messageId)) {
assignments[folderId].push(messageId);
} else if (!add && idx >= 0) {
assignments[folderId].splice(idx, 1);
}
saveAssignments(assignments);
};
@@ -262,36 +218,13 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
}
};
const isMessageInAnyCustomFolder = (messageId: string): boolean => {
for (const msgIds of Object.values(getAssignments())) {
if (msgIds.includes(messageId)) return true;
}
return false;
};
const isMessageInAnyCustomFolder = (messageId: string): boolean =>
getAssignedFolderIds(messageId, getAssignments()).length > 0;
const shouldShowBadgesInList = (): boolean => {
return api.settings.showTagsInAllMessages || activeFolderId !== null;
};
const getSelectedMessageId = (): string | null => {
const selectedMsg = document.querySelector("[class*='MessageList__selected___']");
return selectedMsg?.getAttribute("data-message") ?? null;
};
const getMessageIdFromEvent = (target: HTMLElement): string | null => {
const li = target.closest("li[data-message]");
return li?.getAttribute("data-message") ?? null;
};
const getAllVisibleMessageIds = (): string[] => {
const ids: string[] = [];
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
const id = li.getAttribute("data-message");
if (id) ids.push(id);
});
return ids;
};
const showConfirmModal = (title: string, message: string, onConfirm: () => void) => {
const overlay = document.createElement("div");
overlay.className = "bsplus-modal-overlay";
@@ -338,13 +271,11 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
ol.appendChild(section);
}
foldedSection = section;
const folders = getFolders();
section.innerHTML = "";
const header = document.createElement("div");
header.className = "bsplus-folders-header";
header.dataset.folded = "false";
const collapseBtn = document.createElement("button");
collapseBtn.className = "bsplus-folders-collapse";
@@ -426,7 +357,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
editBtn.innerHTML = EDIT_SVG;
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
showEditFolderInput(section!, folder);
showNewFolderInput(section!, folder);
});
actions.appendChild(editBtn);
@@ -518,7 +449,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
const messageId = data.replace("msg:", "");
const folderId = (e.target as HTMLElement).closest("[data-folder-id]")?.getAttribute("data-folder-id");
if (messageId && folderId) {
assignMessageToFolder(messageId, folderId, true);
assignMessageToFolder(messageId, folderId);
applyBadges();
applyFolderFilter();
renderSidebarFolders();
@@ -530,7 +461,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const attachDragListeners = () => {
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
getMessageListItems().forEach((li) => {
if (li.getAttribute("data-bsplus-drag") === "true") return;
li.setAttribute("data-bsplus-drag", "true");
li.draggable = true;
@@ -665,10 +596,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
container.appendChild(picker);
};
const showEditFolderInput = (container: Element, folder: Folder) => {
showNewFolderInput(container, folder);
};
const attachNativeSidebarListeners = () => {
const sidebar = document.querySelector("[class*='Viewer__sidebar___']");
if (!sidebar) return;
@@ -707,7 +634,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
dropdown.dataset.msgId = messageId;
const folders = getFolders();
const currentFolderIds = getMessageFolderIds(messageId);
const currentFolderIds = getAssignedFolderIds(messageId, getAssignments());
if (folders.length === 0) {
const empty = document.createElement("div");
@@ -747,7 +674,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
e.stopPropagation();
e.preventDefault();
toggleMessageInFolder(messageId, folder.id);
const nowChecked = getMessageFolderIds(messageId).includes(folder.id);
const nowChecked = getAssignedFolderIds(messageId, getAssignments()).includes(folder.id);
item.classList.toggle("bsplus-checked", nowChecked);
check.style.borderColor = nowChecked ? folder.color : "";
check.style.background = nowChecked ? folder.color : "";
@@ -819,7 +746,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
menu.appendChild(title);
const folders = getFolders();
const currentFolderIds = getMessageFolderIds(messageId);
const currentFolderIds = getAssignedFolderIds(messageId, getAssignments());
if (folders.length === 0) {
const empty = document.createElement("div");
@@ -907,7 +834,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const applyFolderFilter = () => {
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]");
const messageItems = getMessageListItems();
const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button");
if (activeFolderId === null) {
if (api.settings.hideFolderedMessagesInAll) {
@@ -952,7 +879,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const attachContextMenuListeners = () => {
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
getMessageListItems().forEach((li) => {
if (li.getAttribute("data-bsplus-ctx") === "true") return;
li.setAttribute("data-bsplus-ctx", "true");
li.addEventListener("contextmenu", (e) => {
@@ -1022,7 +949,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
sidebarObserver?.disconnect();
actionsObserver?.disconnect();
closeDropdown();
styleEl.remove();
document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove());
document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove());
document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove());
@@ -0,0 +1,30 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
import styles from "./styles.css?inline";
const settings = defineSettings({
showTagsInAllMessages: booleanSetting({
default: true,
title: "Show folder tags in All Messages",
description:
"When off, folder tags are not shown on the message list until you select a folder.",
}),
hideFolderedMessagesInAll: booleanSetting({
default: true,
title: "Hide foldered messages in All Messages",
description:
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
}),
});
export default defineLazyPlugin({
id: "messageFolders",
name: "Message Folders",
description: "Organize direct messages into custom folders",
version: "2.0.0",
settings,
disableToggle: true,
defaultEnabled: true,
styles,
loader: () => import("./index"),
});
-57
View File
@@ -1,57 +0,0 @@
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Step 1: Define settings with proper typing
const settings = defineSettings({
someSetting: booleanSetting({
default: true,
title: "Test Plugin",
description: "Some random setting",
}),
});
// Step 2: Create the plugin class with @Setting decorators
class TestPluginClass extends BasePlugin<typeof settings> {
@Setting(settings.someSetting)
someSetting!: boolean;
}
// Step 3: Instantiate and plug it in
const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settings> = {
id: "test",
name: "Test Plugin",
description: "A test plugin for BetterSEQTA+",
version: "1.0.0",
settings: settingsInstance.settings,
disableToggle: true,
beta: true,
run: async (api) => {
console.log("Test plugin running");
api.events.on("ping", (data) => {
console.log("Ping received! Page changed to: ", data);
});
const { unregister } = api.seqta.onPageChange((page) => {
//console.log('Page changed to', page);
api.events.emit("ping", page);
console.log("Current setting value:", api.settings.someSetting);
});
return () => {
console.log("Test plugin stopped");
unregister();
};
},
};
export default testPlugin;
@@ -67,7 +67,6 @@ export type InstallThemeMeta = {
export class ThemeManager {
private static instance: ThemeManager;
private currentTheme: CustomTheme | null = null;
private previousImageVariableNames: string[] = [];
private lastSyncedImageKey: string | null = null;
private originalPreviewColor: string | null = null;
private originalPreviewTheme: boolean | null = null;
@@ -914,9 +913,6 @@ export class ThemeManager {
images: CustomImages,
});
this.lastSyncedImageKey = this.imageSyncKey(CustomImages);
this.previousImageVariableNames = CustomImages.map(
(image) => image.variableName,
);
// Apply theme settings
if (shouldForceThemeAppearance(theme)) {
@@ -951,9 +947,6 @@ export class ThemeManager {
}
}
const newImageVariableNames =
theme.CustomImages?.map((image) => image.variableName) ?? [];
const syncInput: ThemePageSyncInput = {};
if (theme.CustomCSS !== undefined) {
@@ -966,7 +959,6 @@ export class ThemeManager {
syncInput.images = theme.CustomImages;
this.lastSyncedImageKey = imageKey;
}
this.previousImageVariableNames = newImageVariableNames;
}
if (Object.keys(syncInput).length > 0) {
@@ -1009,7 +1001,6 @@ export class ThemeManager {
try {
void syncThemeToPage({ clearPreview: true, images: [] });
this.lastSyncedImageKey = null;
this.previousImageVariableNames = [];
clearCustomThemeAdaptiveCssVariables();
+5 -41
View File
@@ -35,18 +35,9 @@ const DEFAULT_INTERVAL_MS = 600_000;
/**
* IDs of decorative city layers injected when `themeDom.cityLayers` is on.
* Order matters: earlier entries paint behind later ones. Buildings sit
* behind the lit batches (so windows draw over the silhouettes); flicker
* frames sit over the lit batches so blinking windows hide steady ones.
* Sun and moon are last so they paint over everything else in the
* wallpaper stack (still behind #main via z-index 0).
* `city-buildings` paints the night panorama; `city-day` paints the day
* panorama on top with opacity controlled by `--city-day-opacity`.
*/
/**
* `city-buildings` always paints the night panorama; `city-day` paints
* the day panorama on top with opacity controlled by `--city-day-
* opacity`. The two stack so we can CSS-transition opacity between
* them at the day boundary, instead of snapping background-image (which
* doesn't animate). */
const CITY_LAYER_IDS = [
"city-buildings",
"city-day",
@@ -61,7 +52,7 @@ const CITY_LAYER_IDS = [
// Built-in functions themes may reference by exact string match.
const BUILTINS: Record<string, () => void> = {
"setTimeState()": setTimeState,
"setTimeState()": setCityTime,
"setCityTime()": setCityTime,
};
@@ -137,10 +128,7 @@ function readDevOverride(): number | null {
* 19:00 .. 21:00 evening (deep indigo, fading toward night)
* 21:00 .. 24:00 night
*
* The discrete bucket here drives `data-city-state` (used by the car
* sprite swap and the day panorama). The continuous sky-colour lerp in
* `TIME_BOUNDARIES` MUST use the same minute markers so the boundary
* the user sees in the dev slider matches the visible colour change.
* The discrete bucket here drives `data-city-state` (car sprites, day panorama).
*/
function timeStateForMinutes(minutes: number): TimeState {
if (minutes < 5 * 60 + 30) return "night";
@@ -165,19 +153,6 @@ function clamp01(value: number): number {
return Math.max(0, Math.min(1, value));
}
/**
* Sky colour publishing is intentionally NOT time-interpolated. We
* publish exactly one of the five `SKY_COLOR` anchors based on the
* discrete state from `timeStateForMinutes()`, and let CSS handle the
* cross-fade via its own `transition: background-color` on #content.
*
* That way the sky is a flat colour for the entire duration of each
* phase, and only animates between two anchors AT THE MOMENT the state
* boundary is crossed. The animation duration is decoupled from how
* long the phase lasts.
*
* Removed: the previous `lerpColor` + `TIME_BOUNDARIES` minute table.
*/
function skyColorForState(state: TimeState): string {
return SKY_COLOR[state];
}
@@ -247,18 +222,7 @@ function formatMinutes(minutes: number): string {
}
/**
* Read the clock and update `data-city-state` + sky colour on <html>. Only
* writes when the state actually changes, so CSS transitions are driven by
* real state changes rather than every tick.
*/
export function setTimeState(): void {
setCityTime();
}
/**
* Read the clock and publish continuous city variables plus the discrete
* `data-city-state` bucket used by CSS that still needs hard cuts (e.g. day
* panorama swap, night car sprites).
* Read the clock and publish city variables plus `data-city-state`.
*/
export function setCityTime(): void {
const minutes = getMinutesOfDay();
@@ -2,24 +2,30 @@
import { onMount } from "svelte";
import type { Snippet } from "svelte";
import { fade, fly } from "svelte/transition";
import browser from "webextension-polyfill";
import {
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
} from "@/config/googleCalendar";
import { maybeRunDueWeeklySync } from "@/seqta/utils/googleCalendar/calendarSyncListener";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import {
deleteSyncedEventsFromGoogleCalendar,
deleteSyncedEventsFromOutlookCalendar,
} from "@/seqta/utils/calendarSync/syncEngine";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
import {
connectCalendarProvider,
disconnectCalendarProvider,
fetchCalendarStatuses,
getCalendarAccessToken,
runGoogleCalendarSync,
runOutlookCalendarSync,
updateGoogleSyncSettings,
} from "@/seqta/utils/calendarSync/syncRunner";
import type {
GoogleCalendarStatus,
GoogleCalendarSyncProgress,
GoogleCalendarSyncResult,
} from "@/seqta/utils/googleCalendar/types";
import type { OutlookCalendarStatus } from "@/seqta/utils/outlookCalendar/storage";
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
import OutlookCalendarIcon from "./OutlookCalendarIcon.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
@@ -31,10 +37,11 @@
syncProgressPercent,
type CalendarProvider,
} from "./calendarSyncUi";
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
type BusyState = { provider: CalendarProvider; phase: BusyPhase } | null;
type ProviderStatus = { configured: boolean; connected: boolean; lastSyncAt?: number };
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
type BusyState = { provider: CalendarProvider; phase: BusyPhase } | null;
function setProviderStatus(provider: CalendarProvider, patch: Partial<ProviderStatus>) {
if (provider === "google") googleStatus = { ...googleStatus, ...patch };
else outlookStatus = { ...outlookStatus, ...patch };
@@ -128,30 +135,13 @@
}
async function refreshStatus() {
const [google, outlook] = await Promise.all([
browser.runtime.sendMessage({ type: "googleCalendarStatus" }) as Promise<GoogleCalendarStatus>,
browser.runtime.sendMessage({ type: "outlookCalendarStatus" }) as Promise<OutlookCalendarStatus>,
]);
const { google, outlook } = await fetchCalendarStatuses();
googleStatus = google;
outlookStatus = outlook;
syncWeeksAhead = google.syncWeeksAhead ?? 12;
autoSyncWeekly = google.autoSyncWeekly !== false;
}
async function getAccessToken(provider: CalendarProvider): Promise<string> {
const messageType =
provider === "google" ? "googleCalendarGetAccessToken" : "outlookCalendarGetAccessToken";
const res = (await browser.runtime.sendMessage({ type: messageType })) as {
success?: boolean;
accessToken?: string;
error?: string;
};
if (!res?.success || !res.accessToken) {
throw new Error(res?.error ?? "Could not get calendar access token.");
}
return res.accessToken;
}
function handleSyncProgress(progress: GoogleCalendarSyncProgress) {
syncProgress = progress;
}
@@ -160,10 +150,7 @@
syncWeeksAhead?: number;
autoSyncWeekly?: boolean;
}) {
const result = (await browser.runtime.sendMessage({
type: "googleCalendarUpdateSyncSettings",
...patch,
})) as GoogleCalendarStatus & { success?: boolean };
const result = await updateGoogleSyncSettings(patch);
if (result.syncWeeksAhead != null) syncWeeksAhead = result.syncWeeksAhead;
if (result.autoSyncWeekly != null) autoSyncWeekly = result.autoSyncWeekly;
googleStatus = { ...googleStatus, ...result };
@@ -174,12 +161,6 @@
mode: "full" | "incremental" = "full",
): Promise<boolean> {
const run = provider === "google" ? runGoogleCalendarSync : runOutlookCalendarSync;
const format = (result: GoogleCalendarSyncResult) =>
formatLessonSyncResultMessage(
result,
`${calendarProviderLabel(provider)} Calendar`,
);
const result = await run({ mode, onProgress: handleSyncProgress });
syncProgress = null;
@@ -193,7 +174,9 @@
lastSyncAt: result.lastSyncAt ?? providerStatus(provider).lastSyncAt,
});
showToastMessage(format(result));
showToastMessage(
formatLessonSyncResultMessage(result, `${calendarProviderLabel(provider)} Calendar`),
);
return true;
}
@@ -202,12 +185,8 @@
if (!status.configured || isBusy) return;
menuOpen = false;
busy = { provider, phase: "connect" };
const connectType =
provider === "google" ? "googleCalendarConnect" : "outlookCalendarConnect";
try {
const result = (await browser.runtime.sendMessage({
type: connectType,
})) as GoogleCalendarSyncResult;
const result = await connectCalendarProvider(provider);
if (!result.success) {
showToastMessage(
result.error ?? `Could not connect to ${calendarProviderLabel(provider)} Calendar.`,
@@ -225,7 +204,6 @@
busy = null;
}
}
async function syncProvider(provider: CalendarProvider) {
const status = providerStatus(provider);
if (!status.configured || isBusy) return;
@@ -263,7 +241,7 @@
provider === "google"
? deleteSyncedEventsFromGoogleCalendar
: deleteSyncedEventsFromOutlookCalendar;
const result = await deleteFn(location.origin, () => getAccessToken(provider), {
const result = await deleteFn(location.origin, () => getCalendarAccessToken(provider), {
onProgress: handleSyncProgress,
});
@@ -275,11 +253,11 @@
const removed = result.deleted ?? 0;
modalProvider = null;
const label = calendarProviderLabel(provider);
if (removed === 0) {
showToastMessage("No synced events to remove.");
} else {
showToastMessage(`Removed ${removed} event${removed === 1 ? "" : "s"} from ${label} Calendar.`);
}
showToastMessage(
removed === 0
? "No synced events to remove."
: `Removed ${removed} event${removed === 1 ? "" : "s"} from ${label} Calendar.`,
);
} catch (err) {
showToastMessage(err instanceof Error ? err.message : "Remove failed.", true);
} finally {
@@ -304,13 +282,9 @@
if (isBusy || !modalProvider) return;
const provider = modalProvider;
busy = { provider, phase: "disconnect" };
const disconnectType =
provider === "google" ? "googleCalendarDisconnect" : "outlookCalendarDisconnect";
const label = calendarProviderLabel(provider);
try {
const result = (await browser.runtime.sendMessage({
type: disconnectType,
})) as { success?: boolean };
const result = await disconnectCalendarProvider(provider);
if (!result?.success) {
showToastMessage(`Could not disconnect ${label} Calendar.`, true);
return;
@@ -3,9 +3,8 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
import { getFontPreset } from "@/seqta/ui/fonts/presets";
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
import CalendarSyncControl from "./CalendarSyncControl.svelte";
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
import hostStyles from "./calendarSyncHost.css?inline";
export type CalendarProvider = "google" | "outlook";
@@ -164,6 +163,7 @@ export async function mountGoogleCalendarButton(): Promise<void> {
syncCalendarSyncTheme(mountRoot);
controls.appendChild(mountRoot);
const { default: CalendarSyncControl } = await import("./CalendarSyncControl.svelte");
currentApp = mount(CalendarSyncControl, { target: mountRoot });
}
+2 -16
View File
@@ -1,6 +1,5 @@
import type { Plugin } from "../../core/types";
import { waitForElm } from "@/seqta/utils/waitForElm";
import styles from "./styles.css?inline";
interface TimetableEntryData {
ci: number;
@@ -127,20 +126,8 @@ function showEditModal(
roomInput?.focus();
}
const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
id: "timetableEdit",
name: "Edit Rooms & Teachers",
description: "Edit room and teacher names in timetable classes",
version: "1.0.0",
settings: {},
disableToggle: true,
defaultEnabled: true,
run: async (api) => {
const styleEl = document.createElement("style");
styleEl.textContent = styles;
document.head.appendChild(styleEl);
const timetableEditPlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await api.storage.loaded;
let observer: MutationObserver | null = null;
@@ -514,7 +501,6 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
observer?.disconnect();
quickbarObserver?.disconnect();
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
styleEl.remove();
document.querySelectorAll("[data-timetable-edit-processed]").forEach((el) => {
el.removeAttribute("data-timetable-edit-processed");
});
@@ -0,0 +1,14 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import styles from "./styles.css?inline";
export default defineLazyPlugin({
id: "timetableEdit",
name: "Edit Rooms & Teachers",
description: "Edit room and teacher names in timetable classes",
version: "1.0.0",
settings: {},
disableToggle: true,
defaultEnabled: true,
styles,
loader: () => import("./index"),
});