From c927ce23e47fbae5e852a67d4be78059577f05fe Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 10:25:57 -0500 Subject: [PATCH] Add separate entry points --- build.js | 309 ++++++++++++++++++++++-------- dist/birb.js | 306 +++++++++++++---------------- dist/extension.zip | Bin 149143 -> 149002 bytes dist/extension/birb.js | 323 +++++++++++++++---------------- dist/obsidian/main.js | 361 ++++++++++++++++++----------------- dist/userscript/birb.user.js | 314 ++++++++++++++---------------- dist/vencord/birb.export.js | 306 +++++++++++++---------------- src/context.js | 68 +++---- src/platforms/browser.js | 36 ---- src/platforms/extension.js | 4 + src/platforms/obsidian.js | 4 + src/platforms/userscript.js | 4 + src/platforms/vencord.js | 4 + src/platforms/web.js | 4 + 14 files changed, 1032 insertions(+), 1011 deletions(-) delete mode 100644 src/platforms/browser.js create mode 100644 src/platforms/extension.js create mode 100644 src/platforms/obsidian.js create mode 100644 src/platforms/userscript.js create mode 100644 src/platforms/vencord.js create mode 100644 src/platforms/web.js diff --git a/build.js b/build.js index 87307da..ff3324d 100644 --- a/build.js +++ b/build.js @@ -14,7 +14,7 @@ const DIST_DIR = "./dist"; const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; -const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; +const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; const VENCORD_WRAPPER = "./platform-specific/vencord/wrapper.js"; @@ -24,8 +24,13 @@ const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; const VENCORD_DIR = DIST_DIR + "/vencord"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; -// const APPLICATION_ENTRY = SRC_DIR + "/application.js"; -const APPLICATION_ENTRY = SRC_DIR + "/platforms/browser.js"; + +const WEB_ENTRY = SRC_DIR + "/platforms/web.js"; +const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript.js"; +const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension.js"; +const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian.js"; +const VENCORD_ENTRY = SRC_DIR + "/platforms/vencord.js"; + const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const BIRB_OUTPUT = DIST_DIR + "/birb.js"; @@ -78,128 +83,268 @@ const version = `${versionDate}`; // Disable build number for now buildCache.version = version; writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8'); +/** + * @param {string} entryPoint + * @param {boolean} [embedFont] + * @returns {Promise} + */ +async function generateCode(entryPoint, embedFont = false) { + // Bundle with rollup + const bundle = await rollup({ + input: entryPoint, + }); + + await bundle.write({ + file: BUNDLED_OUTPUT, + format: 'iife', + }); + + await bundle.close(); + + let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); + + // Delete bundled file + unlinkSync(BUNDLED_OUTPUT); + + // Replace version placeholder + birbJs = birbJs.replaceAll(VERSION_KEY, version); + + // Compile and insert sprite sheets + for (const spriteSheet of spriteSheets) { + const dataUri = readFileSync(spriteSheet.path, 'base64'); + birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); + } + + // Insert stylesheet + const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); + birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent); + + if (embedFont) { + // Encode font to data URI + const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); + const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; + birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, monocraftDataUri); + } else { + birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); + } + return birbJs; +} + // ============================================= // Build JavaScript function // ============================================= -// Bundle with rollup -const bundle = await rollup({ - input: APPLICATION_ENTRY, -}); - -await bundle.write({ - file: BUNDLED_OUTPUT, - format: 'iife', -}); - -await bundle.close(); - -let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); - -// Delete bundled file -unlinkSync(BUNDLED_OUTPUT); - -// Replace version placeholder -birbJs = birbJs.replaceAll(VERSION_KEY, version); - -// Compile and insert sprite sheets -for (const spriteSheet of spriteSheets) { - const dataUri = readFileSync(spriteSheet.path, 'base64'); - birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); +async function buildWeb() { + const birbJs = await generateCode(WEB_ENTRY); + writeFileSync(BIRB_OUTPUT, birbJs); } -// Insert stylesheet -const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); -birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); +// Bundle with rollup +// const bundle = await rollup({ +// input: APPLICATION_ENTRY, +// }); + +// await bundle.write({ +// file: BUNDLED_OUTPUT, +// format: 'iife', +// }); + +// await bundle.close(); + +// let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); + +// // Delete bundled file +// unlinkSync(BUNDLED_OUTPUT); + +// // Replace version placeholder +// birbJs = birbJs.replaceAll(VERSION_KEY, version); + +// // Compile and insert sprite sheets +// for (const spriteSheet of spriteSheets) { +// const dataUri = readFileSync(spriteSheet.path, 'base64'); +// birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); +// } + +// // Insert stylesheet +// const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); +// birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); -// Write bundled JavaScript function -writeFileSync(BIRB_OUTPUT, birbJs); +// // Write bundled JavaScript function +// writeFileSync(BIRB_OUTPUT, birbJs); // ============================================= // Build userscript // ============================================= // Get userscript header -const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); +// const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); -mkdirSync(USERSCRIPT_DIR, { recursive: true }); -const userScript = userScriptHeader + "\n" + birbJs; -writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); +// mkdirSync(USERSCRIPT_DIR, { recursive: true }); +// const userScript = userScriptHeader + "\n" + birbJs; +// writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); + +async function buildUserscript() { + const birbJs = await generateCode(USERSCRIPT_ENTRY); + // Get userscript header + const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); + + mkdirSync(USERSCRIPT_DIR, { recursive: true }); + const userScript = userScriptHeader + "\n" + birbJs; + writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); +} // ============================================= // Build browser extension // ============================================= -mkdirSync(EXTENSION_DIR, { recursive: true }); +// mkdirSync(EXTENSION_DIR, { recursive: true }); -// Copy birb.js -writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); +// // Copy birb.js +// writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); -// Copy manifest.json -let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); -browserManifest = browserManifest.replace(VERSION_KEY, version); -writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); +// // Copy manifest.json +// let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); +// browserManifest = browserManifest.replace(VERSION_KEY, version); +// writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); -// Copy icons folder -mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); -cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); +// // Copy icons folder +// mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); +// cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); -// Copy fonts folder -mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); -cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); +// // Copy fonts folder +// mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); +// cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); -// Compress extension folder into zip -const output = createWriteStream(DIST_DIR + "/extension.zip"); -const archive = archiver('zip'); +// // Compress extension folder into zip +// const output = createWriteStream(DIST_DIR + "/extension.zip"); +// const archive = archiver('zip'); -output.on('close', () => { - console.log(`Created zip file: ${archive.pointer()} total bytes`); -}); +// output.on('close', () => { +// console.log(`Created zip file: ${archive.pointer()} total bytes`); +// }); -archive.on('error', (err) => { - throw err; -}); +// archive.on('error', (err) => { +// throw err; +// }); -archive.pipe(output); -archive.directory(EXTENSION_DIR + '/', false); -archive.finalize(); +// archive.pipe(output); +// archive.directory(EXTENSION_DIR + '/', false); +// archive.finalize(); + +async function buildExtension() { + const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY); + + mkdirSync(EXTENSION_DIR, { recursive: true }); + + // Copy birb.js + writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); + + // Copy manifest.json + let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); + browserManifest = browserManifest.replace(VERSION_KEY, version); + writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); + + // Copy icons folder + mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); + cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); + + // Copy fonts folder + mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); + cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); + + // Compress extension folder into zip + const output = createWriteStream(DIST_DIR + "/extension.zip"); + const archive = archiver('zip'); + + output.on('close', () => { + console.log(`Created zip file: ${archive.pointer()} total bytes`); + }); + + archive.on('error', (err) => { + throw err; + }); + + archive.pipe(output); + archive.directory(EXTENSION_DIR + '/', false); + archive.finalize(); +} // ============================================= // Build Obsidian plugin // ============================================= -mkdirSync(OBSIDIAN_DIR, { recursive: true }); +// mkdirSync(OBSIDIAN_DIR, { recursive: true }); -// Wrap birb.js with plugin boilerplate -let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); +// // Wrap birb.js with plugin boilerplate +// let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); -// Encode font to data URI since Obsidian plugins can't have external font files -const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); -const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; -obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri); +// // Encode font to data URI since Obsidian plugins can't have external font files +// const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); +// const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; +// obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri); -// Create main.js with plugin code -writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); +// // Create main.js with plugin code +// writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); -// Copy manifest.json -let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); -obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); -writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +// // Copy manifest.json +// let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); +// obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); +// writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); + +async function buildObsidian() { + const birbJs = await generateCode(OBSIDIAN_ENTRY, true); + + mkdirSync(OBSIDIAN_DIR, { recursive: true }); + + // Wrap birb.js with plugin boilerplate + let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); + + // Create main.js with plugin code + writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); + + // Copy manifest.json + let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); + obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); + writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +} // ============================================= // Build Vencord plugin // ============================================= -mkdirSync(VENCORD_DIR, { recursive: true }); +// mkdirSync(VENCORD_DIR, { recursive: true }); -// Wrap birb.js with plugin boilerplate -let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); +// // Wrap birb.js with plugin boilerplate +// let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); -// Set context to "local" -vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); +// // Set context to "local" +// vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); -// Create exported birb function -writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); +// // Create exported birb function +// writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); -console.log(`Build complete: ${version}`); \ No newline at end of file +// console.log(`Build complete: ${version}`); + +async function buildVencord() { + const birbJs = await generateCode(VENCORD_ENTRY); + + mkdirSync(VENCORD_DIR, { recursive: true }); + + // Wrap birb.js with plugin boilerplate + let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); + + // Set context to "local" + vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); + + // Create exported birb function + writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); +} + +console.log("Starting build..."); + +await buildWeb(); +await buildUserscript(); +await buildExtension(); +await buildObsidian(); +await buildVencord(); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index effd5d8..4af8e0b 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,4 +1,4 @@ -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -226,139 +226,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -990,6 +857,140 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2614,41 +2615,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - initializeApplication(new LocalContext()); - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/extension.zip b/dist/extension.zip index 942b312ef7bca932c3f8a574b5945f3dfde81625..9acdc6064d915f65be706250731f02a0f6d6e495 100644 GIT binary patch delta 26091 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 EKPig_?EnA( delta 26291 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 KFoc0m`2PdDObh}5 diff --git a/dist/extension/birb.js b/dist/extension/birb.js index effd5d8..98b5112 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -1,4 +1,4 @@ -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -226,139 +226,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -990,6 +857,155 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class BrowserExtensionContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from browser extension storage"); + return new Promise((resolve) => { + // @ts-expect-error + chrome.storage.sync.get([SAVE_KEY], (result) => { + resolve(result[SAVE_KEY] ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to browser extension storage"); + // @ts-expect-error + chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { + // @ts-expect-error + if (chrome.runtime.lastError) { + // @ts-expect-error + console.error(chrome.runtime.lastError); + } else { + console.log("Settings saved successfully"); + } + }); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in browser extension storage"); + // @ts-expect-error + chrome.storage.sync.clear(); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2614,41 +2630,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new BrowserExtensionContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index dcf8a32..21fbfde 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -3,7 +3,7 @@ module.exports = class PocketBird extends Plugin { onload() { console.log("Loading Pocket Bird version 2025.11.16..."); const OBSIDIAN_PLUGIN = this; - (function (exports) { + (function () { 'use strict'; const Directions = { @@ -231,139 +231,6 @@ module.exports = class PocketBird extends Plugin { return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -995,6 +862,193 @@ module.exports = class PocketBird extends Plugin { } } + const ROOT_PATH = ""; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class ObsidianContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + return new Promise((resolve) => { + // @ts-expect-error + OBSIDIAN_PLUGIN.loadData().then((data) => { + resolve(data ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData|{}} saveData + */ + async putSaveData(saveData) { + // @ts-expect-error + await OBSIDIAN_PLUGIN.saveData(saveData); + } + + /** @override */ + resetSaveData() { + this.putSaveData({}); + } + + /** @override */ + getFocusableElements() { + const elements = [ + ".workspace-leaf", + ".cm-callout", + ".HyperMD-codeblock-begin", + ".status-bar", + ".mobile-navbar" + ]; + return super.getFocusableElements().concat(elements); + } + + /** @override */ + getPath() { + // @ts-expect-error + const file = app.workspace.getActiveFile(); + if (file && this.getActiveEditorElement()) { + return file.path; + } else { + return ROOT_PATH; + } + } + + /** @override */ + getActivePage() { + if (this.getPath() === ROOT_PATH) { + // Root page, use document element + return document.documentElement + } + return this.getActiveEditorElement() ?? document.documentElement; + } + + /** @override */ + isPathApplicable(path) { + return path === this.getPath(); + } + + /** @override */ + areStickyNotesEnabled() { + return this.getPath() !== ROOT_PATH; + } + + /** @returns {HTMLElement|null} */ + getActiveEditorElement() { + // @ts-expect-error + const activeLeaf = app.workspace.activeLeaf; + const leafElement = activeLeaf?.view?.containerEl; + return leafElement?.querySelector(".cm-scroller") ?? null; + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2619,44 +2673,9 @@ module.exports = class PocketBird extends Plugin { }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new ObsidianContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); console.log("Pocket Bird loaded!"); } diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 285dc2e..80f2cf2 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -12,7 +12,7 @@ // @grant GM_deleteValue // ==/UserScript== -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -240,139 +240,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -1004,6 +871,146 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class UserScriptContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from UserScript storage"); + /** @type {BirbSaveData|{}} */ + let saveData = {}; + // @ts-expect-error + saveData = GM_getValue(SAVE_KEY, {}) ?? {}; + return saveData; + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue(SAVE_KEY, saveData); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2628,41 +2635,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new UserScriptContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.export.js index fbf5b28..9680483 100644 --- a/dist/vencord/birb.export.js +++ b/dist/vencord/birb.export.js @@ -1,5 +1,5 @@ export const Birb = () => { -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -227,139 +227,6 @@ export const Birb = () => { return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -991,6 +858,140 @@ export const Birb = () => { } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2615,43 +2616,8 @@ export const Birb = () => { }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - initializeApplication(new LocalContext()); - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); }; \ No newline at end of file diff --git a/src/context.js b/src/context.js index 8187f5f..d52f166 100644 --- a/src/context.js +++ b/src/context.js @@ -13,14 +13,6 @@ const SET_CONTEXT = "__CONTEXT__" */ export class Context { - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - /** * @abstract * @returns {Promise} @@ -102,6 +94,33 @@ export class Context { } } +export class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } +} + export class UserScriptContext extends Context { /** @@ -135,7 +154,7 @@ export class UserScriptContext extends Context { } } -class BrowserExtensionContext extends Context { +export class BrowserExtensionContext extends Context { /** * @override @@ -257,37 +276,6 @@ export class ObsidianContext extends Context { } } -const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), -]; - -const CONTEXTS_BY_KEY = { - // "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext -}; - -/** - * Determines and returns the current context - * @returns {Context} - */ -// export function getContext() { -// if (CONTEXTS_BY_KEY[SET_CONTEXT]) { -// return new CONTEXTS_BY_KEY[SET_CONTEXT](); -// } -// for (const context of contextProcessingOrder) { -// if (context.isContextActive()) { -// return context; -// } -// } -// error("No applicable context found"); -// // return new LocalContext(); -// return null; -// } - /** * Parse URL parameters into a key-value map * @param {string} url diff --git a/src/platforms/browser.js b/src/platforms/browser.js deleted file mode 100644 index 1c73403..0000000 --- a/src/platforms/browser.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Context, SAVE_KEY } from "../context.js"; -import { log } from "../shared.js"; -import { initializeApplication } from "../application"; - -/** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - -export class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } -} - -initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/extension.js b/src/platforms/extension.js new file mode 100644 index 0000000..e92e3a5 --- /dev/null +++ b/src/platforms/extension.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { BrowserExtensionContext } from "../context.js"; + +initializeApplication(new BrowserExtensionContext()); \ No newline at end of file diff --git a/src/platforms/obsidian.js b/src/platforms/obsidian.js new file mode 100644 index 0000000..26bd0a9 --- /dev/null +++ b/src/platforms/obsidian.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { ObsidianContext } from "../context.js"; + +initializeApplication(new ObsidianContext()); \ No newline at end of file diff --git a/src/platforms/userscript.js b/src/platforms/userscript.js new file mode 100644 index 0000000..9a757b7 --- /dev/null +++ b/src/platforms/userscript.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { UserScriptContext } from "../context.js"; + +initializeApplication(new UserScriptContext()); \ No newline at end of file diff --git a/src/platforms/vencord.js b/src/platforms/vencord.js new file mode 100644 index 0000000..c4813ae --- /dev/null +++ b/src/platforms/vencord.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { LocalContext } from "../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/web.js b/src/platforms/web.js new file mode 100644 index 0000000..c4813ae --- /dev/null +++ b/src/platforms/web.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { LocalContext } from "../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file