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 6545b41..942b312 100644 Binary files a/dist/extension.zip and b/dist/extension.zip differ 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