From 6ee9efd5a862c68728e9a2dd475f9089498559ea Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 09:51:46 -0500 Subject: [PATCH] Add browser-specific entry point --- build.js | 3 +- dist/birb.js | 696 +++++++++++++--------------------- dist/extension.zip | Bin 150059 -> 149143 bytes dist/extension/birb.js | 696 +++++++++++++--------------------- dist/extension/manifest.json | 2 +- dist/obsidian/main.js | 698 +++++++++++++---------------------- dist/obsidian/manifest.json | 2 +- dist/userscript/birb.user.js | 698 +++++++++++++---------------------- dist/vencord/birb.export.js | 696 +++++++++++++--------------------- src/application.js | 143 +++---- src/context.js | 100 +---- src/platforms/browser.js | 36 ++ src/shared.js | 12 + src/stickyNotes.js | 2 +- 14 files changed, 1441 insertions(+), 2343 deletions(-) create mode 100644 src/platforms/browser.js diff --git a/build.js b/build.js index 385392a..87307da 100644 --- a/build.js +++ b/build.js @@ -24,7 +24,8 @@ 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 + "/application.js"; +const APPLICATION_ENTRY = SRC_DIR + "/platforms/browser.js"; const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const BIRB_OUTPUT = DIST_DIR + "/birb.js"; diff --git a/dist/birb.js b/dist/birb.js index 152e06e..effd5d8 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,4 +1,4 @@ -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -7,6 +7,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -22,6 +23,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -214,6 +226,139 @@ 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", @@ -845,373 +990,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @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; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @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); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @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(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @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; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - 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, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * 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 @@ -1940,68 +1718,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2062,7 +1796,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2678,16 +2412,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2827,8 +2556,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @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 6545b41288fbb4564ad0e65e380f0c85356cffd1..942b312ef7bca932c3f8a574b5945f3dfde81625 100644 GIT binary patch delta 26961 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 TBMd(nMjl8CgCYogsrml{v0zE2 delta 27901 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 EA3+qN8UO$Q diff --git a/dist/extension/birb.js b/dist/extension/birb.js index 152e06e..effd5d8 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -1,4 +1,4 @@ -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -7,6 +7,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -22,6 +23,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -214,6 +226,139 @@ 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", @@ -845,373 +990,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @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; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @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); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @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(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @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; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - 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, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * 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 @@ -1940,68 +1718,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2062,7 +1796,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2678,16 +2412,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2827,8 +2556,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @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/manifest.json b/dist/extension/manifest.json index 821f64a..95435c9 100644 --- a/dist/extension/manifest.json +++ b/dist/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a pet bird in your browser, what more could you want?", - "version": "2025.11.15", + "version": "2025.11.16", "homepage_url": "https://idreesinc.com", "icons": { "48": "images/icons/transparent/48x48x1.png", diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 9d2be02..dcf8a32 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -1,9 +1,9 @@ const { Plugin, Notice } = require('obsidian'); module.exports = class PocketBird extends Plugin { onload() { - console.log("Loading Pocket Bird version 2025.11.15..."); + console.log("Loading Pocket Bird version 2025.11.16..."); const OBSIDIAN_PLUGIN = this; - (function () { + (function (exports) { 'use strict'; const Directions = { @@ -12,6 +12,7 @@ module.exports = class PocketBird extends Plugin { }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -27,6 +28,17 @@ module.exports = class PocketBird extends Plugin { debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -219,6 +231,139 @@ 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", @@ -850,373 +995,6 @@ module.exports = class PocketBird extends Plugin { } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @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; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @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); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @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(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @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; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - 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, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * 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 @@ -1945,68 +1723,24 @@ module.exports = class PocketBird extends Plugin { /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2067,7 +1801,7 @@ module.exports = class PocketBird extends Plugin { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2683,16 +2417,11 @@ module.exports = class PocketBird extends Plugin { return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2832,11 +2561,102 @@ module.exports = class PocketBird extends Plugin { // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @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; + +})({}); console.log("Pocket Bird loaded!"); } diff --git a/dist/obsidian/manifest.json b/dist/obsidian/manifest.json index 5511d87..4e3d91a 100644 --- a/dist/obsidian/manifest.json +++ b/dist/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "pocket-bird", "name": "Pocket Bird", - "version": "2025.11.15", + "version": "2025.11.16", "minAppVersion": "0.15.0", "description": "Add a pet bird to fly around your notes and keep you company!", "author": "Idrees Hassan", diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 4e5b151..285dc2e 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.11.15 +// @version 2025.11.16 // @description It's a pet bird in your browser, what more could you want? // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js @@ -12,7 +12,7 @@ // @grant GM_deleteValue // ==/UserScript== -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -21,6 +21,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -36,6 +37,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -228,6 +240,139 @@ 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", @@ -859,373 +1004,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @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; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @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); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @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(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @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; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - 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, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * 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 @@ -1954,68 +1732,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2076,7 +1810,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2692,16 +2426,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2841,8 +2570,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @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/vencord/birb.export.js b/dist/vencord/birb.export.js index 1022973..fbf5b28 100644 --- a/dist/vencord/birb.export.js +++ b/dist/vencord/birb.export.js @@ -1,5 +1,5 @@ export const Birb = () => { -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -8,6 +8,7 @@ export const Birb = () => { }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -23,6 +24,17 @@ export const Birb = () => { debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -215,6 +227,139 @@ 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", @@ -846,373 +991,6 @@ export const Birb = () => { } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "local"; - - /** - * @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; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @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); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @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(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @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; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - 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, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * 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 @@ -1941,68 +1719,24 @@ export const Birb = () => { /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2063,7 +1797,7 @@ export const Birb = () => { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2679,16 +2413,11 @@ export const Birb = () => { return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2828,10 +2557,101 @@ export const Birb = () => { // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @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/application.js b/src/application.js index 8a47dd4..c194c03 100644 --- a/src/application.js +++ b/src/application.js @@ -2,9 +2,11 @@ import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; import { Birb, Animations } from './birb.js'; -import { getContext, ObsidianContext } from './context.js'; +import { Context, ObsidianContext } from './context.js'; import { + getContext, + setContext, Directions, isDebug, setDebug, @@ -109,68 +111,24 @@ const MIN_FOCUS_ELEMENT_WIDTH = 100; /** @type {Partial} */ let userSettings = {}; -/** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + +/** + * @param {Context} context */ -function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); +export async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } -log("Loading sprite sheets..."); - -Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, featherPixels]) => { +/** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ +function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -851,10 +809,11 @@ Promise.all([ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; + // const fixedAllowed = getContext() instanceof ObsidianContext; + // TODO: FIX + const fixedAllowed = true; const nonFixedElements = largeElements.filter((el) => { if (fixedAllowed) { return true; @@ -1008,6 +967,60 @@ Promise.all([ // Run the birb init(); draw(); -}).catch((e) => { - error("Error while loading sprite sheets: ", e); -}); \ No newline at end of file +} + +/** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ +function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); +} \ No newline at end of file diff --git a/src/context.js b/src/context.js index bfce713..8187f5f 100644 --- a/src/context.js +++ b/src/context.js @@ -1,6 +1,6 @@ import { debug, log, error } from "./shared.js"; -const SAVE_KEY = "birbSaveData"; +export const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; const SET_CONTEXT = "__CONTEXT__" @@ -17,9 +17,9 @@ export class Context { * @abstract * @returns {boolean} Whether this context is applicable */ - isContextActive() { - throw new Error("Method not implemented"); - } + // isContextActive() { + // throw new Error("Method not implemented"); + // } /** * @abstract @@ -102,54 +102,8 @@ export class Context { } } -export class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @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 { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - /** * @override * @returns {Promise} @@ -183,15 +137,6 @@ export class UserScriptContext extends Context { class BrowserExtensionContext extends Context { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - /** * @override * @returns {Promise} @@ -234,15 +179,6 @@ class BrowserExtensionContext extends Context { export class ObsidianContext extends Context { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - /** * @override * @returns {Promise} @@ -325,11 +261,10 @@ const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), - new LocalContext() ]; const CONTEXTS_BY_KEY = { - "local": LocalContext, + // "local": LocalContext, "userscript": UserScriptContext, "browser-extension": BrowserExtensionContext, "obsidian": ObsidianContext @@ -339,18 +274,19 @@ const CONTEXTS_BY_KEY = { * 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, defaulting to LocalContext"); - return new LocalContext(); -} +// 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 diff --git a/src/platforms/browser.js b/src/platforms/browser.js new file mode 100644 index 0000000..1c73403 --- /dev/null +++ b/src/platforms/browser.js @@ -0,0 +1,36 @@ +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/shared.js b/src/shared.js index f925699..01a5a5a 100644 --- a/src/shared.js +++ b/src/shared.js @@ -4,6 +4,7 @@ export const Directions = { }; let debugMode = location.hostname === "127.0.0.1"; +let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -19,6 +20,17 @@ export function setDebug(value) { debugMode = value; } +export function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; +} + +export function setContext(newContext) { + context = newContext; +} + /** * Create an HTML element with the specified parameters * @param {string} className diff --git a/src/stickyNotes.js b/src/stickyNotes.js index 86d78d3..ab1ea37 100644 --- a/src/stickyNotes.js +++ b/src/stickyNotes.js @@ -1,9 +1,9 @@ import { + getContext, makeElement, makeDraggable, makeClosable } from './shared.js'; -import { getContext } from './context.js'; /** * @typedef {Object} SavedStickyNote