mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
@@ -1,8 +1,41 @@
|
||||
import fs from "fs";
|
||||
import mime from "mime-types";
|
||||
|
||||
/**
|
||||
* A Vite plugin designed to load files as base64 encoded data URLs.
|
||||
* This plugin intercepts module imports that have a `?base64` query parameter
|
||||
* appended to the file path. It then reads the targeted file, converts its content
|
||||
* to a base64 string, and constructs a data URL which is then exported as the
|
||||
* default export of a new JavaScript module.
|
||||
*
|
||||
* @example
|
||||
* // To use this loader, import a file with ?base64 query:
|
||||
* // import myImageBase64 from './path/to/myimage.png?base64';
|
||||
* // myImageBase64 will then be a string like "data:image/png;base64,..."
|
||||
*/
|
||||
export const base64Loader = {
|
||||
/**
|
||||
* The name of the Vite plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
name: "base64-loader",
|
||||
/**
|
||||
* The core transformation function of the Vite plugin.
|
||||
* It is called by Vite for modules that might need transformation. This function
|
||||
* checks if the module ID includes the `?base64` query. If so, it reads the
|
||||
* specified file, converts it to a base64 data URL, and returns a new
|
||||
* JavaScript module that default exports this data URL.
|
||||
*
|
||||
* @param {any} _ The original code of the file. This parameter is unused by this loader.
|
||||
* @param {string} id The ID of the module being transformed. This string typically
|
||||
* contains the absolute file path and any query parameters
|
||||
* (e.g., "/path/to/file.png?base64").
|
||||
* @returns {string | null} If the module ID does not contain `?base64` query,
|
||||
* it returns `null` to indicate no transformation.
|
||||
* Otherwise, it returns a string of JavaScript code
|
||||
* that default exports the base64 data URL of the file.
|
||||
* For example: `export default 'data:image/png;base64,xxxx';`
|
||||
*/
|
||||
transform(_: any, id: string) {
|
||||
const [filePath, query] = id.split("?");
|
||||
if (query !== "base64") return null;
|
||||
|
||||
+39
-6
@@ -1,25 +1,58 @@
|
||||
// ref: https://stackoverflow.com/a/76920975
|
||||
import type { Plugin } from "vite";
|
||||
|
||||
/**
|
||||
* Creates a Vite plugin designed to gracefully handle the conclusion of the build process.
|
||||
* This plugin utilizes the `buildEnd` and `closeBundle` hooks provided by Vite.
|
||||
* It checks for errors at the end of the build:
|
||||
* - If an error occurred during the build (`buildEnd` hook receives an error), it logs the error
|
||||
* and explicitly exits the Node.js process with a status code of 1 (indicating failure).
|
||||
* - If the build completes without errors and the bundle is successfully generated
|
||||
* (`closeBundle` hook is called), it logs a success message and exits the process
|
||||
* with a status code of 0 (indicating success).
|
||||
* This explicit process exiting can be useful in CI/CD environments or scripts that
|
||||
* rely on the process status code to determine the build outcome.
|
||||
* The core logic for using these hooks to exit the process is inspired by
|
||||
* a solution found on StackOverflow (https://stackoverflow.com/a/76920975).
|
||||
*
|
||||
* @returns {Plugin} A Vite plugin object configured with `name`, `buildEnd`, and `closeBundle` hooks.
|
||||
*/
|
||||
export default function ClosePlugin(): Plugin {
|
||||
return {
|
||||
/**
|
||||
* The unique name of this Vite plugin. This name is used by Vite for identification
|
||||
* purposes and will appear in warnings, errors, and logs related to this plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
name: "ClosePlugin", // required, will show up in warnings and errors
|
||||
|
||||
// use this to catch errors when building
|
||||
/**
|
||||
* A Vite hook that is called when the build process has finished, regardless of
|
||||
* whether it was successful or encountered an error.
|
||||
*
|
||||
* @param {Error} [error] An optional error object. If the build failed, this parameter
|
||||
* will contain the error that occurred. If the build was successful,
|
||||
* this parameter will be undefined or null.
|
||||
*/
|
||||
buildEnd(error) {
|
||||
if (error) {
|
||||
console.error("Error bundling");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exit(1); // Exit with status 1 indicating an error
|
||||
} else {
|
||||
console.log("Build ended");
|
||||
console.log("Build ended"); // Log successful completion of the build phase
|
||||
}
|
||||
},
|
||||
|
||||
// use this to catch the end of a build without errors
|
||||
/**
|
||||
* A Vite hook that is called after the `buildEnd` hook, but only if the build
|
||||
* was successful (i.e., no errors were passed to `buildEnd`) and all output
|
||||
* files have been generated and written to disk. This signifies the successful
|
||||
* completion of the entire bundling process.
|
||||
*/
|
||||
closeBundle() {
|
||||
console.log("Bundle closed");
|
||||
process.exit(0);
|
||||
console.log("Bundle closed"); // Log successful closure of the bundle
|
||||
process.exit(0); // Exit with status 0 indicating a successful build
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+23
-10
@@ -1,12 +1,22 @@
|
||||
import type { Browser, BuildTarget, Manifest } from "./types";
|
||||
import type { AnyCase } from "./utils";
|
||||
|
||||
/**
|
||||
*
|
||||
* Packages a given manifest object with a specific target browser identifier.
|
||||
* This function is typically used in multi-browser extension build processes
|
||||
* to create a configuration object that pairs the manifest data with the browser
|
||||
* it's intended for. The `AnyCase<Browser>` type for the browser parameter
|
||||
* implies that browser names like 'chrome', 'firefox', etc., can be provided
|
||||
* in various casings.
|
||||
*
|
||||
* @export
|
||||
* @param {Manifest} manifest
|
||||
* @param {AnyCase<Browser>} browser
|
||||
* @return {*} {@link BuildTarget}
|
||||
* @param {Manifest} manifest The core manifest data for the extension,
|
||||
* compatible with `chrome.runtime.ManifestV3` as defined by the {@link Manifest} type.
|
||||
* @param {AnyCase<Browser>} browser The target browser identifier (e.g., 'chrome', 'firefox', 'CHROME').
|
||||
* Refers to the {@link Browser} type, allowing for flexible casing.
|
||||
* @returns {BuildTarget} An object that pairs the `manifest` with its target `browser`.
|
||||
* The structure is `{ manifest: Manifest; browser: AnyCase<Browser>; }`
|
||||
* as defined by the {@link BuildTarget} type.
|
||||
*/
|
||||
export function createManifest(
|
||||
manifest: Manifest,
|
||||
@@ -19,14 +29,17 @@ export function createManifest(
|
||||
}
|
||||
|
||||
/**
|
||||
* create a base Manifest to inherit from
|
||||
* type Manifest = chrome.runtime.ManifestV3
|
||||
*
|
||||
* use as shared base to extend inBrowser manifests
|
||||
* Defines a base manifest object.
|
||||
* This function is typically used to establish a common, shared foundation for an extension's manifest
|
||||
* (compatible with `chrome.runtime.ManifestV3` as per the {@link Manifest} type).
|
||||
* This base can then be extended or modified for different browsers or specific build configurations.
|
||||
* For example, you might define core permissions and properties here, and then add
|
||||
* browser-specific keys in subsequent steps.
|
||||
*
|
||||
* @export
|
||||
* @param {Manifest} manifest
|
||||
* @return {*} {@link Manifest}
|
||||
* @param {Manifest} manifest The core manifest data to be used as a base.
|
||||
* This should conform to the {@link Manifest} type structure.
|
||||
* @returns {Manifest} The provided manifest object, intended to serve as a reusable base.
|
||||
*/
|
||||
export function createManifestBase(manifest: Manifest): Manifest {
|
||||
return manifest;
|
||||
|
||||
+42
-9
@@ -1,27 +1,60 @@
|
||||
// vite-plugin-inline-worker-dev.ts
|
||||
// vite-plugin-inline-worker-dev.ts
|
||||
import { Plugin } from "vite";
|
||||
import fs from "fs/promises";
|
||||
import { build, transform } from "esbuild";
|
||||
import { build } from "esbuild";
|
||||
|
||||
/**
|
||||
* Creates a Vite plugin designed for bundling and inlining web worker scripts during development.
|
||||
* This plugin specifically targets module imports that include a `?inlineWorker` query parameter.
|
||||
* When such an import is encountered, the plugin bundles the worker script using `esbuild`
|
||||
* and then generates JavaScript code that inlines this bundled worker as a Blob,
|
||||
* creating the worker instance via `URL.createObjectURL()`.
|
||||
* The name "vite:inline-worker-dev" suggests it's primarily intended for development builds.
|
||||
*
|
||||
* @returns {Plugin} A Vite plugin object with `name` and `load` properties.
|
||||
*/
|
||||
export default function InlineWorkerDevPlugin(): Plugin {
|
||||
return {
|
||||
/**
|
||||
* The unique name of this Vite plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
name: "vite:inline-worker-dev",
|
||||
/**
|
||||
* The Vite hook responsible for loading and transforming modules.
|
||||
* This function intercepts modules imported with `?inlineWorker`.
|
||||
* For such modules, it bundles the worker script and returns JavaScript code
|
||||
* that, when executed, will create an instance of this worker from an inlined Blob.
|
||||
*
|
||||
* @async
|
||||
* @param {string} id The path or ID of the module Vite is attempting to load,
|
||||
* potentially including query parameters (e.g., "/path/to/worker.ts?inlineWorker").
|
||||
* @returns {Promise<string | null>} A promise that resolves to:
|
||||
* - `null` if the module ID does not include `?inlineWorker`.
|
||||
* - A string of JavaScript code if the module is an inline worker.
|
||||
* This code will define a default export function (e.g., `InlineWorker`)
|
||||
* that, when called, creates and returns a new `Worker` instance
|
||||
* from the bundled and inlined worker script.
|
||||
*/
|
||||
async load(id) {
|
||||
if (id.includes("?inlineWorker")) {
|
||||
const [cleanPath] = id.split("?");
|
||||
console.log("cleanPath", cleanPath);
|
||||
const code = await fs.readFile(cleanPath, "utf-8");
|
||||
// Note: Original code had `await fs.readFile(cleanPath, "utf-8");` but `code` wasn't used.
|
||||
// `esbuild` directly takes `cleanPath` as an entry point.
|
||||
const result = await build({
|
||||
entryPoints: [cleanPath],
|
||||
entryPoints: [cleanPath], // esbuild uses the file path directly
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: "browser",
|
||||
format: "iife",
|
||||
target: "esnext",
|
||||
write: false, // We want the output in memory, not written to disk
|
||||
platform: "browser", // Target environment for the worker code
|
||||
format: "iife", // Immediately Invoked Function Expression, suitable for workers
|
||||
target: "esnext", // Transpile to modern JavaScript
|
||||
});
|
||||
|
||||
const workerCode = result.outputFiles[0].text;
|
||||
|
||||
// Construct JavaScript code that will create the worker from a Blob.
|
||||
// This code is what gets returned to Vite and replaces the original import.
|
||||
const workerBlobCode = `
|
||||
const code = ${JSON.stringify(workerCode)};
|
||||
export default function InlineWorker() {
|
||||
@@ -31,7 +64,7 @@ export default function InlineWorkerDevPlugin(): Plugin {
|
||||
`;
|
||||
return workerBlobCode;
|
||||
}
|
||||
return null;
|
||||
return null; // Let Vite handle other modules normally
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+101
-18
@@ -1,8 +1,33 @@
|
||||
/**
|
||||
* @fileoverview
|
||||
* This script is a command-line utility for publishing the BetterSEQTA+ extension.
|
||||
* It automates the process of finding the latest built extension ZIP files for specified
|
||||
* browsers, zipping the project source code (for Firefox), and then invoking the
|
||||
* `publish-extension` tool with the appropriate arguments.
|
||||
*
|
||||
* To use this script, invoke it with Node.js followed by browser arguments:
|
||||
* e.g., `node lib/publish.js --b chrome firefox`
|
||||
* or `node lib/publish.js --b chrome`
|
||||
* or `node lib/publish.js --b firefox`
|
||||
*/
|
||||
|
||||
const glob = require("glob");
|
||||
const semver = require("semver");
|
||||
const { execSync } = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Determines the latest version string from a list of filenames that include version numbers.
|
||||
* Filenames are expected to follow a pattern like `betterseqtaplus@3.4.5.1-chrome.zip`.
|
||||
* This function handles potential 4-part versions (e.g., `3.4.5.1`) by trimming them
|
||||
* to 3 parts (e.g., `3.4.5`) for comparison using the `semver` library. After identifying
|
||||
* the latest semver-compatible version, it returns the original full version string
|
||||
* (e.g., "3.4.5.1") that corresponds to this latest version.
|
||||
*
|
||||
* @param {string[]} files An array of filenames.
|
||||
* @returns {string | null} The latest version string (e.g., "3.4.5.1") found among the files,
|
||||
* or `null` if no valid version numbers are found or no files are provided.
|
||||
*/
|
||||
function getLatestVersion(files) {
|
||||
console.log("Files passed to getLatestVersion:", files);
|
||||
|
||||
@@ -19,32 +44,56 @@ function getLatestVersion(files) {
|
||||
if (!match) return null;
|
||||
|
||||
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
|
||||
const semverVersion = fullVersion.split(".").slice(0, 3).join("."); // Trim to 3.4.5
|
||||
// Trim to 3 parts for semver comparison, as semver typically handles X.Y.Z
|
||||
const semverVersion = fullVersion.split(".").slice(0, 3).join(".");
|
||||
|
||||
return { fullVersion, semverVersion };
|
||||
})
|
||||
.filter(Boolean);
|
||||
.filter(Boolean); // Remove null entries if any file didn't match
|
||||
|
||||
console.log(
|
||||
"Extracted versions:",
|
||||
versions.map((v) => v.semverVersion),
|
||||
);
|
||||
|
||||
if (versions.length === 0) {
|
||||
console.log("No versions extracted.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find latest version using the trimmed semver format
|
||||
const latestSemver = semver.maxSatisfying(
|
||||
versions.map((v) => v.semverVersion),
|
||||
"*",
|
||||
"*", // Satisfy any version, effectively finding the max
|
||||
);
|
||||
console.log("Latest SemVer-compatible version:", latestSemver);
|
||||
|
||||
// Get the full version that matches the latest SemVer version
|
||||
const latestVersion =
|
||||
versions.find((v) => v.semverVersion === latestSemver)?.fullVersion || null;
|
||||
|
||||
console.log("Final selected latest version:", latestVersion);
|
||||
return latestVersion;
|
||||
if (!latestSemver) {
|
||||
console.log("Could not determine latest semver version.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the original full version string that matches the identified latest SemVer version
|
||||
const latestVersionData = versions.find(
|
||||
(v) => v.semverVersion === latestSemver,
|
||||
);
|
||||
const latestFullVersion = latestVersionData ? latestVersionData.fullVersion : null;
|
||||
|
||||
console.log("Final selected latest version:", latestFullVersion);
|
||||
return latestFullVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the latest built ZIP file for a specific browser.
|
||||
* It constructs a glob pattern based on the browser name (e.g., `dist/betterseqtaplus@*-*chrome.zip`),
|
||||
* finds all matching files, and then uses `getLatestVersion` to identify the version string
|
||||
* of the most recent file. Finally, it returns the full path to that specific file.
|
||||
*
|
||||
* @param {string} browser A string indicating the target browser (e.g., "chrome", "firefox").
|
||||
* @returns {string | undefined} The filepath string to the latest ZIP file for the specified browser,
|
||||
* or `undefined` if no matching file is found or if the latest version
|
||||
* cannot be determined.
|
||||
*/
|
||||
function getLatestFiles(browser) {
|
||||
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
||||
console.log("Glob pattern:", pattern);
|
||||
@@ -52,15 +101,32 @@ function getLatestFiles(browser) {
|
||||
const files = glob.sync(pattern);
|
||||
console.log("Files found for browser", browser, ":", files);
|
||||
|
||||
const latestVersion = getLatestVersion(files);
|
||||
if (files.length === 0) {
|
||||
console.log("No files found for browser", browser);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the exact file by matching the original full version
|
||||
const latestVersion = getLatestVersion(files);
|
||||
if (!latestVersion) {
|
||||
console.log("Could not determine latest version for browser", browser);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the exact file by matching the original full version string
|
||||
const latestFile = files.find((file) => file.includes(`@${latestVersion}-`));
|
||||
|
||||
console.log("Latest file for browser", browser, ":", latestFile);
|
||||
return latestFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ZIP file of the project's source code, excluding specified development-related
|
||||
* files and directories such as `node_modules`, `dist`, `.git`, etc.
|
||||
* It uses the `7z` command-line tool to perform the archiving.
|
||||
* The output filename is fixed as `dist/betterseqtaplus@latest-sources.zip`.
|
||||
*
|
||||
* @returns {string} The filename of the created ZIP file (e.g., `dist/betterseqtaplus@latest-sources.zip`).
|
||||
*/
|
||||
function zipSources() {
|
||||
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
|
||||
|
||||
@@ -74,17 +140,31 @@ function zipSources() {
|
||||
"LICENSE",
|
||||
"package.json",
|
||||
]
|
||||
.map((pattern) => `-x!${pattern}`)
|
||||
.map((pattern) => `-x!${pattern}`) // Format for 7z exclude syntax
|
||||
.join(" ");
|
||||
|
||||
// Command to zip the current directory's contents into zipFileName, applying exclude patterns
|
||||
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
|
||||
|
||||
console.log("Zipping project sources with command:", zipCommand);
|
||||
execSync(zipCommand, { stdio: "inherit" });
|
||||
execSync(zipCommand, { stdio: "inherit" }); // Execute synchronously and show output
|
||||
|
||||
return zipFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orchestrates the extension publishing process for the specified browsers.
|
||||
* This function performs the following steps:
|
||||
* 1. Calls `getLatestFiles` to find the latest built ZIP for Chrome if "chrome" is in `browsers`.
|
||||
* 2. Calls `getLatestFiles` to find the latest built ZIP for Firefox if "firefox" is in `browsers`.
|
||||
* 3. Calls `zipSources` to create a source code ZIP if "firefox" is in `browsers` (required for Mozilla Add-ons).
|
||||
* 4. Validates that all required files were found and that at least one browser was specified. Exits if not.
|
||||
* 5. Constructs the `publish-extension` command-line string with the appropriate arguments
|
||||
* based on the found ZIP files for the specified browsers.
|
||||
* 6. Executes the constructed `publish-extension` command.
|
||||
*
|
||||
* @param {string[]} browsers An array of browser strings (e.g., ["chrome", "firefox"]) for which to publish the extension.
|
||||
*/
|
||||
function runPublishCommand(browsers) {
|
||||
const chromeZip = browsers.includes("chrome")
|
||||
? getLatestFiles("chrome")
|
||||
@@ -92,6 +172,7 @@ function runPublishCommand(browsers) {
|
||||
const firefoxZip = browsers.includes("firefox")
|
||||
? getLatestFiles("firefox")
|
||||
: null;
|
||||
// Sources are typically only needed for Firefox submissions
|
||||
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
|
||||
|
||||
console.log("Chrome zip:", chromeZip);
|
||||
@@ -100,15 +181,16 @@ function runPublishCommand(browsers) {
|
||||
|
||||
if (browsers.length === 0) {
|
||||
console.log("No browsers specified. Exiting.");
|
||||
process.exit(0);
|
||||
process.exit(0); // Exit gracefully if no action is needed
|
||||
}
|
||||
|
||||
// Check if required files are missing for the specified browsers
|
||||
if (
|
||||
(browsers.includes("chrome") && !chromeZip) ||
|
||||
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
|
||||
) {
|
||||
console.error("Could not find required zip files for specified browsers.");
|
||||
process.exit(1);
|
||||
process.exit(1); // Exit with error status
|
||||
}
|
||||
|
||||
let command = "publish-extension";
|
||||
@@ -120,12 +202,13 @@ function runPublishCommand(browsers) {
|
||||
}
|
||||
|
||||
console.log("Running command:", command);
|
||||
execSync(command, { stdio: "inherit" });
|
||||
execSync(command, { stdio: "inherit" }); // Execute and show output
|
||||
}
|
||||
|
||||
// Parse command-line arguments
|
||||
// Parse command-line arguments to determine which browsers to publish for
|
||||
const args = process.argv.slice(2);
|
||||
const browserIndex = args.indexOf("--b");
|
||||
const browserIndex = args.indexOf("--b"); // Find the --b flag
|
||||
// If --b is found, take all subsequent arguments as browser names
|
||||
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
|
||||
|
||||
runPublishCommand(browsers);
|
||||
|
||||
+42
-4
@@ -1,17 +1,55 @@
|
||||
import fs from "fs";
|
||||
|
||||
/**
|
||||
* Creates a Vite plugin designed to improve the reliability of Hot Module Replacement (HMR)
|
||||
* for global CSS files.
|
||||
*
|
||||
* When a JavaScript/TypeScript module that imports a CSS file is updated, Vite's HMR
|
||||
* might not always reliably update the styles injected by that global CSS. This plugin
|
||||
* attempts to mitigate this by listening for hot updates. If an updated module
|
||||
* has direct importers that are CSS files (e.g., a JS file imports a global CSS file),
|
||||
* this plugin will "touch" those CSS files by updating their access and modification
|
||||
* timestamps using `fs.utimesSync`. This action can help signal to Vite or the browser
|
||||
* that the CSS file has changed, potentially triggering a more reliable style reload.
|
||||
*
|
||||
* @returns {import('vite').Plugin} A Vite plugin object configured with `name` and `handleHotUpdate` hooks.
|
||||
*/
|
||||
export default function touchGlobalCSSPlugin() {
|
||||
return {
|
||||
/**
|
||||
* The unique name of this Vite plugin.
|
||||
* This name is used by Vite for identification purposes and will appear in logs.
|
||||
* @type {string}
|
||||
*/
|
||||
name: "touch-global-css",
|
||||
/**
|
||||
* A Vite hook that is called when a module is hot-updated.
|
||||
* This function inspects the importers of the updated module. If any of these
|
||||
* importers are CSS files, their filesystem timestamps are updated ("touched").
|
||||
*
|
||||
* @param {object} context The context object provided by Vite's `handleHotUpdate` hook.
|
||||
* @param {Array<import('vite').ModuleNode>} context.modules An array of `ModuleNode` instances that have been updated.
|
||||
* This plugin specifically accesses `modules[0]._clientModule.importers`
|
||||
* to find CSS files that import the updated module.
|
||||
*/
|
||||
handleHotUpdate({ modules }) {
|
||||
// log all of the staticImportedUrls
|
||||
const importers = modules[0]._clientModule.importers;
|
||||
// It's assumed `modules[0]` is the primary updated module of interest.
|
||||
// `_clientModule` and `importers` might be internal or less stable Vite APIs.
|
||||
const importers = modules[0]?._clientModule?.importers;
|
||||
if (importers) {
|
||||
importers.forEach((importer) => {
|
||||
if (importer.file.includes(".css")) {
|
||||
console.log("touching", importer.file);
|
||||
// Check if the importer is a CSS file
|
||||
if (importer.file && importer.file.includes(".css")) {
|
||||
console.log("[touch-global-css] touching", importer.file);
|
||||
try {
|
||||
// Update the access and modification times of the CSS file to the current time
|
||||
fs.utimesSync(importer.file, new Date(), new Date());
|
||||
} catch (err) {
|
||||
console.error(`[touch-global-css] Error touching file ${importer.file}:`, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+144
-8
@@ -1,6 +1,9 @@
|
||||
import type { ManifestV3Export } from "@crxjs/vite-plugin";
|
||||
import { type AnyCase, createEnum } from "./utils";
|
||||
import { type AnyCase, createEnum, ObjectValues } from "./utils";
|
||||
|
||||
/**
|
||||
* Enumerates supported JavaScript frameworks for project generation or configuration.
|
||||
*/
|
||||
export const FrameworkEnum = {
|
||||
React: "React",
|
||||
Vanilla: "Vanilla",
|
||||
@@ -10,6 +13,9 @@ export const FrameworkEnum = {
|
||||
Vue: "Vue",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Enumerates supported web browsers, typically for targeting builds or configurations.
|
||||
*/
|
||||
export const BrowserEnum = {
|
||||
Chrome: "Chrome",
|
||||
Brave: "Brave",
|
||||
@@ -19,15 +25,26 @@ export const BrowserEnum = {
|
||||
Safari: "Safari",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Enumerates supported programming languages for project setup.
|
||||
* This enum is not exported, suggesting it's for internal use within this module or related modules.
|
||||
*/
|
||||
const LanguageEnum = {
|
||||
TypeScript: "TypeScript",
|
||||
JavaScript: "JavaScript",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Enumerates supported styling options or libraries.
|
||||
*/
|
||||
export const StyleEnum = {
|
||||
Tailwind: "Tailwind",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Enumerates supported package managers.
|
||||
*/
|
||||
export const PackageManagerEnum = {
|
||||
Bun: "Bun",
|
||||
PnPm: "PnPm",
|
||||
@@ -35,7 +52,21 @@ export const PackageManagerEnum = {
|
||||
Yarn: "Yarn",
|
||||
} as const;
|
||||
|
||||
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
|
||||
/**
|
||||
* Defines the structure for browser-specific settings within a web extension manifest.
|
||||
* This is particularly used for Firefox (gecko) extensions to specify properties like
|
||||
* an extension ID, and minimum/maximum supported browser versions.
|
||||
* The structure is based on common manifest extensions for Firefox.
|
||||
* See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings
|
||||
* The link in the original code (// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts)
|
||||
* also points to type definitions that include this structure.
|
||||
*
|
||||
* @property {object} [browser_specific_settings] - Container for browser-specific settings.
|
||||
* @property {object} [browser_specific_settings.gecko] - Settings specific to Gecko-based browsers (e.g., Firefox).
|
||||
* @property {string} [browser_specific_settings.gecko.id] - The unique identifier for the extension in Firefox.
|
||||
* @property {string} [browser_specific_settings.gecko.strict_min_version] - The minimum version of Firefox the extension is compatible with.
|
||||
* @property {string} [browser_specific_settings.gecko.strict_max_version] - The maximum version of Firefox the extension is compatible with.
|
||||
*/
|
||||
export type BrowserSpecificSettings = {
|
||||
browser_specific_settings?: {
|
||||
gecko?: {
|
||||
@@ -46,59 +77,164 @@ export type BrowserSpecificSettings = {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the structure of a Chrome Manifest V3 file.
|
||||
* This type is an alias for `ManifestV3Export` from the `@crxjs/vite-plugin`,
|
||||
* which provides a comprehensive definition for Chrome extension manifests.
|
||||
*/
|
||||
export type Manifest = ManifestV3Export;
|
||||
|
||||
/** Alias for the `icons` property within a Chrome Manifest V3. */
|
||||
export type ManifestIcons = chrome.runtime.ManifestIcons;
|
||||
/** Alias for the `background` property within a Chrome Manifest V3. */
|
||||
export type ManifestBackground = chrome.runtime.ManifestV3["background"];
|
||||
/** Alias for the `content_scripts` property within a Chrome Manifest V3. */
|
||||
export type ManifestContentScripts =
|
||||
chrome.runtime.ManifestV3["content_scripts"];
|
||||
/** Alias for the `web_accessible_resources` property within a Chrome Manifest V3. */
|
||||
export type ManifestWebAccessibleResources =
|
||||
chrome.runtime.ManifestV3["web_accessible_resources"];
|
||||
/** Alias for the `commands` property within a Chrome Manifest V3. */
|
||||
export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
|
||||
/** Alias for the `action` property (or `browser_action`/`page_action`) within a Chrome Manifest V3. */
|
||||
export type ManifestAction = chrome.runtime.ManifestV3["action"];
|
||||
/** Alias for the `permissions` property within a Chrome Manifest V3. */
|
||||
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
|
||||
/** Alias for the `options_ui` property within a Chrome Manifest V3. */
|
||||
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
|
||||
/** Alias for the `chrome_url_overrides` property within a Chrome Manifest V3. */
|
||||
export type ManifestURLOverrides =
|
||||
chrome.runtime.ManifestV3["chrome_url_overrides"];
|
||||
|
||||
/**
|
||||
* Creates a type that accepts a string literal `T` in either its capitalized or lowercase form.
|
||||
* Useful for defining types that should be case-insensitive for specific known strings.
|
||||
* @template T - A string literal type.
|
||||
*/
|
||||
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>;
|
||||
|
||||
/**
|
||||
* Creates a record type where both keys and values are derived from a string literal `T`,
|
||||
* specifically using `BrowserName<T>` which allows for capitalized or lowercase forms.
|
||||
* This could be used to define an object where, for example, keys are 'Chrome' or 'chrome'
|
||||
* and values are also 'Chrome' or 'chrome'.
|
||||
* @template T - A string literal type, typically representing a browser name.
|
||||
*/
|
||||
export type BrowserEnumType<T extends string> = {
|
||||
[browser in BrowserName<T>]: BrowserName<T>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the target browser for a build, allowing for various casings of browser names
|
||||
* (e.g., "chrome", "Chrome", "CHROME") through the `AnyCase<Browser>` utility type.
|
||||
* `Browser` itself is a union of specific browser name strings (e.g., "Chrome" | "Firefox").
|
||||
*/
|
||||
export type BuildMode = AnyCase<Browser>;
|
||||
|
||||
/**
|
||||
* Defines an object structure that pairs a web extension `Manifest`
|
||||
* with its target `browser` (represented as `AnyCase<Browser>`).
|
||||
* This is commonly used in build processes to manage configurations for different browsers.
|
||||
*/
|
||||
export type BuildTarget = {
|
||||
manifest: Manifest;
|
||||
browser: AnyCase<Browser>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the configuration options for a build process.
|
||||
* @property {"build" | "serve"} [command] - The type of build command (e.g., 'build' for production, 'serve' for development).
|
||||
* @property {AnyCase<Browser> | string | undefined} [mode] - The target build mode, typically a browser name (allowing various casings)
|
||||
* or potentially other custom mode strings.
|
||||
*/
|
||||
export type BuildConfig = {
|
||||
command?: "build" | "serve";
|
||||
mode?: AnyCase<Browser> | string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines the structure for repository information, commonly found in `package.json`.
|
||||
* @property {string} type - The type of the repository (e.g., "git").
|
||||
* @property {string} [url] - The URL of the repository.
|
||||
* @property {Bugs} [bugs] - An object containing information about where to report bugs.
|
||||
*/
|
||||
export interface Repository {
|
||||
type: string;
|
||||
url?: string;
|
||||
bugs?: Bugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the structure for bug reporting information, often part of the `Repository` interface.
|
||||
* @property {string} [url] - The URL of the issue tracker.
|
||||
* @property {string} [email] - The email address for reporting bugs.
|
||||
*/
|
||||
export interface Bugs {
|
||||
url?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum];
|
||||
/**
|
||||
* A string literal union type representing supported browser names, derived from the values of `BrowserEnum`.
|
||||
* e.g., "Chrome" | "Firefox" | ...
|
||||
*/
|
||||
export type Browser = ObjectValues<typeof BrowserEnum>;
|
||||
|
||||
/**
|
||||
* A constant intended to provide access to browser names, potentially in various casings.
|
||||
* Its type `AnyCase<Browser>` suggests it can be used where case-insensitivity for browser names is needed.
|
||||
* The `createEnum(BrowserEnum)` call aims to produce a representation of browser names from `BrowserEnum`.
|
||||
* Note: `createEnum` from `lib/utils.ts` has a declared return type of `ObjectValues<T>` (a union of values),
|
||||
* while its implementation uses `Object.values()` which returns an array. This constant will hold the
|
||||
* runtime array value, but its JSDoc type refers to the more restrictive `AnyCase<Browser>` union type.
|
||||
*/
|
||||
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
|
||||
|
||||
export type PackageManager =
|
||||
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum];
|
||||
/**
|
||||
* A string literal union type representing supported package managers, derived from the values of `PackageManagerEnum`.
|
||||
* e.g., "Bun" | "PnPm" | "Npm" | "Yarn"
|
||||
*/
|
||||
export type PackageManager = ObjectValues<typeof PackageManagerEnum>;
|
||||
/**
|
||||
* A constant intended to provide access to package manager names, potentially in various casings.
|
||||
* Its type `AnyCase<PackageManager>` suggests it can be used where case-insensitivity for package manager names is needed.
|
||||
* Utilizes `createEnum(PackageManagerEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||
*/
|
||||
export const PackageManager: AnyCase<PackageManager> =
|
||||
createEnum(PackageManagerEnum);
|
||||
|
||||
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum];
|
||||
/**
|
||||
* A string literal union type representing supported JavaScript frameworks, derived from the values of `FrameworkEnum`.
|
||||
* e.g., "React" | "Vanilla" | ...
|
||||
*/
|
||||
export type Framework = ObjectValues<typeof FrameworkEnum>;
|
||||
/**
|
||||
* A constant intended to provide access to framework names, potentially in various casings.
|
||||
* Its type `AnyCase<Framework>` suggests it can be used where case-insensitivity for framework names is needed.
|
||||
* Utilizes `createEnum(FrameworkEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||
*/
|
||||
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
|
||||
|
||||
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum];
|
||||
/**
|
||||
* A string literal union type representing supported styling options, derived from the values of `StyleEnum`.
|
||||
* e.g., "Tailwind"
|
||||
*/
|
||||
export type Style = ObjectValues<typeof StyleEnum>;
|
||||
/**
|
||||
* A constant intended to provide access to style option names, potentially in various casings.
|
||||
* Its type `AnyCase<Style>` suggests it can be used where case-insensitivity for style names is needed.
|
||||
* Utilizes `createEnum(StyleEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||
*/
|
||||
export const Style: AnyCase<Style> = createEnum(StyleEnum);
|
||||
|
||||
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum];
|
||||
/**
|
||||
* A string literal union type representing supported programming languages, derived from the values of `LanguageEnum`.
|
||||
* e.g., "TypeScript" | "JavaScript"
|
||||
*/
|
||||
export type Language = ObjectValues<typeof LanguageEnum>;
|
||||
/**
|
||||
* A constant intended to provide access to programming language names, potentially in various casings.
|
||||
* Its type `AnyCase<Language>` suggests it can be used where case-insensitivity for language names is needed.
|
||||
* Utilizes `createEnum(LanguageEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||
*/
|
||||
export const Language: AnyCase<Language> = createEnum(LanguageEnum);
|
||||
|
||||
@@ -1,21 +1,84 @@
|
||||
/**
|
||||
* Extracts a union type of all values from the properties of an object type `T`.
|
||||
*
|
||||
* @template T - An object type (typically a Record or an enum-like object).
|
||||
* @example
|
||||
* type MyObject = { a: "foo", b: "bar", c: 123 };
|
||||
* type MyObjectValues = ObjectValues<MyObject>; // "foo" | "bar" | 123
|
||||
*/
|
||||
export type ObjectValues<T> = T[keyof T];
|
||||
|
||||
/**
|
||||
* Creates a union of an object's string values, often used to represent the set of possible values for an enum-like object.
|
||||
* Note: The implementation `Object.values(enumObj) as unknown as ObjectValues<T>` returns an array at runtime,
|
||||
* but the declared return type `ObjectValues<T>` is a union of the object's property values.
|
||||
* This type signature suggests it's intended to represent the set of possible string values from `enumObj`.
|
||||
*
|
||||
* @template T - An object type where keys are strings and values are strings (e.g., `const MyEnum = { VAL_A: "A", VAL_B: "B" }`).
|
||||
* @param {T} enumObj - The object from which to extract values.
|
||||
* @returns {ObjectValues<T>} A union type representing all possible string values of the `enumObj`.
|
||||
* For example, if `enumObj` is `{ A: "valA", B: "valB" }`, the return type is `"valA" | "valB"`.
|
||||
* (Runtime behavior of `Object.values()` is to return an array like `["valA", "valB"]`).
|
||||
*/
|
||||
export function createEnum<T extends Record<string, string>>(enumObj: T) {
|
||||
return Object.values(enumObj) as unknown as ObjectValues<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
|
||||
* of a given string literal type `T`.
|
||||
*
|
||||
* @template T - A string literal type.
|
||||
* @example
|
||||
* type MyString = "example";
|
||||
* type MyStringAnyCase = AnyCase<MyString>; // "EXAMPLE" | "example" | "Example" | "example" (Uncapitalize<"Example"> is "example")
|
||||
*/
|
||||
export type AnyCase<T extends string> =
|
||||
| Uppercase<T>
|
||||
| Lowercase<T>
|
||||
| Capitalize<T>
|
||||
| Uncapitalize<T>;
|
||||
|
||||
/**
|
||||
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
|
||||
* of the union of two given string literal types `T` and `K`.
|
||||
* This is useful for representing a combined set of related string constants where case variations are permitted for each.
|
||||
*
|
||||
* @template T - A string literal type.
|
||||
* @template K - Another string literal type.
|
||||
* @example
|
||||
* type Lang1 = "english";
|
||||
* type Lang2 = "french";
|
||||
* type CombinedLangsAnyCase = AnyCaseLanguage<Lang1, Lang2>;
|
||||
* // Result includes: "ENGLISH" | "english" | "English" | "FRENCH" | "french" | "French" etc.
|
||||
* // for all case variations of "english" and "french".
|
||||
*/
|
||||
export type AnyCaseLanguage<T extends string, K extends string> =
|
||||
| Uppercase<T | K>
|
||||
| Lowercase<T | K>
|
||||
| Capitalize<T | K>
|
||||
| Uncapitalize<T | K>;
|
||||
|
||||
/**
|
||||
* Extracts a new object type containing only the keys of `T` whose properties are optional
|
||||
* (i.e., their type includes `undefined`). The values associated with these keys retain their original types.
|
||||
*
|
||||
* @template T - An object type.
|
||||
* @example
|
||||
* type MyObject = {
|
||||
* requiredProp: string;
|
||||
* optionalProp?: number;
|
||||
* anotherOptional?: boolean | undefined;
|
||||
* nullProp: string | null;
|
||||
* };
|
||||
* type MyOptionalProps = OptionalKeys<MyObject>;
|
||||
* // MyOptionalProps would be conceptually equivalent to:
|
||||
* // {
|
||||
* // optionalProp?: number;
|
||||
* // anotherOptional?: boolean | undefined;
|
||||
* // }
|
||||
* // The actual resulting type is an object type with only these optional keys.
|
||||
*/
|
||||
export type OptionalKeys<T> = {
|
||||
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
|
||||
};
|
||||
|
||||
@@ -24,6 +24,19 @@ if (document.childNodes[1]) {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes BetterSEQTA+ on a SEQTA page.
|
||||
*
|
||||
* This function performs the following steps:
|
||||
* 1. Verifies that the current page is a SEQTA page.
|
||||
* 2. Injects CSS styles for document loading.
|
||||
* 3. Changes the page's favicon.
|
||||
* 4. Initializes the extension's settings state.
|
||||
* 5. Sets default storage if settings are not already defined.
|
||||
* 6. Calls the main function to apply core BetterSEQTA+ modifications.
|
||||
* 7. Initializes legacy and new plugins if the extension is enabled.
|
||||
* 8. Logs success or error messages during initialization.
|
||||
*/
|
||||
async function init() {
|
||||
const hasSEQTATitle = document.title.includes("SEQTA Learn");
|
||||
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import Parser from "rss-parser";
|
||||
|
||||
/**
|
||||
* Fetches news articles specifically for Australia from the NewsAPI.
|
||||
*
|
||||
* This function handles a specific case for fetching Australian news. It includes a
|
||||
* mechanism to retry the fetch operation by appending "%00" to the URL if a
|
||||
* rate limit error (`response.code == "rateLimited"`) is encountered. This is
|
||||
* likely a workaround for cache-busting or bypassing certain rate-limiting measures.
|
||||
*
|
||||
* @param {string} url The NewsAPI URL to fetch Australian news from.
|
||||
* @param {any} sendResponse A callback function (likely from a browser extension message listener)
|
||||
* to send the fetched news data back to the caller.
|
||||
* It's called with an object like `{ news: responseData }`.
|
||||
*/
|
||||
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
||||
fetch(url)
|
||||
.then((result) => result.json())
|
||||
@@ -12,6 +25,12 @@ const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A record mapping lowercase country codes (e.g., "usa", "canada") to an array
|
||||
* of RSS feed URLs for news sources in that country.
|
||||
*
|
||||
* @type {Record<string, string[]>}
|
||||
*/
|
||||
const rssFeedsByCountry: Record<string, string[]> = {
|
||||
usa: [
|
||||
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
||||
@@ -54,6 +73,25 @@ const rssFeedsByCountry: Record<string, string[]> = {
|
||||
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches news articles based on a specified source.
|
||||
*
|
||||
* The source can be:
|
||||
* 1. The string "australia": Fetches news from Australian sources via NewsAPI,
|
||||
* handled by the `fetchAustraliaNews` function.
|
||||
* 2. A lowercase country code (e.g., "usa", "canada"): Fetches news from a predefined
|
||||
* list of RSS feeds for that country, as specified in `rssFeedsByCountry`.
|
||||
* 3. A direct RSS feed URL (starting with "http"): Fetches news directly from this URL.
|
||||
*
|
||||
* The fetched articles are then sent back to the caller using the `sendResponse` callback.
|
||||
*
|
||||
* @param {string} source The news source identifier. This can be "australia", a
|
||||
* lowercase country code, or a direct RSS feed URL.
|
||||
* @param {any} sendResponse A callback function (typically from a browser extension
|
||||
* message listener, like `chrome.runtime.onMessage`)
|
||||
* used to send the fetched news data back to the caller.
|
||||
* It's called with an object like `{ news: { articles: [...] } }`.
|
||||
*/
|
||||
export async function fetchNews(source: string, sendResponse: any) {
|
||||
if (source === "australia") {
|
||||
const date = new Date();
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { type DBSchema, type IDBPDatabase, openDB } from "idb";
|
||||
|
||||
/**
|
||||
* Defines the schema for the IndexedDB database used for storing background image data.
|
||||
*
|
||||
* @interface BackgroundDB
|
||||
* @extends {DBSchema}
|
||||
* @property {object} backgrounds - The object store for background images.
|
||||
* @property {string} backgrounds.key - The type of the key for the object store (in this case, it's `id` as defined in `keyPath`).
|
||||
* @property {object} backgrounds.value - The structure of the objects stored.
|
||||
* @property {string} backgrounds.value.id - The unique identifier for the background image record.
|
||||
* @property {string} backgrounds.value.type - The MIME type of the image (e.g., "image/png", "image/jpeg").
|
||||
* @property {Blob} backgrounds.value.blob - The binary large object (Blob) containing the image data.
|
||||
*/
|
||||
interface BackgroundDB extends DBSchema {
|
||||
backgrounds: {
|
||||
key: string;
|
||||
key: string; // Corresponds to the 'id' property due to keyPath: "id"
|
||||
value: {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -13,6 +25,14 @@ interface BackgroundDB extends DBSchema {
|
||||
|
||||
let db: IDBPDatabase<BackgroundDB> | null = null;
|
||||
|
||||
/**
|
||||
* Initializes and opens an IndexedDB connection or returns an existing one.
|
||||
* If the database doesn't exist or needs an upgrade, the `upgrade` callback
|
||||
* creates the 'backgrounds' object store with 'id' as the keyPath.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<IDBPDatabase<BackgroundDB>>} A promise that resolves with the database instance.
|
||||
*/
|
||||
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
||||
if (db) return db;
|
||||
|
||||
@@ -25,6 +45,12 @@ export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all background image records from the 'backgrounds' object store in IndexedDB.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Array<{id: string, type: string, blob: Blob}>>} A promise that resolves with an array of all background image records.
|
||||
*/
|
||||
export async function readAllData(): Promise<
|
||||
Array<{ id: string; type: string; blob: Blob }>
|
||||
> {
|
||||
@@ -32,6 +58,16 @@ export async function readAllData(): Promise<
|
||||
return db.getAll("backgrounds");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes or updates a background image record in the 'backgrounds' object store.
|
||||
* If a record with the given `id` already exists, it will be updated. Otherwise, a new record is created.
|
||||
*
|
||||
* @async
|
||||
* @param {string} id - The unique identifier for the background image record.
|
||||
* @param {string} type - The MIME type of the image (e.g., "image/png").
|
||||
* @param {Blob} blob - The Blob object containing the image data.
|
||||
* @returns {Promise<void>} A promise that resolves when the data has been successfully written.
|
||||
*/
|
||||
export async function writeData(
|
||||
id: string,
|
||||
type: string,
|
||||
@@ -41,16 +77,37 @@ export async function writeData(
|
||||
await db.put("backgrounds", { id, type, blob });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a background image record from the 'backgrounds' object store by its ID.
|
||||
*
|
||||
* @async
|
||||
* @param {string} id - The unique identifier of the background image record to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the data has been successfully deleted.
|
||||
*/
|
||||
export async function deleteData(id: string): Promise<void> {
|
||||
const db = await openDatabase();
|
||||
await db.delete("backgrounds", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all records from the 'backgrounds' object store in IndexedDB.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<void>} A promise that resolves when all data has been successfully cleared.
|
||||
*/
|
||||
export async function clearAllData(): Promise<void> {
|
||||
const db = await openDatabase();
|
||||
await db.clear("backgrounds");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single background image record from the 'backgrounds' object store by its ID.
|
||||
*
|
||||
* @async
|
||||
* @param {string} id - The unique identifier of the background image record to retrieve.
|
||||
* @returns {Promise<{id: string, type: string, blob: Blob} | undefined>} A promise that resolves with the
|
||||
* background image record if found, or undefined otherwise.
|
||||
*/
|
||||
export async function getDataById(
|
||||
id: string,
|
||||
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
|
||||
@@ -58,6 +115,10 @@ export async function getDataById(
|
||||
return db.get("backgrounds", id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the active IndexedDB connection and nullifies the global `db` variable.
|
||||
* This is important to release resources and allow for proper database management.
|
||||
*/
|
||||
export function closeDatabase(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
@@ -65,12 +126,24 @@ export function closeDatabase(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if IndexedDB is supported
|
||||
/**
|
||||
* Checks if IndexedDB is supported by the current browser environment.
|
||||
*
|
||||
* @returns {boolean} True if IndexedDB is supported, false otherwise.
|
||||
*/
|
||||
export function isIndexedDBSupported(): boolean {
|
||||
return "indexedDB" in window;
|
||||
}
|
||||
|
||||
// Helper function to check if there's enough storage space
|
||||
/**
|
||||
* Estimates available storage space and checks if it's sufficient for the specified `requiredSpace`.
|
||||
* Uses the `navigator.storage.estimate()` API if available.
|
||||
* If the API is not available or cannot determine space, it defaults to assuming enough space is available.
|
||||
*
|
||||
* @async
|
||||
* @param {number} requiredSpace - The amount of storage space required, in bytes.
|
||||
* @returns {Promise<boolean>} A promise that resolves with true if enough space is estimated to be available, false otherwise.
|
||||
*/
|
||||
export async function hasEnoughStorageSpace(
|
||||
requiredSpace: number,
|
||||
): Promise<boolean> {
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
type BackgroundUpdateCallback = () => void;
|
||||
|
||||
/**
|
||||
* A singleton class used to notify listeners about generic background updates or events.
|
||||
* These updates typically signify that UI components or other parts of the application
|
||||
* might need to refresh or re-evaluate background-related data (e.g., after a new background
|
||||
* image is added, removed, or changed).
|
||||
*/
|
||||
class BackgroundUpdates {
|
||||
private static instance: BackgroundUpdates;
|
||||
private listeners: Set<BackgroundUpdateCallback> = new Set();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the BackgroundUpdates class.
|
||||
* @returns {BackgroundUpdates} The singleton instance.
|
||||
*/
|
||||
public static getInstance(): BackgroundUpdates {
|
||||
if (!BackgroundUpdates.instance) {
|
||||
BackgroundUpdates.instance = new BackgroundUpdates();
|
||||
@@ -13,14 +23,31 @@ class BackgroundUpdates {
|
||||
return BackgroundUpdates.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback function to be invoked when a background update is triggered.
|
||||
*
|
||||
* @param {BackgroundUpdateCallback} callback The function to call when a background update occurs.
|
||||
* This callback takes no arguments and returns void.
|
||||
*/
|
||||
public addListener(callback: BackgroundUpdateCallback): void {
|
||||
this.listeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a previously added callback function.
|
||||
* After calling this method, the provided callback will no longer be invoked when a background update is triggered.
|
||||
*
|
||||
* @param {BackgroundUpdateCallback} callback The callback function to remove from the listeners.
|
||||
*/
|
||||
public removeListener(callback: BackgroundUpdateCallback): void {
|
||||
this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes all registered listener callbacks, signifying that a background update has occurred.
|
||||
* This method should be called whenever a change to background data happens that requires
|
||||
* other parts of the application to be notified.
|
||||
*/
|
||||
public triggerUpdate(): void {
|
||||
this.listeners.forEach((callback) => callback());
|
||||
}
|
||||
|
||||
@@ -21,14 +21,30 @@ class SettingsPopup {
|
||||
return SettingsPopup.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback function to be invoked when the settings popup is closed.
|
||||
*
|
||||
* @param {SettingsPopupCallback} callback The function to call when the settings popup closes.
|
||||
* This callback takes no arguments and returns void.
|
||||
*/
|
||||
public addListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a previously added callback function.
|
||||
* After calling this method, the provided callback will no longer be invoked when the settings popup closes.
|
||||
*
|
||||
* @param {SettingsPopupCallback} callback The callback function to remove from the listeners.
|
||||
*/
|
||||
public removeListener(callback: SettingsPopupCallback): void {
|
||||
this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes all registered listener callbacks.
|
||||
* This method should be called when the settings popup is closed to notify all subscribed components or services.
|
||||
*/
|
||||
public triggerClose(): void {
|
||||
this.listeners.forEach((callback) => callback());
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
type ThemeUpdateCallback = () => void;
|
||||
|
||||
/**
|
||||
* A singleton class used to notify listeners about theme-related updates.
|
||||
* These updates can include events like theme changes, custom theme modifications,
|
||||
* or any other event that might require UI components to refresh their appearance
|
||||
* or re-apply theme styles.
|
||||
*/
|
||||
class ThemeUpdates {
|
||||
private static instance: ThemeUpdates;
|
||||
private listeners: Set<ThemeUpdateCallback> = new Set();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the ThemeUpdates class.
|
||||
* @returns {ThemeUpdates} The singleton instance.
|
||||
*/
|
||||
public static getInstance(): ThemeUpdates {
|
||||
if (!ThemeUpdates.instance) {
|
||||
ThemeUpdates.instance = new ThemeUpdates();
|
||||
@@ -13,14 +23,31 @@ class ThemeUpdates {
|
||||
return ThemeUpdates.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback function to be invoked when a theme update is triggered.
|
||||
*
|
||||
* @param {ThemeUpdateCallback} callback The function to call when a theme update occurs.
|
||||
* This callback takes no arguments and returns void.
|
||||
*/
|
||||
public addListener(callback: ThemeUpdateCallback): void {
|
||||
this.listeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a previously added callback function.
|
||||
* After calling this method, the provided callback will no longer be invoked when a theme update is triggered.
|
||||
*
|
||||
* @param {ThemeUpdateCallback} callback The callback function to remove from the listeners.
|
||||
*/
|
||||
public removeListener(callback: ThemeUpdateCallback): void {
|
||||
this.listeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes all registered listener callbacks, signifying that a theme-related update has occurred.
|
||||
* This method should be called whenever a change related to themes happens that requires
|
||||
* other parts of the application to be notified.
|
||||
*/
|
||||
public triggerUpdate(): void {
|
||||
this.listeners.forEach((callback) => callback());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
import type { LoadedCustomTheme } from "@/types/CustomThemes";
|
||||
|
||||
/**
|
||||
* Generates a random 9-character alphanumeric string to be used as a unique ID for images.
|
||||
* This helps in identifying and managing custom images within a theme.
|
||||
*
|
||||
* @returns {string} A randomly generated unique ID string.
|
||||
*/
|
||||
export function generateImageId(): string {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the upload of a new custom image from a file input event.
|
||||
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
|
||||
* generates a unique ID and a default variable name for it, and then adds this new image
|
||||
* to the `CustomImages` array within the provided `theme` object.
|
||||
*
|
||||
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
|
||||
* @param {LoadedCustomTheme} theme The current theme object to which the new image will be added.
|
||||
* @returns {Promise<LoadedCustomTheme> | LoadedCustomTheme} A Promise that resolves with the updated theme object
|
||||
* containing the new image if a file was processed.
|
||||
* Returns the original theme object synchronously if no file was selected.
|
||||
*/
|
||||
export function handleImageUpload(
|
||||
event: Event,
|
||||
theme: LoadedCustomTheme,
|
||||
@@ -34,6 +52,16 @@ export function handleImageUpload(
|
||||
return theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a custom image from the theme based on its ID.
|
||||
* It filters out the image with the specified `imageId` from the `CustomImages` array
|
||||
* in the `theme` object.
|
||||
*
|
||||
* @param {string} imageId The unique ID of the custom image to be removed.
|
||||
* @param {LoadedCustomTheme} theme The current theme object from which the image will be removed.
|
||||
* @returns {LoadedCustomTheme} A new theme object with the specified image removed from its `CustomImages` array.
|
||||
* This function is synchronous.
|
||||
*/
|
||||
export function handleRemoveImage(
|
||||
imageId: string,
|
||||
theme: LoadedCustomTheme,
|
||||
@@ -44,6 +72,17 @@ export function handleRemoveImage(
|
||||
} as LoadedCustomTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the CSS variable name associated with a specific custom image in the theme.
|
||||
* It finds the image by `imageId` in the `CustomImages` array of the `theme` object
|
||||
* and updates its `variableName` property.
|
||||
*
|
||||
* @param {string} imageId The unique ID of the custom image whose variable name is to be updated.
|
||||
* @param {string} variableName The new CSS variable name to assign to the image.
|
||||
* @param {LoadedCustomTheme} theme The current theme object containing the image to be updated.
|
||||
* @returns {LoadedCustomTheme} A new theme object with the updated image variable name.
|
||||
* This function is synchronous.
|
||||
*/
|
||||
export function handleImageVariableChange(
|
||||
imageId: string,
|
||||
variableName: string,
|
||||
@@ -57,6 +96,17 @@ export function handleImageVariableChange(
|
||||
} as LoadedCustomTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the upload of a cover image for the theme from a file input event.
|
||||
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
|
||||
* and then updates the `coverImage` property of the provided `theme` object with this new blob.
|
||||
*
|
||||
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
|
||||
* @param {LoadedCustomTheme} theme The current theme object whose cover image will be updated.
|
||||
* @returns {Promise<LoadedCustomTheme>} A Promise that resolves with the updated theme object
|
||||
* containing the new cover image if a file was processed.
|
||||
* Returns a Promise resolving with the original theme object if no file was selected.
|
||||
*/
|
||||
export function handleCoverImageUpload(
|
||||
event: Event,
|
||||
theme: LoadedCustomTheme,
|
||||
|
||||
@@ -48,6 +48,40 @@ function createSEQTAAPI(): SEQTAAPI {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reactive and persistent settings store for a given plugin.
|
||||
* This store is a Svelte-like store, providing reactivity, persistence
|
||||
* via `browser.storage.local`, and default value handling.
|
||||
*
|
||||
* @template T - Represents the structure of the plugin's settings, extending `PluginSettings`.
|
||||
* @param {Plugin<T, any>} plugin The plugin instance for which the settings store is being created.
|
||||
* `plugin.id` is used for namespacing the settings in storage,
|
||||
* and `plugin.settings` provides the definitions and default values for each setting.
|
||||
* @returns {SettingsAPI<T> & { loaded: Promise<void> }} An object that functions as a Svelte store,
|
||||
* enhanced with specific methods for settings management.
|
||||
* The object includes:
|
||||
* - Reactivity: Changes to settings can be subscribed to using Svelte's store subscription pattern
|
||||
* (though not explicitly a Svelte store, it behaves similarly for direct property access and updates).
|
||||
* The `onChange` method provides a more direct way to listen for specific key changes.
|
||||
* - Persistence: Settings are automatically loaded from `browser.storage.local` when the store is created
|
||||
* and saved back whenever a setting is changed via the proxy's setter.
|
||||
* - Default Values: Uses default values from the `plugin.settings` definition if no stored value exists for a setting.
|
||||
* - `loaded`: A Promise that resolves when the settings have been successfully loaded from storage,
|
||||
* allowing operations to be deferred until settings are ready.
|
||||
* - Direct property access for getting values (e.g., `settingsStore.mySettingKey`).
|
||||
* - Direct property assignment for setting values (e.g., `settingsStore.mySettingKey = newValue`), which also persists the change.
|
||||
* - `onChange(key, callback)`: Method to listen for changes to a specific setting. (Note: The prompt mentioned `listen`, this is `onChange`).
|
||||
* Returns an object with an `unregister` method.
|
||||
* - `offChange(key, callback)`: Method to stop listening for changes to a specific setting.
|
||||
* The following methods are not explicitly present on the returned proxy from `createSettingsAPI` but are typically
|
||||
* expected in a full "Svelte store" settings manager. The current implementation relies on direct property
|
||||
* manipulation for get/set, and re-initialization for reset-like behavior or would require external implementation
|
||||
* of reset logic if needed:
|
||||
* - `get(key)`: (Achieved by direct property access: `settingsStore.key`)
|
||||
* - `set(key, value)`: (Achieved by direct property assignment: `settingsStore.key = value`)
|
||||
* - `reset(key)`: (Would require manual re-application of `plugin.settings[key].default` and then setting it)
|
||||
* - `resetAll()`: (Would require iterating through all `plugin.settings` and applying defaults, then setting them)
|
||||
*/
|
||||
function createSettingsAPI<T extends PluginSettings>(
|
||||
plugin: Plugin<T>,
|
||||
): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||
@@ -293,6 +327,32 @@ function createEventsAPI(pluginId: string): EventsAPI {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a tailored API object for a specific plugin.
|
||||
* This API object provides the plugin with various functionalities such as
|
||||
* managing settings, accessing namespaced storage, interacting with SEQTA-specific features,
|
||||
* and handling plugin-specific events.
|
||||
*
|
||||
* @template T - The type of the plugin's settings, extending `PluginSettings`.
|
||||
* @template S - The type of the data the plugin will store in its namespaced storage.
|
||||
* @param {Plugin<T, S>} plugin The plugin instance for which the API is being created.
|
||||
* The plugin's `id` and `name` are used internally by the API
|
||||
* for namespacing and identification but are accessed from the `plugin` object directly.
|
||||
* @returns {PluginAPI<T, S>} An API object containing the following key properties:
|
||||
* - `seqta`: An API for interacting with SEQTA-specific functionalities, created by `createSEQTAAPI()`.
|
||||
* This includes methods like `onMount` for DOM element appearance, `getFiber` for React component inspection,
|
||||
* `getCurrentPage` for getting the current SEQTA page, and `onPageChange` for listening to page navigations.
|
||||
* - `settings`: An API for managing plugin-specific settings, created by `createSettingsAPI(plugin)`.
|
||||
* It allows getting, setting, and listening to changes in the plugin's settings,
|
||||
* which are stored persistently and namespaced to the plugin. Includes a `loaded` promise.
|
||||
* - `storage`: An API for providing namespaced storage for the plugin, created by `createStorageAPI<S>(plugin.id)`.
|
||||
* It allows the plugin to store and retrieve arbitrary data, namespaced to prevent conflicts
|
||||
* with other plugins or parts of the extension. Includes a `loaded` promise and `onChange` listeners.
|
||||
* - `events`: An API for allowing the plugin to dispatch and listen for custom events within its own scope,
|
||||
* created by `createEventsAPI(plugin.id)`. It provides `on(event, callback)` to listen for
|
||||
* plugin-specific events and `emit(event, ...args)` to dispatch them. These events are namespaced
|
||||
* to the plugin.
|
||||
*/
|
||||
export function createPluginAPI<T extends PluginSettings, S = any>(
|
||||
plugin: Plugin<T, S>,
|
||||
): PluginAPI<T, S> {
|
||||
|
||||
+151
-3
@@ -22,6 +22,12 @@ interface StorageChange<T = any> {
|
||||
newValue?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton class responsible for the entire lifecycle of plugins.
|
||||
* This includes registration, starting, stopping, event dispatching,
|
||||
* managing plugin-specific styles, and listening for plugin setting changes
|
||||
* to automatically start or stop plugins.
|
||||
*/
|
||||
export class PluginManager {
|
||||
private static instance: PluginManager;
|
||||
private plugins: Map<string, Plugin<any, any>> = new Map();
|
||||
@@ -31,10 +37,18 @@ export class PluginManager {
|
||||
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
private styleElements: Map<string, HTMLStyleElement> = new Map();
|
||||
|
||||
/**
|
||||
* Private constructor to enforce singleton pattern.
|
||||
* Initializes the listener for plugin state changes from storage.
|
||||
*/
|
||||
private constructor() {
|
||||
this.setupPluginStateListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance of the PluginManager.
|
||||
* @returns {PluginManager} The singleton instance.
|
||||
*/
|
||||
public static getInstance(): PluginManager {
|
||||
if (!PluginManager.instance) {
|
||||
PluginManager.instance = new PluginManager();
|
||||
@@ -42,6 +56,15 @@ export class PluginManager {
|
||||
return PluginManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event to a specific plugin.
|
||||
* If the plugin is currently running, the event is dispatched immediately via a DOM CustomEvent.
|
||||
* If the plugin is not running, the event is added to a backlog to be processed when the plugin starts.
|
||||
*
|
||||
* @param {string} pluginId The ID of the target plugin.
|
||||
* @param {string} event The name of the event to dispatch (e.g., "update").
|
||||
* @param {any} [args] Optional arguments to pass with the event.
|
||||
*/
|
||||
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||
|
||||
@@ -57,6 +80,14 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes and dispatches any events that were backlogged for a plugin.
|
||||
* This is typically called after a plugin has successfully started.
|
||||
*
|
||||
* @private
|
||||
* @param {string} pluginId The ID of the plugin for which to process backlogged events.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async processBackloggedEvents(pluginId: string) {
|
||||
for (const [key, argsList] of this.eventBacklog.entries()) {
|
||||
const [eventPluginId, event] = key.split(":");
|
||||
@@ -69,6 +100,15 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a plugin with the manager.
|
||||
* Plugins must have a unique ID.
|
||||
*
|
||||
* @template T - The type of settings the plugin uses.
|
||||
* @template S - The type of storage the plugin uses.
|
||||
* @param {Plugin<T, S>} plugin The plugin object to register.
|
||||
* @throws {Error} If a plugin with the same ID is already registered.
|
||||
*/
|
||||
public registerPlugin<T extends PluginSettings, S>(
|
||||
plugin: Plugin<T, S>,
|
||||
): void {
|
||||
@@ -78,6 +118,22 @@ export class PluginManager {
|
||||
this.plugins.set(plugin.id, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a specific plugin by its ID.
|
||||
* This involves:
|
||||
* - Checking if the plugin exists and isn't already running.
|
||||
* - Creating and providing the plugin API (settings, storage, etc.).
|
||||
* - Checking if the plugin is enabled (if `disableToggle` is true), respecting its `defaultEnabled` status.
|
||||
* - Injecting any CSS styles defined by the plugin into the document head.
|
||||
* - Waiting for the plugin's settings and storage to be loaded.
|
||||
* - Executing the plugin's `run` method.
|
||||
* - Storing any cleanup function returned by `run` for later use in `stopPlugin`.
|
||||
* - Marking the plugin as running and processing any backlogged events for it.
|
||||
*
|
||||
* @param {string} pluginId The ID of the plugin to start.
|
||||
* @returns {Promise<void>} A promise that resolves when the plugin has started or is determined not to start (e.g., disabled).
|
||||
* @throws {Error} If the plugin is not found, or if an error occurs during plugin initialization or execution.
|
||||
*/
|
||||
public async startPlugin(pluginId: string): Promise<void> {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin) {
|
||||
@@ -139,17 +195,36 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to start all registered plugins.
|
||||
* Errors during the start of individual plugins are caught and logged,
|
||||
* allowing other plugins to attempt to start.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when all plugins have attempted to start.
|
||||
* It uses `Promise.allSettled` to wait for all start operations.
|
||||
*/
|
||||
public async startAllPlugins(): Promise<void> {
|
||||
const startPromises = Array.from(this.plugins.keys()).map((id) =>
|
||||
this.startPlugin(id).catch((error) => {
|
||||
console.error(`Failed to start plugin "${id}":`, error);
|
||||
return Promise.reject(error);
|
||||
return Promise.reject(error); // Still reject to indicate failure for this specific plugin if needed by caller
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.allSettled(startPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a specific plugin by its ID.
|
||||
* This involves:
|
||||
* - Removing any CSS styles injected by the plugin.
|
||||
* - Executing the cleanup function that was returned by the plugin's `run` method (if any).
|
||||
* - Marking the plugin as not running.
|
||||
* - Emitting a "plugin.stopped" event with the pluginId.
|
||||
*
|
||||
* @param {string} pluginId The ID of the plugin to stop.
|
||||
* @returns {Promise<void>} A promise that resolves when the plugin has been stopped.
|
||||
*/
|
||||
public async stopPlugin(pluginId: string): Promise<void> {
|
||||
// Remove plugin styles
|
||||
const styleElement = this.styleElements.get(pluginId);
|
||||
@@ -168,18 +243,47 @@ export class PluginManager {
|
||||
this.emit("plugin.stopped", pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all currently running plugins.
|
||||
* Iterates through all registered plugins and calls `stopPlugin` for each.
|
||||
*/
|
||||
public stopAllPlugins(): void {
|
||||
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a registered plugin by its ID.
|
||||
*
|
||||
* @param {string} pluginId The ID of the plugin to retrieve.
|
||||
* @returns {Plugin | undefined} The plugin object if found, otherwise undefined.
|
||||
*/
|
||||
public getPlugin(pluginId: string): Plugin | undefined {
|
||||
return this.plugins.get(pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of all registered plugin objects.
|
||||
*
|
||||
* @returns {Plugin[]} An array containing all registered plugin objects.
|
||||
*/
|
||||
public getAllPlugins(): Plugin[] {
|
||||
return Array.from(this.plugins.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a structured list of settings for all registered plugins.
|
||||
* This is primarily used for building user interfaces for plugin configuration.
|
||||
* It processes each plugin's defined settings, adding IDs, titles, descriptions,
|
||||
* and default enabled states. For plugins with `disableToggle`, an "enabled"
|
||||
* boolean setting is automatically included.
|
||||
*
|
||||
* @returns {Array<object>} An array of objects, where each object represents a plugin
|
||||
* and contains its ID, name, description, beta status,
|
||||
* and a processed `settings` object. The `settings` object
|
||||
* maps setting keys to their detailed configuration (type, title, etc.).
|
||||
* The specific structure of each setting object within `settings`
|
||||
* depends on its type (boolean, string, number, select, button, hotkey).
|
||||
*/
|
||||
public getAllPluginSettings(): Array<{
|
||||
pluginId: string;
|
||||
name: string;
|
||||
@@ -199,6 +303,8 @@ export class PluginManager {
|
||||
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string })
|
||||
| (Omit<ComponentSetting, "type"> & { type: "component"; id: string; component: any });
|
||||
};
|
||||
// Actual type is more complex, see original code, but this gives the gist for the JSDoc.
|
||||
// Array<{ pluginId: string; name: string; description: string; beta?: boolean; settings: Record<string, ProcessedSetting>; disableToggle?: boolean; }>
|
||||
}> {
|
||||
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||
const settingsEntries = Object.entries(plugin.settings).map(
|
||||
@@ -247,10 +353,24 @@ export class PluginManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific plugin is currently running.
|
||||
*
|
||||
* @param {string} pluginId The ID of the plugin to check.
|
||||
* @returns {boolean} True if the plugin is running, false otherwise.
|
||||
*/
|
||||
public isPluginRunning(pluginId: string): boolean {
|
||||
return this.runningPlugins.get(pluginId) || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to all registered listeners for that event.
|
||||
* This is an internal event bus for the PluginManager itself.
|
||||
*
|
||||
* @private
|
||||
* @param {string} event The name of the event to emit.
|
||||
* @param {any[]} args Arguments to pass to the event listeners.
|
||||
*/
|
||||
private emit(event: string, ...args: any[]): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
@@ -258,6 +378,12 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event listener for PluginManager's internal events.
|
||||
*
|
||||
* @param {string} event The name of the event to listen for (e.g., "plugin.stopped").
|
||||
* @param {(...args: any[]) => void} callback The function to call when the event is emitted.
|
||||
*/
|
||||
public on(event: string, callback: (...args: any[]) => void): void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
@@ -265,6 +391,12 @@ export class PluginManager {
|
||||
this.listeners.get(event)!.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters an event listener for PluginManager's internal events.
|
||||
*
|
||||
* @param {string} event The name of the event.
|
||||
* @param {(...args: any[]) => void} callback The callback function to remove.
|
||||
*/
|
||||
public off(event: string, callback: (...args: any[]) => void): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
@@ -272,7 +404,16 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Add handler for plugin enable/disable state changes
|
||||
/**
|
||||
* Handles the change in a plugin's enabled state.
|
||||
* Starts or stops the plugin based on the new `enabled` value.
|
||||
* This is typically called by `setupPluginStateListener` when a relevant storage change is detected.
|
||||
*
|
||||
* @private
|
||||
* @param {string} pluginId The ID of the plugin whose state has changed.
|
||||
* @param {boolean} enabled The new enabled state of the plugin.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async handlePluginStateChange(
|
||||
pluginId: string,
|
||||
enabled: boolean,
|
||||
@@ -284,7 +425,14 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Add listener for plugin settings changes
|
||||
/**
|
||||
* Sets up a listener for browser storage changes.
|
||||
* This listener monitors changes to plugin settings (specifically the `enabled` property
|
||||
* for plugins with `disableToggle: true`) and calls `handlePluginStateChange`
|
||||
* to automatically start or stop plugins accordingly.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private setupPluginStateListener(): void {
|
||||
browser.storage.onChanged.addListener(
|
||||
(changes: { [key: string]: StorageChange }, area: string) => {
|
||||
|
||||
@@ -5,9 +5,20 @@ import type {
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
HotkeySetting,
|
||||
PluginSettings,
|
||||
ComponentSetting,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Creates a complete `NumberSetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "number".
|
||||
* It's used for defining a numeric setting for a plugin.
|
||||
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||
*
|
||||
* @param {Omit<NumberSetting, "type">} options The configuration options for the number setting,
|
||||
* excluding the `type` property (e.g., `title`, `default`, `min`, `max`).
|
||||
* @returns {NumberSetting} A complete number setting object with `type: "number"`.
|
||||
*/
|
||||
export function numberSetting(
|
||||
options: Omit<NumberSetting, "type">,
|
||||
): NumberSetting {
|
||||
@@ -17,6 +28,16 @@ export function numberSetting(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete `BooleanSetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "boolean".
|
||||
* It's used for defining a boolean (true/false) setting for a plugin.
|
||||
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||
*
|
||||
* @param {Omit<BooleanSetting, "type">} options The configuration options for the boolean setting,
|
||||
* excluding the `type` property (e.g., `title`, `default`).
|
||||
* @returns {BooleanSetting} A complete boolean setting object with `type: "boolean"`.
|
||||
*/
|
||||
export function booleanSetting(
|
||||
options: Omit<BooleanSetting, "type">,
|
||||
): BooleanSetting {
|
||||
@@ -26,6 +47,16 @@ export function booleanSetting(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete `StringSetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "string".
|
||||
* It's used for defining a text-based setting for a plugin.
|
||||
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||
*
|
||||
* @param {Omit<StringSetting, "type">} options The configuration options for the string setting,
|
||||
* excluding the `type` property (e.g., `title`, `default`, `placeholder`).
|
||||
* @returns {StringSetting} A complete string setting object with `type: "string"`.
|
||||
*/
|
||||
export function stringSetting(
|
||||
options: Omit<StringSetting, "type">,
|
||||
): StringSetting {
|
||||
@@ -35,15 +66,36 @@ export function stringSetting(
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSetting<T extends string>(
|
||||
options: Omit<SelectSetting<T>, "type">,
|
||||
): SelectSetting<T> {
|
||||
/**
|
||||
* Creates a complete `SelectSetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "select".
|
||||
* It's used for defining a setting where the user can choose from a predefined list of options.
|
||||
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||
*
|
||||
* @template TValue - The type of the value for each option in the select list (extends string).
|
||||
* @param {Omit<SelectSetting<TValue>, "type">} options The configuration options for the select setting,
|
||||
* excluding the `type` property (e.g., `title`, `default`, `options` array).
|
||||
* @returns {SelectSetting<TValue>} A complete select setting object with `type: "select"`.
|
||||
*/
|
||||
export function selectSetting<TValue extends string>(
|
||||
options: Omit<SelectSetting<TValue>, "type">,
|
||||
): SelectSetting<TValue> {
|
||||
return {
|
||||
type: "select",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete `ButtonSetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "button".
|
||||
* It's used for defining a button in the plugin's settings UI, which can trigger an action.
|
||||
* This function itself does not handle storage or persistence; it defines the button's structure and action.
|
||||
*
|
||||
* @param {Omit<ButtonSetting, "type">} options The configuration options for the button setting,
|
||||
* excluding the `type` property (e.g., `title`, `label`, `trigger` function).
|
||||
* @returns {ButtonSetting} A complete button setting object with `type: "button"`.
|
||||
*/
|
||||
export function buttonSetting(
|
||||
options: Omit<ButtonSetting, "type">,
|
||||
): ButtonSetting {
|
||||
@@ -53,6 +105,17 @@ export function buttonSetting(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete `HotkeySetting` object from its options.
|
||||
* This helper function ensures the `type` property is correctly set to "hotkey".
|
||||
* It's used for defining a setting where the user can configure a keyboard shortcut.
|
||||
* This function itself does not handle storage or persistence; it defines the hotkey setting's structure.
|
||||
*
|
||||
* @param {Omit<HotkeySetting, "type">} options The configuration options for the hotkey setting,
|
||||
* excluding the `type` property (e.g., `title`, `default` hotkey string).
|
||||
* @returns {HotkeySetting} A complete hotkey setting object with `type: "hotkey"`.
|
||||
*/
|
||||
|
||||
export function componentSetting(
|
||||
options: Omit<ComponentSetting, "type">,
|
||||
): ComponentSetting {
|
||||
@@ -71,10 +134,42 @@ export function hotkeySetting(
|
||||
};
|
||||
}
|
||||
|
||||
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
||||
/**
|
||||
* Defines a collection of settings for a plugin.
|
||||
* This function currently acts as an identity function, returning the settings object as is.
|
||||
* Its primary purpose is to provide type inference and a structured way to define
|
||||
* the entire settings configuration for a plugin, ensuring it conforms to the expected type.
|
||||
* This function itself does not handle storage or persistence; it's for structural definition.
|
||||
*
|
||||
* @template TSettings - A record type where keys are setting names and values are setting definition objects
|
||||
* (e.g., `NumberSetting`, `BooleanSetting`).
|
||||
* @param {TSettings} settings The complete settings configuration object for the plugin.
|
||||
* @returns {TSettings} The same settings configuration object, primarily for type checking/inference.
|
||||
*/
|
||||
export function defineSettings<TSettings extends Record<string, any>>(settings: TSettings): TSettings {
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* A property decorator for declaratively defining a plugin setting on a class property.
|
||||
* When a class property is decorated with `@Setting({...})`, this decorator adds the
|
||||
* provided setting definition (`settingDef`) to a static `settings` object on the
|
||||
* class's prototype. This allows settings to be defined alongside their related class logic.
|
||||
* This decorator itself does not handle runtime storage or persistence of setting *values*;
|
||||
* it is for defining the *structure* and *metadata* of a setting.
|
||||
*
|
||||
* Example:
|
||||
* ```typescript
|
||||
* class MyPlugin extends BasePlugin {
|
||||
* @Setting(numberSetting({ title: "My Number", default: 10 }))
|
||||
* myNumberSetting: number; // Type annotation for the setting's value
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param {any} settingDef The setting definition object, typically created by one of the
|
||||
* helper functions like `numberSetting(...)`, `booleanSetting(...)`, etc.
|
||||
* @returns {PropertyDecorator} A property decorator function.
|
||||
*/
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Pauses execution for a specified number of milliseconds.
|
||||
*
|
||||
* This function returns a Promise that resolves after the given delay,
|
||||
* allowing it to be used with `async/await` to pause asynchronous operations.
|
||||
*
|
||||
* @param {number} ms The number of milliseconds to delay.
|
||||
* @returns {Promise<void>} A Promise that resolves after the specified delay.
|
||||
*/
|
||||
export function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -1,12 +1,51 @@
|
||||
// Simple mutex implementation
|
||||
/**
|
||||
* @callback UnlockFunction
|
||||
* @description A function that must be called to release the mutex.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
/**
|
||||
* A simple mutex implementation for managing asynchronous operations.
|
||||
* It ensures that only one operation can hold the lock at a time.
|
||||
* Operations queue up and are granted access sequentially.
|
||||
*/
|
||||
export class Mutex {
|
||||
private mutex = Promise.resolve();
|
||||
|
||||
lock(): PromiseLike<() => void> {
|
||||
let begin: (unlock: () => void) => void;
|
||||
/**
|
||||
* Acquires the mutex.
|
||||
*
|
||||
* This method returns a Promise that resolves with an {@link UnlockFunction}.
|
||||
* The calling code *must* call this {@link UnlockFunction} to release the mutex
|
||||
* once the critical section of code has completed.
|
||||
*
|
||||
* If the mutex is already locked, this method will wait until it is released
|
||||
* before resolving the Promise.
|
||||
*
|
||||
* @returns {Promise<UnlockFunction>} A Promise that resolves with the function to call to release the lock.
|
||||
*/
|
||||
acquire(): Promise<() => void> {
|
||||
let begin: (unlock: () => void) => void = () => {}; // Initialize with a no-op
|
||||
|
||||
this.mutex = this.mutex.then(() => new Promise(begin));
|
||||
const newPromise = new Promise<void>((resolve) => {
|
||||
begin = resolve;
|
||||
});
|
||||
|
||||
return new Promise((res) => (begin = res));
|
||||
const chainedPromise = this.mutex.then(() => {
|
||||
return new Promise<() => void>((resolveOuter) => {
|
||||
// The 'begin' function, when called, will resolve the newPromise,
|
||||
// effectively passing control to the next then() in the chain.
|
||||
// We pass 'begin' itself as the unlock function.
|
||||
// So, when the user calls unlock (which is 'begin'), newPromise resolves.
|
||||
resolveOuter(begin);
|
||||
});
|
||||
});
|
||||
|
||||
this.mutex = newPromise;
|
||||
|
||||
return chainedPromise;
|
||||
}
|
||||
|
||||
// Note: There isn't a separate `release()` method in this pattern.
|
||||
// The lock is released by calling the function returned by `acquire()`.
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
/**
|
||||
* Converts an HTML string into a DOM element, with sanitization and optional styling.
|
||||
*
|
||||
* This function first sanitizes the input HTML string using DOMPurify to prevent XSS attacks.
|
||||
* The sanitization process allows 'onclick' attributes and specific URI schemes.
|
||||
* Then, it parses the sanitized string into an HTML document and returns its body.
|
||||
* Optionally, it can apply predefined CSS styles to the body element.
|
||||
*
|
||||
* @param {string} str The HTML string to convert.
|
||||
* @param {boolean} [styles=false] Whether to apply predefined styles to the document body.
|
||||
* @returns {HTMLElement} The body element of the parsed and sanitized HTML document.
|
||||
*/
|
||||
export default function stringToHTML(str: string, styles = false) {
|
||||
const parser = new DOMParser();
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { eventManager } from "@/seqta/utils/listeners/EventManager";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
|
||||
/**
|
||||
* Asynchronously waits for an element to be present in the DOM.
|
||||
*
|
||||
* This function can use either a polling mechanism (via `setTimeout`) or
|
||||
* a `MutationObserver` (via `eventManager.register`) to detect the element.
|
||||
* By default, it uses the `eventManager` which is more efficient.
|
||||
*
|
||||
* @param {string} selector The CSS selector for the target element.
|
||||
* @param {boolean} [usePolling=false] If true, forces the use of `setTimeout` for polling.
|
||||
* @param {number} [interval=100] The polling interval in milliseconds (only applicable if `usePolling` is true).
|
||||
* @param {number} [maxIterations] Optional. The maximum number of polling attempts before rejecting (only applicable if `usePolling` is true).
|
||||
* @returns {Promise<Element>} A Promise that resolves with the found DOM Element.
|
||||
* If `usePolling` is true and `maxIterations` is reached, the Promise rejects with an Error.
|
||||
*/
|
||||
export async function waitForElm(
|
||||
selector: string,
|
||||
usePolling: boolean = false,
|
||||
|
||||
Reference in New Issue
Block a user