mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
feat: optimisations
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
Reference in New Issue
Block a user