From a5e81e4265f46ce9ebc8a6b94ccf4f0253b5edfe Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 09:25:48 -0500 Subject: [PATCH 1/6] Allow for set contexts --- build.js | 18 + dist/birb.js | 22 +- dist/extension.zip | Bin 149911 -> 150059 bytes dist/extension/birb.js | 22 +- dist/obsidian/main.js | 22 +- dist/userscript/birb.user.js | 22 +- dist/vencord/birb.export.js | 2837 ++++++++++++++++++++++++++ platform-specific/vencord/wrapper.js | 3 + src/context.js | 22 +- 9 files changed, 2953 insertions(+), 15 deletions(-) create mode 100644 dist/vencord/birb.export.js create mode 100644 platform-specific/vencord/wrapper.js diff --git a/build.js b/build.js index 5ada142..385392a 100644 --- a/build.js +++ b/build.js @@ -16,10 +16,12 @@ const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; +const VENCORD_WRAPPER = "./platform-specific/vencord/wrapper.js"; const USERSCRIPT_DIR = DIST_DIR + "/userscript"; const EXTENSION_DIR = DIST_DIR + "/extension"; 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"; @@ -32,6 +34,7 @@ const VERSION_KEY = "__VERSION__"; const STYLESHEET_KEY = "___STYLESHEET___"; const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__"; const CODE_KEY = "__CODE__"; +const CONTEXT_KEY = "__CONTEXT__"; const spriteSheets = [ { @@ -183,4 +186,19 @@ let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +// ============================================= +// Build Vencord plugin +// ============================================= + +mkdirSync(VENCORD_DIR, { recursive: true }); + +// Wrap birb.js with plugin boilerplate +let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); + +// Set context to "local" +vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); + +// Create exported birb function +writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); + console.log(`Build complete: ${version}`); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index f3319da..152e06e 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -847,6 +847,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1033,7 +1034,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1077,6 +1078,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1164,15 +1166,29 @@ } } - const CONTEXTS = [ + 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() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/extension.zip b/dist/extension.zip index 0eca0e49192915ae03c75d18962a63118c8f9079..6545b41288fbb4564ad0e65e380f0c85356cffd1 100644 GIT binary patch delta 27564 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

qUqsk=14&@Kz?|iB#o8Ddw-jc55Z8}c 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 z zu{M8;>c$BFfxiE_nyRpb&xZC+%!BFePxTQkwR8jnZt7$Jkp=`cKgB^+fko94Pp^RD z(K_-1#^xD$L~S@5A!p#R!E!z*DpfOI37uUwq^e(gX;$!_5+&aHtsAQ%@i+6&a?OyG z$`R$;q9R59Z%EN1e!ZbV!Nd{8aNGz#WM$mi98POJ4xCaG#8%u=VnxJbfw_BsMwGiM z8zFBbT6%lW+8NM0pN?ut*^Sh`y z6g=es@FQauvmQ@m;qbsDi|RxIk_0nsIQy?MY*UjNW@bnVZMBMz^p6U>Vk)LACU6o{ zJk-8dwen}vAre@68Xe^iQ2~^2&&ih?YesOI2Jvf zNnV*+g@l<~9@&AtBTDP@rrZwhLla?|dT1T!xmU@4yue%ef%nx5^HfsfJ7@{nrn3Ps zb1v_3<7TjHO?8N*d7hg}VCk&RhqUp)pSWjbXVNtrIo=SUxZ#}}_J)zgji3^hM20~p zPDmDFK#LWtLmGGK3^eIOB3B4UB1KJIrDj>xvH_JNQq&L5^{QwNeE*YAy1uFe-)+QQ zTFb4`nL!|}T#SVoU$e25Y)^^eH<71e(B#u?<~}`9fVF?H`W4L2pTzMo!*$XQFiWd- zWbFAUbg|bR_fh#x#6@;!D4y_=I51a(1KZGsp(+yQEwhx@yCAL)D|$w!J%+SjS17%b z6q6SekZ3xunXx|`Hr<0T>`HwGo4Vw_w<1}ejZRqF4B7Q(!pz!DXF_PZ@!v50ta;hs z30vk%s#tajX021(u(__IsXVjtu&AabJIUTA+vneN`&CWBJzB*g7(F{NK68Wme$y$3 z4AqX`u8a{gt!IcB)XN_ob)wIf@>BO8N8!6 zvU5k7=?Zcc+9%70C}GA~D^*F2P$*#|*;?Adto2E_;fmSDKw124O_#9yVr4-~6u|6tvnudG~mRV)Z774$nm^CHeQVI3#^LF zV#vzSJz2iDKV-s6fkSjiaeH30tYxF)@C|cg7fwq1`s_qf+?>JyYW1NdQXGAu6 zo5X>88|gxL4nZw6V9Zz{fN*Z?!c2$V8ZC&^oob4AAPkNE5E5 zw>|;^D1$EiouL{#bk-r&6O+Eg3i+A97DuDK=bK_UOjEcmX`I&)bwysEtGARSBA1)_ zwA3CEm&v#(E5oX-nEP!rK&eQOz-f?`F^Bx4DA`c4}-f6e*`W^ye zkrVJDLqZ|EK4<0sJaHxN$OaekMH2vPTMugscq8*i0JBsBIR6alEvf;~Fs~zrMnD$y z>j=Fa5DWE6dl|9U&vgSz{)$Em0Q&lh0YDz`75yWEUqqPt&|!cH}AASzjX;1`Ns=={qil~^Ir>-2SD^|8sL`^ zJ9u6C#UDKoIPzEbTALCQ5T>3E4tVrWG zd?0=8Zh;W+EfD3;9R2^<178ROXK@W0Z0b;Q+J2_bXcO?qaFCG}D|DoZ(Mz)c7(YDS1 z)v~StV8GD}0BCTj#J@6Y`GcD;|AQlfpZ^V^NWMUJ>wh7#?H5x8$$zC}{0Gwf`45E1 z_+Q!R^&nD(x_g1A%bhNa=soob{)_Y5*|0%*#r7fsh&gyXL)k5!8j39IpQ-4;?U? z43OhBSwo28H)Jm>D*xs9zl{k%{NtM(94Paz#=ywfApL&vMfpF}E7U(!BJiXP5a+d| z{gY~o4pt9%REEN O0m)!cgn=*f_WuBKU42~u diff --git a/dist/extension/birb.js b/dist/extension/birb.js index f3319da..152e06e 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -847,6 +847,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1033,7 +1034,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1077,6 +1078,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1164,15 +1166,29 @@ } } - const CONTEXTS = [ + 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() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 5cf3f53..9d2be02 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -852,6 +852,7 @@ module.exports = class PocketBird extends Plugin { const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1038,7 +1039,7 @@ module.exports = class PocketBird extends Plugin { */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1082,6 +1083,7 @@ module.exports = class PocketBird extends Plugin { } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1169,15 +1171,29 @@ module.exports = class PocketBird extends Plugin { } } - const CONTEXTS = [ + 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() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 2614285..4e5b151 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -861,6 +861,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1047,7 +1048,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1091,6 +1092,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1178,15 +1180,29 @@ } } - const CONTEXTS = [ + 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() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.export.js new file mode 100644 index 0000000..1022973 --- /dev/null +++ b/dist/vencord/birb.export.js @@ -0,0 +1,2837 @@ +export const Birb = () => { +(function () { + 'use strict'; + + const Directions = { + LEFT: -1, + RIGHT: 1, + }; + + let debugMode = location.hostname === "127.0.0.1"; + + /** + * @returns {boolean} Whether debug mode is enabled + */ + function isDebug() { + return debugMode; + } + + /** + * @param {boolean} value + */ + function setDebug(value) { + debugMode = value; + } + + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + * @param {HTMLElement} [pageElement] The page element to constrain movement within + */ + function makeDraggable(element, parent = true, callback = () => { }, pageElement) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + const page = pageElement || document.documentElement; + const maxX = page.scrollWidth - elementToMove.clientWidth; + const maxY = page.scrollHeight - elementToMove.clientHeight; + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, Math.min(maxX, e.clientX - offsetX))}px`; + elementToMove.style.top = `${Math.max(0, Math.min(maxY, e.clientY - offsetY))}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + * @param {boolean} [allowEscape] Whether to allow closing with the Escape key + */ + function makeClosable(func, closeButton, allowEscape = true) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (allowEscape && e.key === "Escape") { + func(); + } + }); + } + + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (isDebug()) { + console.debug("Birb: ", ...arguments); + } + } + + function error() { + console.error("Birb: ", ...arguments); + } + + /** + * Get a layer from a sprite sheet array + * @param {string[][]} spriteSheet The sprite sheet pixel array + * @param {number} spriteIndex The sprite index + * @param {number} width The width of each sprite + * @returns {string[][]} + */ + function getLayer(spriteSheet, spriteIndex, width) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); + } + return layer; + } + + /** + * The height of the inner browser window + * Will be the same as getFixedWindowHeight() on most browsers + * On iOS, it will vary to be the height excluding the current address bar size (potentially greater than fixed height) + */ + function getWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed/sticky elements behind the address bar + return window.innerHeight; + } + + /** + * The fixed height of the inner browser window + * Will be the same as getWindowHeight() on most browsers + * On iOS, it will always be the height of the window when the address bar is fully expanded + * @returns The true height of the inner browser window + */ + function getFixedWindowHeight() { + return document.documentElement.clientHeight; + } + + /** Indicators for parts of the base bird sprite sheet */ + const Sprite = { + THEME_HIGHLIGHT: "theme-highlight", + TRANSPARENT: "transparent", + OUTLINE: "outline", + BORDER: "border", + FOOT: "foot", + BEAK: "beak", + EYE: "eye", + FACE: "face", + HOOD: "hood", + NOSE: "nose", + BELLY: "belly", + UNDERBELLY: "underbelly", + WING: "wing", + WING_EDGE: "wing-edge", + HEART: "heart", + HEART_BORDER: "heart-border", + HEART_SHINE: "heart-shine", + FEATHER_SPINE: "feather-spine", + }; + + /** @type {Record} */ + const SPRITE_SHEET_COLOR_MAP = { + "transparent": Sprite.TRANSPARENT, + "#ffffff": Sprite.BORDER, + "#000000": Sprite.OUTLINE, + "#010a19": Sprite.BEAK, + "#190301": Sprite.EYE, + "#af8e75": Sprite.FOOT, + "#639bff": Sprite.FACE, + "#99e550": Sprite.HOOD, + "#d95763": Sprite.NOSE, + "#f8b143": Sprite.BELLY, + "#ec8637": Sprite.UNDERBELLY, + "#578ae6": Sprite.WING, + "#326ed9": Sprite.WING_EDGE, + "#c82e2e": Sprite.HEART, + "#501a1a": Sprite.HEART_BORDER, + "#ff6b6b": Sprite.HEART_SHINE, + "#373737": Sprite.FEATHER_SPINE, + }; + + class BirdType { + /** + * @param {string} name + * @param {string} description + * @param {Record} colors + * @param {string[]} [tags] + */ + constructor(name, description, colors, tags = []) { + this.name = name; + this.description = description; + const defaultColors = { + [Sprite.TRANSPARENT]: "transparent", + [Sprite.OUTLINE]: "#000000", + [Sprite.BORDER]: "#ffffff", + [Sprite.BEAK]: "#000000", + [Sprite.EYE]: "#000000", + [Sprite.HEART]: "#c82e2e", + [Sprite.HEART_BORDER]: "#501a1a", + [Sprite.HEART_SHINE]: "#ff6b6b", + [Sprite.FEATHER_SPINE]: "#373737", + [Sprite.HOOD]: colors.face, + [Sprite.NOSE]: colors.face, + }; + /** @type {Record} */ + this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } + } + + /** @type {Record} */ + const SPECIES = { + bluebird: new BirdType("Eastern Bluebird", + "Native to North American and very social, though can be timid around people.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#639bff", + [Sprite.BELLY]: "#f8b143", + [Sprite.UNDERBELLY]: "#ec8637", + [Sprite.WING]: "#578ae6", + [Sprite.WING_EDGE]: "#326ed9", + }), + shimaEnaga: new BirdType("Shima Enaga", + "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#ffffff", + [Sprite.BELLY]: "#ebe9e8", + [Sprite.UNDERBELLY]: "#ebd9d0", + [Sprite.WING]: "#f3d3c1", + [Sprite.WING_EDGE]: "#2d2d2dff", + [Sprite.THEME_HIGHLIGHT]: "#d7ac93", + }), + tuftedTitmouse: new BirdType("Tufted Titmouse", + "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#c7cad7", + [Sprite.BELLY]: "#e4e5eb", + [Sprite.UNDERBELLY]: "#d7cfcb", + [Sprite.WING]: "#b1b5c5", + [Sprite.WING_EDGE]: "#9d9fa9", + }, ["tuft"]), + europeanRobin: new BirdType("European Robin", + "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#ffaf34", + [Sprite.HOOD]: "#aaa094", + [Sprite.BELLY]: "#ffaf34", + [Sprite.UNDERBELLY]: "#babec2", + [Sprite.WING]: "#aaa094", + [Sprite.WING_EDGE]: "#888580", + [Sprite.THEME_HIGHLIGHT]: "#ffaf34", + }), + redCardinal: new BirdType("Red Cardinal", + "Native to the eastern United States, this strikingly red bird is hard to miss.", { + [Sprite.BEAK]: "#d93619", + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#31353d", + [Sprite.HOOD]: "#e83a1b", + [Sprite.BELLY]: "#e83a1b", + [Sprite.UNDERBELLY]: "#dc3719", + [Sprite.WING]: "#d23215", + [Sprite.WING_EDGE]: "#b1321c", + }, ["tuft"]), + americanGoldfinch: new BirdType("American Goldfinch", + "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", { + [Sprite.BEAK]: "#ffaf34", + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#fff255", + [Sprite.NOSE]: "#383838", + [Sprite.HOOD]: "#383838", + [Sprite.BELLY]: "#fff255", + [Sprite.UNDERBELLY]: "#f5ea63", + [Sprite.WING]: "#e8e079", + [Sprite.WING_EDGE]: "#191919", + [Sprite.THEME_HIGHLIGHT]: "#ffcc00" + }), + barnSwallow: new BirdType("Barn Swallow", + "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#db7c4d", + [Sprite.BELLY]: "#f7e1c9", + [Sprite.UNDERBELLY]: "#ebc9a3", + [Sprite.WING]: "#2252a9", + [Sprite.WING_EDGE]: "#1c448b", + [Sprite.HOOD]: "#2252a9", + }), + mistletoebird: new BirdType("Mistletoebird", + "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { + [Sprite.FOOT]: "#6c6a7c", + [Sprite.FACE]: "#352e6d", + [Sprite.BELLY]: "#fd6833", + [Sprite.UNDERBELLY]: "#e6e1d8", + [Sprite.WING]: "#342b7c", + [Sprite.WING_EDGE]: "#282065", + }), + redAvadavat: new BirdType("Red Avadavat", + "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", { + [Sprite.BEAK]: "#f71919", + [Sprite.FOOT]: "#af7575", + [Sprite.FACE]: "#cb092b", + [Sprite.BELLY]: "#ae1724", + [Sprite.UNDERBELLY]: "#831b24", + [Sprite.WING]: "#7e3030", + [Sprite.WING_EDGE]: "#490f0f", + }), + scarletRobin: new BirdType("Scarlet Robin", + "Native to Australia, this striking robin can be found in Eucalyptus forests.", { + [Sprite.FOOT]: "#494949", + [Sprite.FACE]: "#3d3d3d", + [Sprite.BELLY]: "#fc5633", + [Sprite.UNDERBELLY]: "#dcdcdc", + [Sprite.WING]: "#2b2b2b", + [Sprite.WING_EDGE]: "#ebebeb", + [Sprite.THEME_HIGHLIGHT]: "#fc5633", + }), + americanRobin: new BirdType("American Robin", + "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", { + [Sprite.BEAK]: "#e89f30", + [Sprite.FOOT]: "#9f8075", + [Sprite.FACE]: "#2d2d2d", + [Sprite.BELLY]: "#eb7a3a", + [Sprite.UNDERBELLY]: "#eb7a3a", + [Sprite.WING]: "#444444", + [Sprite.WING_EDGE]: "#232323", + [Sprite.THEME_HIGHLIGHT]: "#eb7a3a", + }), + carolinaWren: new BirdType("Carolina Wren", + "Native to the eastern United States, these little birds are known for their curious and energetic nature.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#edc7a9", + [Sprite.NOSE]: "#f7eee5", + [Sprite.HOOD]: "#c58a5b", + [Sprite.BELLY]: "#e1b796", + [Sprite.UNDERBELLY]: "#c79e7c", + [Sprite.WING]: "#c58a5b", + [Sprite.WING_EDGE]: "#866348", + }), + }; + + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } + + class Frame { + + /** @type {{ [tag: string]: string[][] }} */ + #pixelsByTag = {}; + + /** + * @param {Layer[]} layers + */ + constructor(layers) { + /** @type {Set} */ + let tags = new Set(); + for (let layer of layers) { + tags.add(layer.tag); + } + tags.add("default"); + for (let tag of tags) { + let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0); + if (layers[0].tag !== "default") { + throw new Error("First layer must have the 'default' tag"); + } + this.pixels = layers[0].pixels.map(row => row.slice()); + // Pad from top with transparent pixels + while (this.pixels.length < maxHeight) { + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); + } + // Combine layers + for (let i = 1; i < layers.length; i++) { + if (layers[i].tag === "default" || layers[i].tag === tag) { + let layerPixels = layers[i].pixels; + let topMargin = maxHeight - layerPixels.length; + for (let y = 0; y < layerPixels.length; y++) { + for (let x = 0; x < layerPixels[y].length; x++) { + this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; + } + } + } + } + this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); + } + } + + /** + * @param {string} [tag] + * @returns {string[][]} + */ + getPixels(tag = "default") { + return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"]; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {BirdType} [species] + * @param {number} direction + * @param {number} canvasPixelSize + */ + draw(ctx, direction, canvasPixelSize, species) { + // Clear the canvas before drawing the new frame + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const pixels = this.getPixels(species?.tags[0]); + for (let y = 0; y < pixels.length; y++) { + const row = pixels[y]; + for (let x = 0; x < pixels[y].length; x++) { + const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1]; + ctx.fillStyle = species?.colors[cell] ?? cell; + ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize); + } } } + } + + class Anim { + /** + * @param {Frame[]} frames + * @param {number[]} durations + * @param {boolean} loop + */ + constructor(frames, durations, loop = true) { + this.frames = frames; + this.durations = durations; + this.loop = loop; + this.lastFrameIndex = -1; + this.lastDirection = null; + this.lastTimeStart = null; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * Get the current frame index based on elapsed time + * @param {number} time The elapsed time since animation start + * @returns {number} The index of the current frame + */ + getCurrentFrameIndex(time) { + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + return i; + } + } + return this.frames.length - 1; + } + + /** + * Clear the cached frame state + */ + #clearCache() { + this.lastFrameIndex = -1; + this.lastDirection = null; + } + + /** + * Check if the frame needs to be redrawn + * @param {number} frameIndex The current frame index + * @param {number} direction The current direction + * @returns {boolean} Whether the frame needs to be redrawn + */ + #shouldRedraw(frameIndex, direction) { + return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {number} direction + * @param {number} timeStart The start time of the animation in milliseconds + * @param {number} canvasPixelSize The size of a canvas pixel in pixels + * @param {BirdType} [species] The species to use for the animation + * @returns {boolean} Whether the animation is complete + */ + draw(ctx, direction, timeStart, canvasPixelSize, species) { + // Reset cache if animation was restarted + if (this.lastTimeStart !== timeStart) { + this.#clearCache(); + this.lastTimeStart = timeStart; + } + + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + + if (this.loop) { + time %= duration; + } + + const currentFrameIndex = this.getCurrentFrameIndex(time); + + if (this.#shouldRedraw(currentFrameIndex, direction)) { + this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species); + this.lastFrameIndex = currentFrameIndex; + this.lastDirection = direction; + } + + // Return whether animation is complete (for non-looping animations) + return !this.loop && time >= duration; + } + } + + /** + * @typedef {keyof typeof Animations} AnimationType + */ + + const Animations = /** @type {const} */ ({ + STILL: "STILL", + BOB: "BOB", + FLYING: "FLYING", + HEART: "HEART" + }); + + class Birb { + animStart = Date.now(); + x = 0; + y = 0; + direction = Directions.RIGHT; + isAbsolutePositioned = false; + visible = true; + /** @type {AnimationType} */ + currentAnimation = Animations.STILL; + + /** + * @param {number} birbCssScale + * @param {number} canvasPixelSize + * @param {string[][]} spriteSheet The loaded sprite sheet pixel data + * @param {number} spriteWidth + * @param {number} spriteHeight + */ + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + this.birbCssScale = birbCssScale; + this.canvasPixelSize = canvasPixelSize; + this.windowPixelSize = canvasPixelSize * birbCssScale; + this.spriteWidth = spriteWidth; + this.spriteHeight = spriteHeight; + + // Build layers from sprite sheet + this.layers = { + base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)), + down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)), + heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)), + heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)), + heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)), + tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"), + tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"), + wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)), + wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)), + happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)), + }; + + // Build frames from layers + this.frames = { + base: new Frame([this.layers.base, this.layers.tuftBase]), + headDown: new Frame([this.layers.down, this.layers.tuftDown]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), + heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), + heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + }; + + // Build animations from frames + this.animations = { + [Animations.STILL]: new Anim([this.frames.base], [1000]), + [Animations.BOB]: new Anim([ + this.frames.base, + this.frames.headDown + ], [ + 420, + 420 + ]), + [Animations.FLYING]: new Anim([ + this.frames.base, + this.frames.wingsUp, + this.frames.headDown, + this.frames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + [Animations.HEART]: new Anim([ + this.frames.heartOne, + this.frames.heartTwo, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + // Create canvas element + this.canvas = document.createElement("canvas"); + this.canvas.id = "birb"; + this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize; + this.canvas.height = spriteHeight * canvasPixelSize; + + this.ctx = this.canvas.getContext("2d"); + + // Append to document + document.body.appendChild(this.canvas); + } + + /** + * Draw the current animation frame + * @param {BirdType} species The species color data + * @returns {boolean} Whether the animation has completed (for non-looping animations) + */ + draw(species) { + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * @returns {AnimationType} The current animation key + */ + getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Set the current animation by name and reset the animation timer + * @param {AnimationType} animationName + */ + setAnimation(animationName) { + this.currentAnimation = animationName; + this.animStart = Date.now(); + } + + /** + * Get the frames object + * @returns {Record} + */ + getFrames() { + return this.frames; + } + + /** + * Get the canvas element + * @returns {HTMLCanvasElement} + */ + getElement() { + return this.canvas; + } + + /** + * Get the canvas width in CSS pixels + * @returns {number} + */ + getElementWidth() { + return this.canvas.width * this.birbCssScale; + } + + /** + * Get the canvas height in CSS pixels + * @returns {number} + */ + getElementHeight() { + return this.canvas.height * this.birbCssScale; + } + + getElementTop() { + const rect = this.canvas.getBoundingClientRect(); + return rect.top; + } + + /** + * Set the X position + * @param {number} x + */ + setX(x) { + this.x = x; + let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2)); + this.canvas.style.left = `${x + mod}px`; + } + + /** + * Set the Y position + * @param {number} y + */ + setY(y) { + this.y = y; + let bottom; + if (this.isAbsolutePositioned) { + // Position is absolute, convert from fixed + // Account for address bar shrinkage on iOS + bottom = y - window.scrollY - (getWindowHeight() - getFixedWindowHeight()); + } else { + // Position is fixed + bottom = y; + } + this.canvas.style.bottom = `${bottom}px`; + } + + /** + * Get the current X position + * @returns {number} + */ + getX() { + return this.x; + } + + /** + * Get the current Y position + * @returns {number} + */ + getY() { + return this.y; + } + + /** + * Set the direction the bird is facing + * @param {number} direction + */ + setDirection(direction) { + this.direction = direction; + } + + /** + * Set whether the element should be absolutely positioned + * @param {boolean} absolute + */ + setAbsolutePositioned(absolute) { + this.isAbsolutePositioned = absolute; + if (absolute) { + this.canvas.classList.add("birb-absolute"); + } else { + this.canvas.classList.remove("birb-absolute"); + } + // Update Y position to apply the new positioning mode + this.setY(this.y); + } + + /** + * Set visibility of the bird + * @param {boolean} visible + */ + setVisible(visible) { + this.visible = visible; + this.canvas.style.display = visible ? "" : "none"; + } + + /** + * Get visibility of the bird + * @returns {boolean} + */ + isVisible() { + return this.visible; + } + } + + 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 + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + /** + * @param {StickyNote} stickyNote + * @param {HTMLElement} page + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, page, onSave, onDelete) { + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + const color = getColor(stickyNote.id); + noteElement.style.setProperty("--birb-highlight", color); + noteElement.style.setProperty("--birb-border-color", color); + + // Create header + const header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", "Sticky Note"); + const closeButton = makeElement("birb-window-close", "x"); + header.appendChild(titleDiv); + header.appendChild(closeButton); + + // Create content + const content = makeElement("birb-window-content"); + const textarea = document.createElement("textarea"); + textarea.className = "birb-sticky-note-input"; + textarea.style.width = "150px"; + textarea.placeholder = "Write your notes here and they'll stick to the page!"; + textarea.value = stickyNote.content; + content.appendChild(textarea); + + noteElement.appendChild(header); + noteElement.appendChild(content); + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + page.appendChild(noteElement); + + makeDraggable(header, true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }, page); + + if (closeButton) { + makeClosable(() => { + if (stickyNote.content.trim() === "" || confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton, false); + } + + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + const pageElement = getContext().getActivePage(); + const context = getContext(); + for (let stickyNote of stickyNotes) { + if (context.isPathApplicable(stickyNote.site)) { + renderStickyNote(stickyNote, pageElement, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + if (getContext().areStickyNotesEnabled() === false) { + return; + } + const id = Date.now().toString(); + const site = getContext().getPath(); + const stickyNote = new StickyNote(id, site, ""); + const page = getContext().getActivePage(); + const element = renderStickyNote(stickyNote, page, onSave, () => onDelete(stickyNote)); + element.style.left = `${page.clientWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${page.scrollTop + page.clientHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + + /** + * Get a color based on the mod of the sticky note ID + * @param {string} id + * @returns {string} A color hex code + */ + function getColor(id) { + const colors = ["#ff8baa", "#79bcff", "#d18bff", "#6de192", "#ffd17c", "#ffb37c", "#ff7c7c"]; + const index = parseInt(id, 10) % colors.length; + return colors[index]; + } + + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + */ + constructor(text, action, removeMenu = true) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + } + } + + class ConditionalMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {() => boolean} condition + * @param {boolean} [removeMenu] + */ + constructor(text, action, condition, removeMenu = true) { + super(text, action, removeMenu); + this.condition = condition; + } + } + + class DebugMenuItem extends ConditionalMenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, () => isDebug(), removeMenu); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + + /** + * @param {MenuItem} item + * @param {() => void} removeMenuCallback + * @returns {HTMLElement} + */ + function makeMenuItem(item, removeMenuCallback) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); + } + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenuCallback(); + } + item.action(); + }); + return menuItem; + } + + /** + * Add the menu to the page if it doesn't already exist + * @param {MenuItem[]} menuItems + * @param {string} title + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { + return; + } + let menu = makeElement("birb-window", undefined, MENU_ID); + let header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", title); + header.appendChild(titleDiv); + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!(item instanceof ConditionalMenuItem) || item.condition()) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); + + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); + } + + /** + * Remove the menu from the page + */ + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; + } + + /** + * @param {MenuItem[]} menuItems + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + error("Birb: Content not found"); + return; + } + while (content.firstChild) { + content.removeChild(content.firstChild); + } + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!(item instanceof ConditionalMenuItem) || item.condition()) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); + } + + /** + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote + */ + + /** + * @typedef {Object} BirbSaveData + * @property {string[]} unlockedSpecies + * @property {string} currentSpecies + * @property {Partial} settings + * @property {SavedStickyNote[]} [stickyNotes] + */ + + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + const DEFAULT_SETTINGS = { + birbMode: false + }; + + // Rendering constants + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const FEATHER_SPRITE_WIDTH = 32; + const BIRB_CSS_SCALE = 1; + const UI_CSS_SCALE = isMobile() ? 0.9 : 1; + const CANVAS_PIXEL_SIZE = 1; + const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; + + // Build-time assets + const STYLESHEET = `@font-face { + font-family: 'Monocraft'; + src: url("https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf") format('opentype'); + font-weight: normal; + font-style: normal; +} + +:root { + --birb-border-size: 2px; + --birb-neg-border-size: calc(var(--birb-border-size) * -1); + --birb-double-border-size: calc(var(--birb-border-size) * 2); + --birb-neg-double-border-size: calc(var(--birb-neg-border-size) * 2); + --birb-highlight: #ffa3cb; + --birb-border-color: var(--birb-highlight); + --birb-background-color: #ffecda; + --birb-mix-color: color-mix(in srgb, var(--birb-highlight) 50%, var(--birb-background-color)); + --birb-scale: ${BIRB_CSS_SCALE}; + --birb-ui-scale: ${UI_CSS_SCALE}; +} + +#birb { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483638 !important; + cursor: pointer; +} + +.birb-absolute { + position: absolute !important; +} + +.birb-decoration { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + +.birb-window { + font-family: "Monocraft", monospace !important; + line-height: initial !important; + color: #000000 !important; + z-index: 2147483639 !important; + position: fixed; + background-color: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-border-color), + var(--birb-neg-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color), + var(--birb-double-border-size) 0 var(--birb-border-color), + var(--birb-neg-double-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-double-border-size) var(--birb-border-color), + 0 var(--birb-double-border-size) var(--birb-border-color), + 0 0 0 var(--birb-border-size) var(--birb-border-color), + 0 0 0 var(--birb-double-border-size) white, + var(--birb-double-border-size) 0 0 var(--birb-border-size) white, + var(--birb-neg-double-border-size) 0 0 var(--birb-border-size) white, + 0 var(--birb-neg-double-border-size) 0 var(--birb-border-size) white, + 0 var(--birb-double-border-size) 0 var(--birb-border-size) white; + box-sizing: border-box; + display: flex; + flex-direction: column; + transform: scale(var(--birb-ui-scale)) !important; + animation: pop-in 0.08s; + transition-timing-function: ease-in; +} + +#birb-menu { + transition-duration: 0.2s; + transition-timing-function: ease-out; + min-width: 140px; + z-index: 2147483639 !important; +} + +#birb-menu-exit { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2147483637 !important; +} + +@keyframes pop-in { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 100% { + opacity: 1; + transform: scale(var(--birb-ui-scale)); + } +} + +.birb-window-header { + box-sizing: border-box; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px; + padding-top: 3px; + padding-bottom: 3px; + padding-left: 30px; + padding-right: 30px; + background-color: var(--birb-highlight); + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + var(--birb-neg-border-size) var(--birb-border-size) var(--birb-border-color), + var(--birb-border-size) var(--birb-border-size) var(--birb-border-color); + color: var(--birb-border-color) !important; + font-size: 16px; +} + +.birb-window-title { + text-align: center; + flex-grow: 1; + user-select: none; + color: var(--birb-background-color); + white-space: nowrap; +} + +.birb-window-close { + position: absolute; + top: 1px; + right: 0; + color: var(--birb-background-color); + user-select: none; + cursor: pointer; + padding-left: 5px; + padding-right: 5px; +} + +.birb-window-close:hover { + transform: scale(1.1); +} + +.birb-window-content { + box-sizing: border-box; + background-color: var(--birb-background-color); + margin-top: var(--birb-border-size); + flex-grow: 1; + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: calc(var(--birb-double-border-size)); + padding-bottom: var(--birb-border-size); +} + +.birb-pico-8-content { + background: #111111; + box-shadow: none; + display: flex; + justify-content: center; + overflow: hidden; + border: none; +} + +.birb-pico-8-content iframe { + width: 300px; + margin-left: -15px; + margin-right: -30px; + margin-top: -10px; + margin-bottom: -23px; + border: none; + aspect-ratio: 1; +} + +.birb-music-player-content { + background: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + justify-content: center; + overflow: hidden; + padding: 10px; +} + +.birb-menu-item { + width: calc(100% - var(--birb-double-border-size)); + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + opacity: 0.7 !important; + user-select: none; + display: flex; + justify-content: space-between; + cursor: pointer; + color: black !important; +} + +.birb-menu-item:hover { + opacity: 1 !important; + background: var(--birb-highlight) !important; + color: white !important; + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + 0 var(--birb-border-size) var(--birb-highlight); +} + +.birb-menu-item-arrow { + display: inline-block; +} + +.birb-window-separator { + width: 100%; + height: var(--birb-border-size); + background-color: var(--birb-border-color); + box-sizing: border-box; + margin-top: var(--birb-double-border-size); + margin-bottom: var(--birb-double-border-size); + opacity: 0.4; +} + +#birb-field-guide { + width: 322px !important; +} + +.birb-grid-content { + display: grid; + grid-template-rows: repeat(3, auto); + grid-auto-flow: column; + gap: 10px; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + justify-content: center; + align-items: center; +} + +.birb-grid-item { + width: 64px; + height: 64px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.birb-grid-item:hover { + border-color: var(--birb-highlight); +} + +.birb-grid-item canvas { + image-rendering: pixelated; + transform: scale(2) !important; + padding-bottom: var(--birb-border-size); +} + +.birb-grid-item, .birb-field-guide-description, .birb-message-content { + border: var(--birb-border-size) solid rgb(255, 207, 144); + box-shadow: 0 0 0 var(--birb-border-size) white; + background: rgba(255, 221, 177, 0.5); +} + +.birb-grid-item-locked { + cursor: auto; + filter: grayscale(100%) sepia(30%); +} + +.birb-grid-item-locked canvas { + filter: contrast(90%); +} + +.birb-grid-item-selected { + border: var(--birb-border-size) solid var(--birb-highlight); + background: var(--birb-mix-color); +} + +.birb-field-guide-description { + max-width: calc(100% - 20px); + margin-left: 10px; + margin-right: 10px; + margin-top: 5px; + padding: 8px; + padding-top: 4px; + padding-bottom: 4px; + margin-bottom: 10px; + font-size: 14px; + box-sizing: border-box; + color: rgb(124, 108, 75); +} + +#birb-feather { + cursor: pointer; +} + +.birb-message-content { + box-sizing: border-box; + margin: 2px; + width: 100%; + padding: 10px; + font-size: 14px; + color: rgb(124, 108, 75); +} + +.birb-sticky-note { + position: absolute; + box-sizing: border-box; + animation: fade-in 0.15s ease-in; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.birb-sticky-note > .birb-window-content { + padding: 0; +} + +.birb-sticky-note-input { + width: 100%; + height: 100%; + padding: 10px !important; + resize: both !important; + min-width: 175px !important; + min-height: 135px !important; + box-sizing: border-box !important; + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + color: black !important; + background-color: transparent !important; + border: none !important; +} + +.birb-sticky-note-input:focus { + outline: none !important; + box-shadow: none !important; +}`; + const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; + const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + + // Element IDs + const FIELD_GUIDE_ID = "birb-field-guide"; + const FEATHER_ID = "birb-feather"; + + const DEFAULT_BIRD = "bluebird"; + + // Birb movement + const HOP_SPEED = 0.07; + const FLY_SPEED = isMobile() ? 0.175 : 0.25; + const HOP_DISTANCE = 35; + + // Timing constants (in milliseconds) + const UPDATE_INTERVAL = 1000 / 60; // 60 FPS + const AFK_TIME = isDebug() ? 0 : 1000 * 5; + const PET_BOOST_DURATION = 1000 * 60 * 5; + const PET_MENU_COOLDOWN = 1000; + const URL_CHECK_INTERVAL = 150; + const HOP_DELAY = 500; + + // Random event chances per tick + const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds + const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds + const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + + // Feathers + const FEATHER_FALL_SPEED = 1; + const PET_FEATHER_BOOST = 2; + + // Focus element constraints + 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} + */ + 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); + }; + }); + } + + log("Loading sprite sheets..."); + + Promise.all([ + loadSpriteSheetPixels(SPRITE_SHEET), + loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) + ]).then(([birbPixels, featherPixels]) => { + + const SPRITE_SHEET = birbPixels; + const FEATHER_SPRITE_SHEET = featherPixels; + + const featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + const FEATHER_ANIMATIONS = { + feather: new Anim([ + featherFrames.feather, + ], [ + 1000, + ]), + }; + + const menuItems = [ + new MenuItem(`Pet ${birdBirb()}`, pet), + new MenuItem("Field Guide", insertFieldGuide), + new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()), + new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in SPECIES) { + unlockBird(type); + } + }), + new DebugMenuItem("Add Feather", () => { + activateFeather(); + }), + new DebugMenuItem("Disable Debug", () => { + setDebug(false); + }), + new Separator(), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), + ]; + + const settingsItems = [ + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), + new Separator(), + new MenuItem("Toggle Birb Mode", () => { + userSettings.birbMode = !userSettings.birbMode; + save(); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); + if (userSettings.birbMode) { + message.appendChild(document.createElement("br")); + message.appendChild(document.createElement("br")); + message.appendChild(document.createTextNode("Welcome back to 2012")); + } + 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), + ]; + + const styleElement = document.createElement("style"); + + /** @type {Birb} */ + let birb; + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let frozen = false; + let stateStart = Date.now(); + let currentState = States.IDLE; + let ticks = 0; + // Bird's current position + let birdY = 0; + let birdX = 40; + // Bird's starting position (when flying) + let startX = 0; + let startY = 0; + // Bird's target position (when flying) + let targetX = 0; + let targetY = 0; + /** @type {HTMLElement|null} */ + let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; + let lastActionTimestamp = Date.now(); + /** @type {number[]} */ + let petStack = []; + let currentSpecies = DEFAULT_BIRD; + let unlockedSpecies = [DEFAULT_BIRD]; + // let visible = true; + let lastPetTimestamp = 0; + /** @type {StickyNote[]} */ + let stickyNotes = []; + + async function load() { + /** @type {BirbSaveData|Object} */ + let saveData = await getContext().getSaveData(); + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!('settings' in saveData)) { + log("No user settings found in save data, starting fresh"); + } + + userSettings = saveData.settings ?? {}; + unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; + currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; + stickyNotes = []; + + if (saveData.stickyNotes) { + for (let note of saveData.stickyNotes) { + if (note.id) { + stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); + } + } + } + + log(stickyNotes.length + " sticky notes loaded"); + switchSpecies(currentSpecies); + } + + function save() { + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, + settings: userSettings + }; + + if (stickyNotes.length > 0) { + saveData.stickyNotes = stickyNotes.map(note => ({ + id: note.id, + site: note.site, + content: note.content, + top: note.top, + left: note.left + })); + } + + getContext().putSaveData(saveData); + } + + function resetSaveData() { + getContext().resetSaveData(); + load(); + } + + /** + * Get the user settings merged with default settings + * @returns {Settings} The merged settings + */ + function settings() { + return { ...DEFAULT_SETTINGS, ...userSettings }; + } + + /** + * Bird or birb, you decide + */ + function birdBirb() { + return settings().birbMode ? "Birb" : "Bird"; + } + + function init() { + log("Sprite sheets loaded successfully, initializing bird..."); + + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); + return; + } + + load().then(onLoad); + } + + function onLoad() { + styleElement.textContent = STYLESHEET; + document.head.appendChild(styleElement); + + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb.setAnimation(Animations.BOB); + + window.addEventListener("scroll", () => { + lastActionTimestamp = Date.now(); + }); + + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + const birbElement = birb.getElement(); + + onClick(birbElement, () => { + if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + // Currently being pet, don't open menu + return; + } + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); + + birbElement.addEventListener("mouseover", () => { + lastActionTimestamp = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 3) { + pet(); + // Clear the stack + petStack = []; + } + } + }); + + birbElement.addEventListener("touchmove", (e) => { + pet(); + }); + + drawStickyNotes(stickyNotes, save, deleteStickyNote); + + let lastPath = getContext().getPath().split("?")[0]; + setInterval(() => { + const currentPath = getContext().getPath().split("?")[0]; + if (currentPath !== lastPath) { + log("Path changed, updating sticky notes: " + currentPath); + lastPath = currentPath; + drawStickyNotes(stickyNotes, save, deleteStickyNote); + } + }, URL_CHECK_INTERVAL); + + setInterval(update, UPDATE_INTERVAL); + + focusOnElement(true); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + birb.setVisible(false); + // Won't be restored on fullscreen exit + } + + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Date.now() - stateStart > HOP_DELAY && Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) { + hop(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < FOCUS_SWITCH_CHANCE) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + + // Double the chance of a feather if recently pet + const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; + if (birb.isVisible() && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!birb || !birb.isVisible()) { + return; + } + + updateFocusedElementBounds(); + + // Update the bird's position + if (currentState === States.IDLE) { + if (focusedElement && !isWithinHorizontalBounds()) { + flySomewhere(); + } + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED, 2)) { + setState(States.IDLE); + } + } + + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > getWindowHeight()) { + // Fly to another element or the ground if the focused element moves out of bounds + flySomewhere(); + } + + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); + } + + // Clamp startY, birdY, targetY to a bit above the top of the window + const maxY = getWindowHeight() * 1.5; + startY = Math.min(startY, maxY); + birdY = Math.min(birdY, maxY); + targetY = Math.min(targetY, maxY); + + // Update HTML element position + birb.setX(birdX); + birb.setY(birdY); + } + + /** + * @param {StickyNote} stickyNote + */ + function deleteStickyNote(stickyNote) { + stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); + save(); + } + + /** + * Create a window element with header and content + * @param {string} id + * @param {string} title + * @param {HTMLElement} contentElement + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentElement, onClose) { + const window = makeElement("birb-window", undefined, id); + + const header = makeElement("birb-window-header"); + const titleElement = makeElement("birb-window-title"); + titleElement.textContent = title; + const closeButton = makeElement("birb-window-close"); + closeButton.textContent = "x"; + + header.appendChild(titleElement); + header.appendChild(closeButton); + + const contentWrapper = makeElement("birb-window-content"); + contentWrapper.appendChild(contentElement); + + window.appendChild(header); + window.appendChild(contentWrapper); + + document.body.appendChild(window); + makeDraggable(header); + + makeClosable(() => { + window.remove(); + }, closeButton); + + return window; + } + + function activateFeather() { + if (document.querySelector("#" + FEATHER_ID)) { + return; + } + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); + if (speciesToUnlock.length === 0) { + // No more species to unlock + return; + } + const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; + insertFeather(birdType); + } + + /** + * @param {string} birdType + */ + function insertFeather(birdType) { + let type = SPECIES[birdType]; + const featherCanvas = document.createElement("canvas"); + featherCanvas.id = FEATHER_ID; + featherCanvas.classList.add("birb-decoration"); + featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); + featherCanvas.style.marginLeft = `${x}px`; + featherCanvas.style.top = `${-featherCanvas.height}px`; + const featherCtx = featherCanvas.getContext("2d"); + if (!featherCtx) { + return; + } + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); + document.body.appendChild(featherCanvas); + onClick(featherCanvas, () => { + unlockBird(birdType); + removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } + }); + } + + function removeFeather() { + const feather = document.querySelector("#" + FEATHER_ID); + if (feather) { + feather.remove(); + } + } + + /** + * @param {string} birdType + */ + function unlockBird(birdType) { + if (!unlockedSpecies.includes(birdType)) { + unlockedSpecies.push(birdType); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode("You've found a ")); + const bold = document.createElement("b"); + bold.textContent = SPECIES[birdType].name; + message.appendChild(bold); + message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species.")); + insertModal("New Bird Unlocked!", message); + } + save(); + } + + function updateFeather() { + const feather = document.querySelector("#birb-feather"); + if (!feather || !(feather instanceof HTMLElement)) { + return; + } + const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; + feather.style.top = `${Math.min(y, getWindowHeight() - feather.offsetHeight)}px`; + if (y < getWindowHeight() - feather.offsetHeight) { + feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; + } + } + + /** + * @param {HTMLElement} element + */ + function centerElement(element) { + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${getWindowHeight() / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {HTMLElement} content + */ + function insertModal(title, content) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, content); + + modal.style.width = "270px"; + centerElement(modal); + } + + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > getWindowHeight() / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } + function insertFieldGuide() { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const contentContainer = document.createElement("div"); + const content = makeElement("birb-grid-content"); + const description = makeElement("birb-field-guide-description"); + contentContainer.appendChild(content); + contentContainer.appendChild(description); + + const fieldGuide = createWindow( + FIELD_GUIDE_ID, + "Field Guide", + contentContainer + ); + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = SPECIES[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + + const boldName = document.createElement("b"); + boldName.textContent = type.name; + + const spacer = document.createElement("div"); + spacer.style.height = "0.3em"; + + const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); + + const fragment = document.createDocumentFragment(); + fragment.appendChild(boldName); + fragment.appendChild(spacer); + fragment.appendChild(descText); + + return fragment; + }; + + description.appendChild(generateDescription(currentSpecies)); + for (const [id, type] of Object.entries(SPECIES)) { + const unlocked = unlockedSpecies.includes(id); + const speciesElement = makeElement("birb-grid-item"); + if (id === currentSpecies) { + speciesElement.classList.add("birb-grid-item-selected"); + } + const speciesCanvas = document.createElement("canvas"); + speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + const speciesCtx = speciesCanvas.getContext("2d"); + if (!speciesCtx) { + return; + } + birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + speciesElement.appendChild(speciesCanvas); + content.appendChild(speciesElement); + if (unlocked) { + onClick(speciesElement, () => { + switchSpecies(id); + document.querySelectorAll(".birb-grid-item").forEach((element) => { + element.classList.remove("birb-grid-item-selected"); + }); + speciesElement.classList.add("birb-grid-item-selected"); + }); + } else { + speciesElement.classList.add("birb-grid-item-locked"); + } + speciesElement.addEventListener("mouseover", () => { + description.textContent = ""; + description.appendChild(generateDescription(id)); + }); + speciesElement.addEventListener("mouseout", () => { + description.textContent = ""; + description.appendChild(generateDescription(currentSpecies)); + }); + } + centerElement(fieldGuide); + } + + function removeFieldGuide() { + const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); + if (fieldGuide) { + fieldGuide.remove(); + } + } + + /** + * @param {string} type + */ + function switchSpecies(type) { + currentSpecies = type; + // Update CSS variable --birb-highlight to be wing color + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); + save(); + } + + /** + * Update the birds location from the start to the target location on a parabolic path + * @param {number} speed The speed of the bird along the path + * @param {number} [intensity] The intensity of the parabolic path + * @returns {boolean} Whether the bird has reached the target location + */ + function updateParabolicPath(speed, intensity = 2.5) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, getWindowHeight()) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT); + } + return complete; + } + + function getFocusedElementRandomX() { + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; + } + + function isWithinHorizontalBounds() { + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; + } + + function getFocusedY() { + return getWindowHeight() - focusedBounds.top; + } + + /** + * Fly to either an element or the ground + */ + function flySomewhere() { + // On mobile, always prefer to focus on an element + // If not mobile, 50% chance to focus on ground + // if ((!isMobile() && coinFlip()) || !focusOnElement()) { + // focusOnGround(); + // } + if (!focusOnElement()) { + focusOnGround(); + } + } + + function focusOnGround() { + focusedElement = null; + updateFocusedElementBounds(); + flyTo(Math.random() * window.innerWidth, 0); + } + + /** + * Focus on an element within the viewport + * @param {boolean} [teleport] Whether to teleport to the element instead of flying + * @returns Whether an element to focus on was found + */ + function focusOnElement(teleport = false) { + if (frozen) { + return false; + } + const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); + const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); + const inWindow = Array.from(elements).filter((img) => { + const rect = img.getBoundingClientRect(); + return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight(); + }); + const visible = Array.from(inWindow).filter((img) => { + const style = window.getComputedStyle(img); + if (style.display === "none" || style.visibility === "hidden" || (style.opacity && parseFloat(style.opacity) < 0.25)) { + return false; + } + 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; + } + const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + if (teleport) { + teleportTo(getFocusedElementRandomX(), getFocusedY()); + } else { + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + return randomElement !== null; + } + + /** + * @param {number} x + * @param {number} y + */ + function teleportTo(x, y) { + birdX = x; + birdY = y; + setState(States.IDLE); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getWindowHeight() }; + return; + } + let { left, right, top } = focusedElement.getBoundingClientRect(); + if (focusedElement.classList.contains("birb-sticky-note")) { + top -= 4.5 * UI_CSS_SCALE; + if (focusedBounds.left !== left) { + // Sticky note has moved + const oldWidth = focusedBounds.right - focusedBounds.left; + const newWidth = right - left; + if (oldWidth === newWidth) { + // Move bird along with note + if (currentState === States.IDLE) { + birdX += left - focusedBounds.left; + } else if (currentState === States.HOP) { + startX += left - focusedBounds.left; + startY += top - focusedBounds.top; + targetX += left - focusedBounds.left; + targetY += top - focusedBounds.top; + } + } + } + } + focusedBounds = { left, right, top }; + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + birb.setAnimation(Animations.FLYING); + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { + targetX = birdX - HOP_DISTANCE; + } else { + targetX = birdX + HOP_DISTANCE; + } + targetY = getFocusedY(); + } + } + + function pet() { + if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + birb.setAnimation(Animations.FLYING); + } + + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + + /** + * Set the current state and reset the state timer + * @param {string} state + */ + function setState(state) { + stateStart = Date.now(); + startX = birdX; + startY = birdY; + currentState = state; + if (state === States.IDLE) { + birb.setAnimation(Animations.BOB); + } + birb.setAbsolutePositioned(isAbsolute()); + birb.setY(birdY); + } + + // Helper functions + + /** + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {number} amount + * @param {number} [intensity] + * @returns {{x: number, y: number}} + */ + function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const midX = startX + Math.cos(angle) * distance / 2; + const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; + const t = amount; + const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; + const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; + return { x, y }; + } + + // Run the birb + init(); + draw(); + }).catch((e) => { + error("Error while loading sprite sheets: ", e); + }); + +})(); + +}; \ No newline at end of file diff --git a/platform-specific/vencord/wrapper.js b/platform-specific/vencord/wrapper.js new file mode 100644 index 0000000..9205fc7 --- /dev/null +++ b/platform-specific/vencord/wrapper.js @@ -0,0 +1,3 @@ +export const Birb = () => { +__CODE__ +}; \ No newline at end of file diff --git a/src/context.js b/src/context.js index 5811414..bfce713 100644 --- a/src/context.js +++ b/src/context.js @@ -2,6 +2,7 @@ import { debug, log, error } from "./shared.js"; const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; +const SET_CONTEXT = "__CONTEXT__" /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -188,7 +189,7 @@ class BrowserExtensionContext extends Context { */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -232,6 +233,7 @@ class BrowserExtensionContext extends Context { } export class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -319,15 +321,29 @@ export class ObsidianContext extends Context { } } -const CONTEXTS = [ +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} + */ export function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } From 6ee9efd5a862c68728e9a2dd475f9089498559ea Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 09:51:46 -0500 Subject: [PATCH 2/6] Add browser-specific entry point --- build.js | 3 +- dist/birb.js | 696 +++++++++++++--------------------- dist/extension.zip | Bin 150059 -> 149143 bytes dist/extension/birb.js | 696 +++++++++++++--------------------- dist/extension/manifest.json | 2 +- dist/obsidian/main.js | 698 +++++++++++++---------------------- dist/obsidian/manifest.json | 2 +- dist/userscript/birb.user.js | 698 +++++++++++++---------------------- dist/vencord/birb.export.js | 696 +++++++++++++--------------------- src/application.js | 143 +++---- src/context.js | 100 +---- src/platforms/browser.js | 36 ++ src/shared.js | 12 + src/stickyNotes.js | 2 +- 14 files changed, 1441 insertions(+), 2343 deletions(-) create mode 100644 src/platforms/browser.js diff --git a/build.js b/build.js index 385392a..87307da 100644 --- a/build.js +++ b/build.js @@ -24,7 +24,8 @@ const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; const VENCORD_DIR = DIST_DIR + "/vencord"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; -const APPLICATION_ENTRY = SRC_DIR + "/application.js"; +// const APPLICATION_ENTRY = SRC_DIR + "/application.js"; +const APPLICATION_ENTRY = SRC_DIR + "/platforms/browser.js"; const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const BIRB_OUTPUT = DIST_DIR + "/birb.js"; diff --git a/dist/birb.js b/dist/birb.js index 152e06e..effd5d8 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,4 +1,4 @@ -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -7,6 +7,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -22,6 +23,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -214,6 +226,139 @@ return document.documentElement.clientHeight; } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + // isContextActive() { + // throw new Error("Method not implemented"); + // } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + /** + * Determines and returns the current context + * @returns {Context} + */ + // export function getContext() { + // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + // return new CONTEXTS_BY_KEY[SET_CONTEXT](); + // } + // for (const context of contextProcessingOrder) { + // if (context.isContextActive()) { + // return context; + // } + // } + // error("No applicable context found"); + // // return new LocalContext(); + // return null; + // } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -845,373 +990,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - isContextActive() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from UserScript storage"); - /** @type {BirbSaveData|{}} */ - let saveData = {}; - // @ts-expect-error - saveData = GM_getValue(SAVE_KEY, {}) ?? {}; - return saveData; - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue(SAVE_KEY, saveData); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue(SAVE_KEY); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from browser extension storage"); - return new Promise((resolve) => { - // @ts-expect-error - chrome.storage.sync.get([SAVE_KEY], (result) => { - resolve(result[SAVE_KEY] ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to browser extension storage"); - // @ts-expect-error - chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { - // @ts-expect-error - if (chrome.runtime.lastError) { - // @ts-expect-error - console.error(chrome.runtime.lastError); - } else { - console.log("Settings saved successfully"); - } - }); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in browser extension storage"); - // @ts-expect-error - chrome.storage.sync.clear(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - return new Promise((resolve) => { - // @ts-expect-error - OBSIDIAN_PLUGIN.loadData().then((data) => { - resolve(data ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData|{}} saveData - */ - async putSaveData(saveData) { - // @ts-expect-error - await OBSIDIAN_PLUGIN.saveData(saveData); - } - - /** @override */ - resetSaveData() { - this.putSaveData({}); - } - - /** @override */ - getFocusableElements() { - const elements = [ - ".workspace-leaf", - ".cm-callout", - ".HyperMD-codeblock-begin", - ".status-bar", - ".mobile-navbar" - ]; - return super.getFocusableElements().concat(elements); - } - - /** @override */ - getPath() { - // @ts-expect-error - const file = app.workspace.getActiveFile(); - if (file && this.getActiveEditorElement()) { - return file.path; - } else { - return ROOT_PATH; - } - } - - /** @override */ - getActivePage() { - if (this.getPath() === ROOT_PATH) { - // Root page, use document element - return document.documentElement - } - return this.getActiveEditorElement() ?? document.documentElement; - } - - /** @override */ - isPathApplicable(path) { - return path === this.getPath(); - } - - /** @override */ - areStickyNotesEnabled() { - return this.getPath() !== ROOT_PATH; - } - - /** @returns {HTMLElement|null} */ - getActiveEditorElement() { - // @ts-expect-error - const activeLeaf = app.workspace.activeLeaf; - const leafElement = activeLeaf?.view?.containerEl; - return leafElement?.querySelector(".cm-scroller") ?? null; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1940,68 +1718,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2062,7 +1796,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2678,16 +2412,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2827,8 +2556,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + initializeApplication(new LocalContext()); + + exports.LocalContext = LocalContext; + + return exports; + +})({}); diff --git a/dist/extension.zip b/dist/extension.zip index 6545b41288fbb4564ad0e65e380f0c85356cffd1..942b312ef7bca932c3f8a574b5945f3dfde81625 100644 GIT binary patch delta 26961 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 TBMd(nMjl8CgCYogsrml{v0zE2 delta 27901 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 EA3+qN8UO$Q diff --git a/dist/extension/birb.js b/dist/extension/birb.js index 152e06e..effd5d8 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -1,4 +1,4 @@ -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -7,6 +7,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -22,6 +23,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -214,6 +226,139 @@ return document.documentElement.clientHeight; } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + // isContextActive() { + // throw new Error("Method not implemented"); + // } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + /** + * Determines and returns the current context + * @returns {Context} + */ + // export function getContext() { + // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + // return new CONTEXTS_BY_KEY[SET_CONTEXT](); + // } + // for (const context of contextProcessingOrder) { + // if (context.isContextActive()) { + // return context; + // } + // } + // error("No applicable context found"); + // // return new LocalContext(); + // return null; + // } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -845,373 +990,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - isContextActive() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from UserScript storage"); - /** @type {BirbSaveData|{}} */ - let saveData = {}; - // @ts-expect-error - saveData = GM_getValue(SAVE_KEY, {}) ?? {}; - return saveData; - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue(SAVE_KEY, saveData); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue(SAVE_KEY); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from browser extension storage"); - return new Promise((resolve) => { - // @ts-expect-error - chrome.storage.sync.get([SAVE_KEY], (result) => { - resolve(result[SAVE_KEY] ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to browser extension storage"); - // @ts-expect-error - chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { - // @ts-expect-error - if (chrome.runtime.lastError) { - // @ts-expect-error - console.error(chrome.runtime.lastError); - } else { - console.log("Settings saved successfully"); - } - }); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in browser extension storage"); - // @ts-expect-error - chrome.storage.sync.clear(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - return new Promise((resolve) => { - // @ts-expect-error - OBSIDIAN_PLUGIN.loadData().then((data) => { - resolve(data ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData|{}} saveData - */ - async putSaveData(saveData) { - // @ts-expect-error - await OBSIDIAN_PLUGIN.saveData(saveData); - } - - /** @override */ - resetSaveData() { - this.putSaveData({}); - } - - /** @override */ - getFocusableElements() { - const elements = [ - ".workspace-leaf", - ".cm-callout", - ".HyperMD-codeblock-begin", - ".status-bar", - ".mobile-navbar" - ]; - return super.getFocusableElements().concat(elements); - } - - /** @override */ - getPath() { - // @ts-expect-error - const file = app.workspace.getActiveFile(); - if (file && this.getActiveEditorElement()) { - return file.path; - } else { - return ROOT_PATH; - } - } - - /** @override */ - getActivePage() { - if (this.getPath() === ROOT_PATH) { - // Root page, use document element - return document.documentElement - } - return this.getActiveEditorElement() ?? document.documentElement; - } - - /** @override */ - isPathApplicable(path) { - return path === this.getPath(); - } - - /** @override */ - areStickyNotesEnabled() { - return this.getPath() !== ROOT_PATH; - } - - /** @returns {HTMLElement|null} */ - getActiveEditorElement() { - // @ts-expect-error - const activeLeaf = app.workspace.activeLeaf; - const leafElement = activeLeaf?.view?.containerEl; - return leafElement?.querySelector(".cm-scroller") ?? null; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1940,68 +1718,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2062,7 +1796,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2678,16 +2412,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2827,8 +2556,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + initializeApplication(new LocalContext()); + + exports.LocalContext = LocalContext; + + return exports; + +})({}); diff --git a/dist/extension/manifest.json b/dist/extension/manifest.json index 821f64a..95435c9 100644 --- a/dist/extension/manifest.json +++ b/dist/extension/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Pocket Bird", "description": "It's a pet bird in your browser, what more could you want?", - "version": "2025.11.15", + "version": "2025.11.16", "homepage_url": "https://idreesinc.com", "icons": { "48": "images/icons/transparent/48x48x1.png", diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 9d2be02..dcf8a32 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -1,9 +1,9 @@ const { Plugin, Notice } = require('obsidian'); module.exports = class PocketBird extends Plugin { onload() { - console.log("Loading Pocket Bird version 2025.11.15..."); + console.log("Loading Pocket Bird version 2025.11.16..."); const OBSIDIAN_PLUGIN = this; - (function () { + (function (exports) { 'use strict'; const Directions = { @@ -12,6 +12,7 @@ module.exports = class PocketBird extends Plugin { }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -27,6 +28,17 @@ module.exports = class PocketBird extends Plugin { debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -219,6 +231,139 @@ module.exports = class PocketBird extends Plugin { return document.documentElement.clientHeight; } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + // isContextActive() { + // throw new Error("Method not implemented"); + // } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + /** + * Determines and returns the current context + * @returns {Context} + */ + // export function getContext() { + // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + // return new CONTEXTS_BY_KEY[SET_CONTEXT](); + // } + // for (const context of contextProcessingOrder) { + // if (context.isContextActive()) { + // return context; + // } + // } + // error("No applicable context found"); + // // return new LocalContext(); + // return null; + // } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -850,373 +995,6 @@ module.exports = class PocketBird extends Plugin { } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - isContextActive() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from UserScript storage"); - /** @type {BirbSaveData|{}} */ - let saveData = {}; - // @ts-expect-error - saveData = GM_getValue(SAVE_KEY, {}) ?? {}; - return saveData; - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue(SAVE_KEY, saveData); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue(SAVE_KEY); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from browser extension storage"); - return new Promise((resolve) => { - // @ts-expect-error - chrome.storage.sync.get([SAVE_KEY], (result) => { - resolve(result[SAVE_KEY] ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to browser extension storage"); - // @ts-expect-error - chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { - // @ts-expect-error - if (chrome.runtime.lastError) { - // @ts-expect-error - console.error(chrome.runtime.lastError); - } else { - console.log("Settings saved successfully"); - } - }); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in browser extension storage"); - // @ts-expect-error - chrome.storage.sync.clear(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - return new Promise((resolve) => { - // @ts-expect-error - OBSIDIAN_PLUGIN.loadData().then((data) => { - resolve(data ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData|{}} saveData - */ - async putSaveData(saveData) { - // @ts-expect-error - await OBSIDIAN_PLUGIN.saveData(saveData); - } - - /** @override */ - resetSaveData() { - this.putSaveData({}); - } - - /** @override */ - getFocusableElements() { - const elements = [ - ".workspace-leaf", - ".cm-callout", - ".HyperMD-codeblock-begin", - ".status-bar", - ".mobile-navbar" - ]; - return super.getFocusableElements().concat(elements); - } - - /** @override */ - getPath() { - // @ts-expect-error - const file = app.workspace.getActiveFile(); - if (file && this.getActiveEditorElement()) { - return file.path; - } else { - return ROOT_PATH; - } - } - - /** @override */ - getActivePage() { - if (this.getPath() === ROOT_PATH) { - // Root page, use document element - return document.documentElement - } - return this.getActiveEditorElement() ?? document.documentElement; - } - - /** @override */ - isPathApplicable(path) { - return path === this.getPath(); - } - - /** @override */ - areStickyNotesEnabled() { - return this.getPath() !== ROOT_PATH; - } - - /** @returns {HTMLElement|null} */ - getActiveEditorElement() { - // @ts-expect-error - const activeLeaf = app.workspace.activeLeaf; - const leafElement = activeLeaf?.view?.containerEl; - return leafElement?.querySelector(".cm-scroller") ?? null; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1945,68 +1723,24 @@ module.exports = class PocketBird extends Plugin { /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2067,7 +1801,7 @@ module.exports = class PocketBird extends Plugin { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2683,16 +2417,11 @@ module.exports = class PocketBird extends Plugin { return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2832,11 +2561,102 @@ module.exports = class PocketBird extends Plugin { // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + initializeApplication(new LocalContext()); + + exports.LocalContext = LocalContext; + + return exports; + +})({}); console.log("Pocket Bird loaded!"); } diff --git a/dist/obsidian/manifest.json b/dist/obsidian/manifest.json index 5511d87..4e3d91a 100644 --- a/dist/obsidian/manifest.json +++ b/dist/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "pocket-bird", "name": "Pocket Bird", - "version": "2025.11.15", + "version": "2025.11.16", "minAppVersion": "0.15.0", "description": "Add a pet bird to fly around your notes and keep you company!", "author": "Idrees Hassan", diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 4e5b151..285dc2e 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Pocket Bird // @namespace https://idreesinc.com -// @version 2025.11.15 +// @version 2025.11.16 // @description It's a pet bird in your browser, what more could you want? // @author Idrees // @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js @@ -12,7 +12,7 @@ // @grant GM_deleteValue // ==/UserScript== -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -21,6 +21,7 @@ }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -36,6 +37,17 @@ debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -228,6 +240,139 @@ return document.documentElement.clientHeight; } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + // isContextActive() { + // throw new Error("Method not implemented"); + // } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + /** + * Determines and returns the current context + * @returns {Context} + */ + // export function getContext() { + // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + // return new CONTEXTS_BY_KEY[SET_CONTEXT](); + // } + // for (const context of contextProcessingOrder) { + // if (context.isContextActive()) { + // return context; + // } + // } + // error("No applicable context found"); + // // return new LocalContext(); + // return null; + // } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -859,373 +1004,6 @@ } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "__CONTEXT__"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - isContextActive() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from UserScript storage"); - /** @type {BirbSaveData|{}} */ - let saveData = {}; - // @ts-expect-error - saveData = GM_getValue(SAVE_KEY, {}) ?? {}; - return saveData; - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue(SAVE_KEY, saveData); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue(SAVE_KEY); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from browser extension storage"); - return new Promise((resolve) => { - // @ts-expect-error - chrome.storage.sync.get([SAVE_KEY], (result) => { - resolve(result[SAVE_KEY] ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to browser extension storage"); - // @ts-expect-error - chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { - // @ts-expect-error - if (chrome.runtime.lastError) { - // @ts-expect-error - console.error(chrome.runtime.lastError); - } else { - console.log("Settings saved successfully"); - } - }); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in browser extension storage"); - // @ts-expect-error - chrome.storage.sync.clear(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - return new Promise((resolve) => { - // @ts-expect-error - OBSIDIAN_PLUGIN.loadData().then((data) => { - resolve(data ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData|{}} saveData - */ - async putSaveData(saveData) { - // @ts-expect-error - await OBSIDIAN_PLUGIN.saveData(saveData); - } - - /** @override */ - resetSaveData() { - this.putSaveData({}); - } - - /** @override */ - getFocusableElements() { - const elements = [ - ".workspace-leaf", - ".cm-callout", - ".HyperMD-codeblock-begin", - ".status-bar", - ".mobile-navbar" - ]; - return super.getFocusableElements().concat(elements); - } - - /** @override */ - getPath() { - // @ts-expect-error - const file = app.workspace.getActiveFile(); - if (file && this.getActiveEditorElement()) { - return file.path; - } else { - return ROOT_PATH; - } - } - - /** @override */ - getActivePage() { - if (this.getPath() === ROOT_PATH) { - // Root page, use document element - return document.documentElement - } - return this.getActiveEditorElement() ?? document.documentElement; - } - - /** @override */ - isPathApplicable(path) { - return path === this.getPath(); - } - - /** @override */ - areStickyNotesEnabled() { - return this.getPath() !== ROOT_PATH; - } - - /** @returns {HTMLElement|null} */ - getActiveEditorElement() { - // @ts-expect-error - const activeLeaf = app.workspace.activeLeaf; - const leafElement = activeLeaf?.view?.containerEl; - return leafElement?.querySelector(".cm-scroller") ?? null; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1954,68 +1732,24 @@ /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2076,7 +1810,7 @@ insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2692,16 +2426,11 @@ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2841,8 +2570,99 @@ // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + initializeApplication(new LocalContext()); + + exports.LocalContext = LocalContext; + + return exports; + +})({}); diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.export.js index 1022973..fbf5b28 100644 --- a/dist/vencord/birb.export.js +++ b/dist/vencord/birb.export.js @@ -1,5 +1,5 @@ export const Birb = () => { -(function () { +(function (exports) { 'use strict'; const Directions = { @@ -8,6 +8,7 @@ export const Birb = () => { }; let debugMode = location.hostname === "127.0.0.1"; + let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -23,6 +24,17 @@ export const Birb = () => { debugMode = value; } + function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; + } + + function setContext(newContext) { + context = newContext; + } + /** * Create an HTML element with the specified parameters * @param {string} className @@ -215,6 +227,139 @@ export const Birb = () => { return document.documentElement.clientHeight; } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + // isContextActive() { + // throw new Error("Method not implemented"); + // } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + /** + * Determines and returns the current context + * @returns {Context} + */ + // export function getContext() { + // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + // return new CONTEXTS_BY_KEY[SET_CONTEXT](); + // } + // for (const context of contextProcessingOrder) { + // if (context.isContextActive()) { + // return context; + // } + // } + // error("No applicable context found"); + // // return new LocalContext(); + // return null; + // } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -846,373 +991,6 @@ export const Birb = () => { } } - const SAVE_KEY = "birbSaveData"; - const ROOT_PATH = ""; - const SET_CONTEXT = "local"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - isContextActive() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - class UserScriptContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from UserScript storage"); - /** @type {BirbSaveData|{}} */ - let saveData = {}; - // @ts-expect-error - saveData = GM_getValue(SAVE_KEY, {}) ?? {}; - return saveData; - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to UserScript storage"); - // @ts-expect-error - GM_setValue(SAVE_KEY, saveData); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in UserScript storage"); - // @ts-expect-error - GM_deleteValue(SAVE_KEY); - } - } - - class BrowserExtensionContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from browser extension storage"); - return new Promise((resolve) => { - // @ts-expect-error - chrome.storage.sync.get([SAVE_KEY], (result) => { - resolve(result[SAVE_KEY] ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to browser extension storage"); - // @ts-expect-error - chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { - // @ts-expect-error - if (chrome.runtime.lastError) { - // @ts-expect-error - console.error(chrome.runtime.lastError); - } else { - console.log("Settings saved successfully"); - } - }); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in browser extension storage"); - // @ts-expect-error - chrome.storage.sync.clear(); - } - } - - class ObsidianContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - return new Promise((resolve) => { - // @ts-expect-error - OBSIDIAN_PLUGIN.loadData().then((data) => { - resolve(data ?? {}); - }); - }); - } - - /** - * @override - * @param {BirbSaveData|{}} saveData - */ - async putSaveData(saveData) { - // @ts-expect-error - await OBSIDIAN_PLUGIN.saveData(saveData); - } - - /** @override */ - resetSaveData() { - this.putSaveData({}); - } - - /** @override */ - getFocusableElements() { - const elements = [ - ".workspace-leaf", - ".cm-callout", - ".HyperMD-codeblock-begin", - ".status-bar", - ".mobile-navbar" - ]; - return super.getFocusableElements().concat(elements); - } - - /** @override */ - getPath() { - // @ts-expect-error - const file = app.workspace.getActiveFile(); - if (file && this.getActiveEditorElement()) { - return file.path; - } else { - return ROOT_PATH; - } - } - - /** @override */ - getActivePage() { - if (this.getPath() === ROOT_PATH) { - // Root page, use document element - return document.documentElement - } - return this.getActiveEditorElement() ?? document.documentElement; - } - - /** @override */ - isPathApplicable(path) { - return path === this.getPath(); - } - - /** @override */ - areStickyNotesEnabled() { - return this.getPath() !== ROOT_PATH; - } - - /** @returns {HTMLElement|null} */ - getActiveEditorElement() { - // @ts-expect-error - const activeLeaf = app.workspace.activeLeaf; - const leafElement = activeLeaf?.view?.containerEl; - return leafElement?.querySelector(".cm-scroller") ?? null; - } - } - - const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), - new LocalContext() - ]; - - const CONTEXTS_BY_KEY = { - "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext - }; - - /** - * Determines and returns the current context - * @returns {Context} - */ - function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); - } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -1941,68 +1719,24 @@ export const Birb = () => { /** @type {Partial} */ let userSettings = {}; - /** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + + /** + * @param {Context} context */ - function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); + async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } - log("Loading sprite sheets..."); - - Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) - ]).then(([birbPixels, featherPixels]) => { + /** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ + function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -2063,7 +1797,7 @@ export const Birb = () => { insertModal(`${birdBirb()} Mode`, message); }), new Separator(), - new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + new MenuItem("2025.11.16", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.16"); }, false), ]; const styleElement = document.createElement("style"); @@ -2679,16 +2413,11 @@ export const Birb = () => { return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); - // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; const nonFixedElements = largeElements.filter((el) => { - if (fixedAllowed) { + { return true; } - const style = window.getComputedStyle(el); - return style.position !== "fixed" && style.position !== "sticky"; }); if (nonFixedElements.length === 0) { return false; @@ -2828,10 +2557,101 @@ export const Birb = () => { // Run the birb init(); draw(); - }).catch((e) => { - error("Error while loading sprite sheets: ", e); - }); + } -})(); + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + /** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + initializeApplication(new LocalContext()); + + exports.LocalContext = LocalContext; + + return exports; + +})({}); }; \ No newline at end of file diff --git a/src/application.js b/src/application.js index 8a47dd4..c194c03 100644 --- a/src/application.js +++ b/src/application.js @@ -2,9 +2,11 @@ import Frame from './frame.js'; import Layer from './layer.js'; import Anim from './anim.js'; import { Birb, Animations } from './birb.js'; -import { getContext, ObsidianContext } from './context.js'; +import { Context, ObsidianContext } from './context.js'; import { + getContext, + setContext, Directions, isDebug, setDebug, @@ -109,68 +111,24 @@ const MIN_FOCUS_ELEMENT_WIDTH = 100; /** @type {Partial} */ let userSettings = {}; -/** - * Load the sprite sheet and return the pixel-map template - * @param {string} dataUri - * @param {boolean} [templateColors] - * @returns {Promise} + +/** + * @param {Context} context */ -function loadSpriteSheetPixels(dataUri, templateColors = true) { - return new Promise((resolve, reject) => { - const img = new Image(); - img.src = dataUri; - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - const ctx = canvas.getContext('2d'); - if (!ctx) { - reject(new Error('Failed to get canvas context')); - return; - } - ctx.drawImage(img, 0, 0); - const imageData = ctx.getImageData(0, 0, img.width, img.height); - const pixels = imageData.data; - const hexArray = []; - for (let y = 0; y < img.height; y++) { - const row = []; - for (let x = 0; x < img.width; x++) { - const index = (y * img.width + x) * 4; - const r = pixels[index]; - const g = pixels[index + 1]; - const b = pixels[index + 2]; - const a = pixels[index + 3]; - if (a === 0) { - row.push(Sprite.TRANSPARENT); - continue; - } - const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; - if (!templateColors) { - row.push(hex); - continue; - } - if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { - error(`Unknown color: ${hex}`); - row.push(Sprite.TRANSPARENT); - } - row.push(SPRITE_SHEET_COLOR_MAP[hex]); - } - hexArray.push(row); - } - resolve(hexArray); - }; - img.onerror = (err) => { - reject(err); - }; - }); +export async function initializeApplication(context) { + log("birbOS booting up..."); + setContext(context); + log("Loading sprite sheets..."); + const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET); + const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET); + startApplication(birbPixels, featherPixels); } -log("Loading sprite sheets..."); - -Promise.all([ - loadSpriteSheetPixels(SPRITE_SHEET), - loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) -]).then(([birbPixels, featherPixels]) => { +/** + * @param {string[][]} birbPixels + * @param {string[][]} featherPixels + */ +function startApplication(birbPixels, featherPixels) { const SPRITE_SHEET = birbPixels; const FEATHER_SPRITE_SHEET = featherPixels; @@ -851,10 +809,11 @@ Promise.all([ return true; }); /** @type {HTMLElement[]} */ - // @ts-expect-error const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); // Ensure the bird doesn't land on fixed or sticky elements - const fixedAllowed = getContext() instanceof ObsidianContext; + // const fixedAllowed = getContext() instanceof ObsidianContext; + // TODO: FIX + const fixedAllowed = true; const nonFixedElements = largeElements.filter((el) => { if (fixedAllowed) { return true; @@ -1008,6 +967,60 @@ Promise.all([ // Run the birb init(); draw(); -}).catch((e) => { - error("Error while loading sprite sheets: ", e); -}); \ No newline at end of file +} + +/** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ +function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); +} \ No newline at end of file diff --git a/src/context.js b/src/context.js index bfce713..8187f5f 100644 --- a/src/context.js +++ b/src/context.js @@ -1,6 +1,6 @@ import { debug, log, error } from "./shared.js"; -const SAVE_KEY = "birbSaveData"; +export const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; const SET_CONTEXT = "__CONTEXT__" @@ -17,9 +17,9 @@ export class Context { * @abstract * @returns {boolean} Whether this context is applicable */ - isContextActive() { - throw new Error("Method not implemented"); - } + // isContextActive() { + // throw new Error("Method not implemented"); + // } /** * @abstract @@ -102,54 +102,8 @@ export class Context { } } -export class LocalContext extends Context { - - /** - * @override - * @returns {boolean} - */ - isContextActive() { - return window.location.hostname === "127.0.0.1" - || window.location.hostname === "localhost" - || window.location.hostname.startsWith("192.168."); - } - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } -} - export class UserScriptContext extends Context { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof GM_getValue === "function"; - } - /** * @override * @returns {Promise} @@ -183,15 +137,6 @@ export class UserScriptContext extends Context { class BrowserExtensionContext extends Context { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; - } - /** * @override * @returns {Promise} @@ -234,15 +179,6 @@ class BrowserExtensionContext extends Context { export class ObsidianContext extends Context { - /** - * @override - * @returns {boolean} - */ - isContextActive() { - // @ts-expect-error - return typeof app !== "undefined" && typeof app.vault !== "undefined"; - } - /** * @override * @returns {Promise} @@ -325,11 +261,10 @@ const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), - new LocalContext() ]; const CONTEXTS_BY_KEY = { - "local": LocalContext, + // "local": LocalContext, "userscript": UserScriptContext, "browser-extension": BrowserExtensionContext, "obsidian": ObsidianContext @@ -339,18 +274,19 @@ const CONTEXTS_BY_KEY = { * Determines and returns the current context * @returns {Context} */ -export function getContext() { - if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - return new CONTEXTS_BY_KEY[SET_CONTEXT](); - } - for (const context of contextProcessingOrder) { - if (context.isContextActive()) { - return context; - } - } - error("No applicable context found, defaulting to LocalContext"); - return new LocalContext(); -} +// export function getContext() { +// if (CONTEXTS_BY_KEY[SET_CONTEXT]) { +// return new CONTEXTS_BY_KEY[SET_CONTEXT](); +// } +// for (const context of contextProcessingOrder) { +// if (context.isContextActive()) { +// return context; +// } +// } +// error("No applicable context found"); +// // return new LocalContext(); +// return null; +// } /** * Parse URL parameters into a key-value map diff --git a/src/platforms/browser.js b/src/platforms/browser.js new file mode 100644 index 0000000..1c73403 --- /dev/null +++ b/src/platforms/browser.js @@ -0,0 +1,36 @@ +import { Context, SAVE_KEY } from "../context.js"; +import { log } from "../shared.js"; +import { initializeApplication } from "../application"; + +/** + * @typedef {import('../application.js').BirbSaveData} BirbSaveData + */ + +export class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } +} + +initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/shared.js b/src/shared.js index f925699..01a5a5a 100644 --- a/src/shared.js +++ b/src/shared.js @@ -4,6 +4,7 @@ export const Directions = { }; let debugMode = location.hostname === "127.0.0.1"; +let context = null; /** * @returns {boolean} Whether debug mode is enabled @@ -19,6 +20,17 @@ export function setDebug(value) { debugMode = value; } +export function getContext() { + if (!context) { + throw new Error("Context requested before being set"); + } + return context; +} + +export function setContext(newContext) { + context = newContext; +} + /** * Create an HTML element with the specified parameters * @param {string} className diff --git a/src/stickyNotes.js b/src/stickyNotes.js index 86d78d3..ab1ea37 100644 --- a/src/stickyNotes.js +++ b/src/stickyNotes.js @@ -1,9 +1,9 @@ import { + getContext, makeElement, makeDraggable, makeClosable } from './shared.js'; -import { getContext } from './context.js'; /** * @typedef {Object} SavedStickyNote From c927ce23e47fbae5e852a67d4be78059577f05fe Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 10:25:57 -0500 Subject: [PATCH 3/6] Add separate entry points --- build.js | 309 ++++++++++++++++++++++-------- dist/birb.js | 306 +++++++++++++---------------- dist/extension.zip | Bin 149143 -> 149002 bytes dist/extension/birb.js | 323 +++++++++++++++---------------- dist/obsidian/main.js | 361 ++++++++++++++++++----------------- dist/userscript/birb.user.js | 314 ++++++++++++++---------------- dist/vencord/birb.export.js | 306 +++++++++++++---------------- src/context.js | 68 +++---- src/platforms/browser.js | 36 ---- src/platforms/extension.js | 4 + src/platforms/obsidian.js | 4 + src/platforms/userscript.js | 4 + src/platforms/vencord.js | 4 + src/platforms/web.js | 4 + 14 files changed, 1032 insertions(+), 1011 deletions(-) delete mode 100644 src/platforms/browser.js create mode 100644 src/platforms/extension.js create mode 100644 src/platforms/obsidian.js create mode 100644 src/platforms/userscript.js create mode 100644 src/platforms/vencord.js create mode 100644 src/platforms/web.js diff --git a/build.js b/build.js index 87307da..ff3324d 100644 --- a/build.js +++ b/build.js @@ -14,7 +14,7 @@ const DIST_DIR = "./dist"; const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; -const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; +const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; const VENCORD_WRAPPER = "./platform-specific/vencord/wrapper.js"; @@ -24,8 +24,13 @@ const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; const VENCORD_DIR = DIST_DIR + "/vencord"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; -// const APPLICATION_ENTRY = SRC_DIR + "/application.js"; -const APPLICATION_ENTRY = SRC_DIR + "/platforms/browser.js"; + +const WEB_ENTRY = SRC_DIR + "/platforms/web.js"; +const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript.js"; +const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension.js"; +const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian.js"; +const VENCORD_ENTRY = SRC_DIR + "/platforms/vencord.js"; + const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const BIRB_OUTPUT = DIST_DIR + "/birb.js"; @@ -78,128 +83,268 @@ const version = `${versionDate}`; // Disable build number for now buildCache.version = version; writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8'); +/** + * @param {string} entryPoint + * @param {boolean} [embedFont] + * @returns {Promise} + */ +async function generateCode(entryPoint, embedFont = false) { + // Bundle with rollup + const bundle = await rollup({ + input: entryPoint, + }); + + await bundle.write({ + file: BUNDLED_OUTPUT, + format: 'iife', + }); + + await bundle.close(); + + let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); + + // Delete bundled file + unlinkSync(BUNDLED_OUTPUT); + + // Replace version placeholder + birbJs = birbJs.replaceAll(VERSION_KEY, version); + + // Compile and insert sprite sheets + for (const spriteSheet of spriteSheets) { + const dataUri = readFileSync(spriteSheet.path, 'base64'); + birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); + } + + // Insert stylesheet + const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); + birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent); + + if (embedFont) { + // Encode font to data URI + const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); + const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; + birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, monocraftDataUri); + } else { + birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); + } + return birbJs; +} + // ============================================= // Build JavaScript function // ============================================= -// Bundle with rollup -const bundle = await rollup({ - input: APPLICATION_ENTRY, -}); - -await bundle.write({ - file: BUNDLED_OUTPUT, - format: 'iife', -}); - -await bundle.close(); - -let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); - -// Delete bundled file -unlinkSync(BUNDLED_OUTPUT); - -// Replace version placeholder -birbJs = birbJs.replaceAll(VERSION_KEY, version); - -// Compile and insert sprite sheets -for (const spriteSheet of spriteSheets) { - const dataUri = readFileSync(spriteSheet.path, 'base64'); - birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); +async function buildWeb() { + const birbJs = await generateCode(WEB_ENTRY); + writeFileSync(BIRB_OUTPUT, birbJs); } -// Insert stylesheet -const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); -birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); +// Bundle with rollup +// const bundle = await rollup({ +// input: APPLICATION_ENTRY, +// }); + +// await bundle.write({ +// file: BUNDLED_OUTPUT, +// format: 'iife', +// }); + +// await bundle.close(); + +// let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); + +// // Delete bundled file +// unlinkSync(BUNDLED_OUTPUT); + +// // Replace version placeholder +// birbJs = birbJs.replaceAll(VERSION_KEY, version); + +// // Compile and insert sprite sheets +// for (const spriteSheet of spriteSheets) { +// const dataUri = readFileSync(spriteSheet.path, 'base64'); +// birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); +// } + +// // Insert stylesheet +// const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); +// birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); -// Write bundled JavaScript function -writeFileSync(BIRB_OUTPUT, birbJs); +// // Write bundled JavaScript function +// writeFileSync(BIRB_OUTPUT, birbJs); // ============================================= // Build userscript // ============================================= // Get userscript header -const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); +// const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); -mkdirSync(USERSCRIPT_DIR, { recursive: true }); -const userScript = userScriptHeader + "\n" + birbJs; -writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); +// mkdirSync(USERSCRIPT_DIR, { recursive: true }); +// const userScript = userScriptHeader + "\n" + birbJs; +// writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); + +async function buildUserscript() { + const birbJs = await generateCode(USERSCRIPT_ENTRY); + // Get userscript header + const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); + + mkdirSync(USERSCRIPT_DIR, { recursive: true }); + const userScript = userScriptHeader + "\n" + birbJs; + writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); +} // ============================================= // Build browser extension // ============================================= -mkdirSync(EXTENSION_DIR, { recursive: true }); +// mkdirSync(EXTENSION_DIR, { recursive: true }); -// Copy birb.js -writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); +// // Copy birb.js +// writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); -// Copy manifest.json -let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); -browserManifest = browserManifest.replace(VERSION_KEY, version); -writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); +// // Copy manifest.json +// let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); +// browserManifest = browserManifest.replace(VERSION_KEY, version); +// writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); -// Copy icons folder -mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); -cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); +// // Copy icons folder +// mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); +// cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); -// Copy fonts folder -mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); -cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); +// // Copy fonts folder +// mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); +// cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); -// Compress extension folder into zip -const output = createWriteStream(DIST_DIR + "/extension.zip"); -const archive = archiver('zip'); +// // Compress extension folder into zip +// const output = createWriteStream(DIST_DIR + "/extension.zip"); +// const archive = archiver('zip'); -output.on('close', () => { - console.log(`Created zip file: ${archive.pointer()} total bytes`); -}); +// output.on('close', () => { +// console.log(`Created zip file: ${archive.pointer()} total bytes`); +// }); -archive.on('error', (err) => { - throw err; -}); +// archive.on('error', (err) => { +// throw err; +// }); -archive.pipe(output); -archive.directory(EXTENSION_DIR + '/', false); -archive.finalize(); +// archive.pipe(output); +// archive.directory(EXTENSION_DIR + '/', false); +// archive.finalize(); + +async function buildExtension() { + const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY); + + mkdirSync(EXTENSION_DIR, { recursive: true }); + + // Copy birb.js + writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); + + // Copy manifest.json + let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); + browserManifest = browserManifest.replace(VERSION_KEY, version); + writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); + + // Copy icons folder + mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); + cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); + + // Copy fonts folder + mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); + cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); + + // Compress extension folder into zip + const output = createWriteStream(DIST_DIR + "/extension.zip"); + const archive = archiver('zip'); + + output.on('close', () => { + console.log(`Created zip file: ${archive.pointer()} total bytes`); + }); + + archive.on('error', (err) => { + throw err; + }); + + archive.pipe(output); + archive.directory(EXTENSION_DIR + '/', false); + archive.finalize(); +} // ============================================= // Build Obsidian plugin // ============================================= -mkdirSync(OBSIDIAN_DIR, { recursive: true }); +// mkdirSync(OBSIDIAN_DIR, { recursive: true }); -// Wrap birb.js with plugin boilerplate -let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); +// // Wrap birb.js with plugin boilerplate +// let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); -// Encode font to data URI since Obsidian plugins can't have external font files -const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); -const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; -obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri); +// // Encode font to data URI since Obsidian plugins can't have external font files +// const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); +// const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; +// obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri); -// Create main.js with plugin code -writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); +// // Create main.js with plugin code +// writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); -// Copy manifest.json -let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); -obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); -writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +// // Copy manifest.json +// let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); +// obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); +// writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); + +async function buildObsidian() { + const birbJs = await generateCode(OBSIDIAN_ENTRY, true); + + mkdirSync(OBSIDIAN_DIR, { recursive: true }); + + // Wrap birb.js with plugin boilerplate + let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); + + // Create main.js with plugin code + writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); + + // Copy manifest.json + let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); + obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); + writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +} // ============================================= // Build Vencord plugin // ============================================= -mkdirSync(VENCORD_DIR, { recursive: true }); +// mkdirSync(VENCORD_DIR, { recursive: true }); -// Wrap birb.js with plugin boilerplate -let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); +// // Wrap birb.js with plugin boilerplate +// let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); -// Set context to "local" -vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); +// // Set context to "local" +// vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); -// Create exported birb function -writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); +// // Create exported birb function +// writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); -console.log(`Build complete: ${version}`); \ No newline at end of file +// console.log(`Build complete: ${version}`); + +async function buildVencord() { + const birbJs = await generateCode(VENCORD_ENTRY); + + mkdirSync(VENCORD_DIR, { recursive: true }); + + // Wrap birb.js with plugin boilerplate + let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); + + // Set context to "local" + vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); + + // Create exported birb function + writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); +} + +console.log("Starting build..."); + +await buildWeb(); +await buildUserscript(); +await buildExtension(); +await buildObsidian(); +await buildVencord(); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index effd5d8..4af8e0b 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -1,4 +1,4 @@ -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -226,139 +226,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -990,6 +857,140 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2614,41 +2615,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - initializeApplication(new LocalContext()); - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/extension.zip b/dist/extension.zip index 942b312ef7bca932c3f8a574b5945f3dfde81625..9acdc6064d915f65be706250731f02a0f6d6e495 100644 GIT binary patch delta 26091 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 EKPig_?EnA( delta 26291 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 KFoc0m`2PdDObh}5 diff --git a/dist/extension/birb.js b/dist/extension/birb.js index effd5d8..98b5112 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -1,4 +1,4 @@ -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -226,139 +226,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -990,6 +857,155 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class BrowserExtensionContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from browser extension storage"); + return new Promise((resolve) => { + // @ts-expect-error + chrome.storage.sync.get([SAVE_KEY], (result) => { + resolve(result[SAVE_KEY] ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to browser extension storage"); + // @ts-expect-error + chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { + // @ts-expect-error + if (chrome.runtime.lastError) { + // @ts-expect-error + console.error(chrome.runtime.lastError); + } else { + console.log("Settings saved successfully"); + } + }); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in browser extension storage"); + // @ts-expect-error + chrome.storage.sync.clear(); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2614,41 +2630,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new BrowserExtensionContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index dcf8a32..21fbfde 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -3,7 +3,7 @@ module.exports = class PocketBird extends Plugin { onload() { console.log("Loading Pocket Bird version 2025.11.16..."); const OBSIDIAN_PLUGIN = this; - (function (exports) { + (function () { 'use strict'; const Directions = { @@ -231,139 +231,6 @@ module.exports = class PocketBird extends Plugin { return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -995,6 +862,193 @@ module.exports = class PocketBird extends Plugin { } } + const ROOT_PATH = ""; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class ObsidianContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + return new Promise((resolve) => { + // @ts-expect-error + OBSIDIAN_PLUGIN.loadData().then((data) => { + resolve(data ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData|{}} saveData + */ + async putSaveData(saveData) { + // @ts-expect-error + await OBSIDIAN_PLUGIN.saveData(saveData); + } + + /** @override */ + resetSaveData() { + this.putSaveData({}); + } + + /** @override */ + getFocusableElements() { + const elements = [ + ".workspace-leaf", + ".cm-callout", + ".HyperMD-codeblock-begin", + ".status-bar", + ".mobile-navbar" + ]; + return super.getFocusableElements().concat(elements); + } + + /** @override */ + getPath() { + // @ts-expect-error + const file = app.workspace.getActiveFile(); + if (file && this.getActiveEditorElement()) { + return file.path; + } else { + return ROOT_PATH; + } + } + + /** @override */ + getActivePage() { + if (this.getPath() === ROOT_PATH) { + // Root page, use document element + return document.documentElement + } + return this.getActiveEditorElement() ?? document.documentElement; + } + + /** @override */ + isPathApplicable(path) { + return path === this.getPath(); + } + + /** @override */ + areStickyNotesEnabled() { + return this.getPath() !== ROOT_PATH; + } + + /** @returns {HTMLElement|null} */ + getActiveEditorElement() { + // @ts-expect-error + const activeLeaf = app.workspace.activeLeaf; + const leafElement = activeLeaf?.view?.containerEl; + return leafElement?.querySelector(".cm-scroller") ?? null; + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2619,44 +2673,9 @@ module.exports = class PocketBird extends Plugin { }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new ObsidianContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); console.log("Pocket Bird loaded!"); } diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 285dc2e..80f2cf2 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -12,7 +12,7 @@ // @grant GM_deleteValue // ==/UserScript== -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -240,139 +240,6 @@ return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -1004,6 +871,146 @@ } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class UserScriptContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from UserScript storage"); + /** @type {BirbSaveData|{}} */ + let saveData = {}; + // @ts-expect-error + saveData = GM_getValue(SAVE_KEY, {}) ?? {}; + return saveData; + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue(SAVE_KEY, saveData); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2628,41 +2635,6 @@ }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ + initializeApplication(new UserScriptContext()); - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - - initializeApplication(new LocalContext()); - - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.export.js index fbf5b28..9680483 100644 --- a/dist/vencord/birb.export.js +++ b/dist/vencord/birb.export.js @@ -1,5 +1,5 @@ export const Birb = () => { -(function (exports) { +(function () { 'use strict'; const Directions = { @@ -227,139 +227,6 @@ export const Birb = () => { return document.documentElement.clientHeight; } - const SAVE_KEY = "birbSaveData"; - - /** - * @typedef {import('./application.js').BirbSaveData} BirbSaveData - */ - - /** - * @abstract - */ - class Context { - - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - - /** - * @abstract - * @returns {Promise} - */ - async getSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @abstract - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - throw new Error("Method not implemented"); - } - - /** - * @abstract - */ - resetSaveData() { - throw new Error("Method not implemented"); - } - - /** - * @returns {string[]} A list of CSS selectors for focusable elements - */ - getFocusableElements() { - return ["img", "video", ".birb-sticky-note"]; - } - - getFocusElementTopMargin() { - return 80; - } - - /** - * @returns {string} The current path of the active page in this context - */ - getPath() { - // Default to website URL - return window.location.href; - } - - /** - * @returns {HTMLElement} The current active page element where sticky notes can be applied - */ - getActivePage() { - // Default to root element - return document.documentElement; - } - - /** - * Checks if a path is applicable given the context - * @param {string} path Can be a site URL or another context-specific path - * @returns {boolean} Whether the path matches the current context state - */ - isPathApplicable(path) { - // Default to website URL matching - const currentUrl = window.location.href; - const stickyNoteWebsite = path.split("?")[0]; - const currentWebsite = currentUrl.split("?")[0]; - - if (stickyNoteWebsite !== currentWebsite) { - return false; - } - - const pathParams = parseUrlParams(path); - const currentParams = parseUrlParams(currentUrl); - - if (window.location.hostname === "www.youtube.com") { - if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { - return false; - } - } - return true; - } - - areStickyNotesEnabled() { - return true; - } - } - - /** - * Determines and returns the current context - * @returns {Context} - */ - // export function getContext() { - // if (CONTEXTS_BY_KEY[SET_CONTEXT]) { - // return new CONTEXTS_BY_KEY[SET_CONTEXT](); - // } - // for (const context of contextProcessingOrder) { - // if (context.isContextActive()) { - // return context; - // } - // } - // error("No applicable context found"); - // // return new LocalContext(); - // return null; - // } - - /** - * Parse URL parameters into a key-value map - * @param {string} url - * @returns {Record} - */ - function parseUrlParams(url) { - const queryString = url.split("?")[1]; - if (!queryString) return {}; - - return queryString.split("&").reduce((params, param) => { - const [key, value] = param.split("="); - return { ...params, [key]: value }; - }, {}); - } - /** Indicators for parts of the base bird sprite sheet */ const Sprite = { THEME_HIGHLIGHT: "theme-highlight", @@ -991,6 +858,140 @@ export const Birb = () => { } } + const SAVE_KEY = "birbSaveData"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + /** * @typedef {Object} SavedStickyNote * @property {string} id @@ -2615,43 +2616,8 @@ export const Birb = () => { }); } - /** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - - class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } - } - initializeApplication(new LocalContext()); - exports.LocalContext = LocalContext; - - return exports; - -})({}); +})(); }; \ No newline at end of file diff --git a/src/context.js b/src/context.js index 8187f5f..d52f166 100644 --- a/src/context.js +++ b/src/context.js @@ -13,14 +13,6 @@ const SET_CONTEXT = "__CONTEXT__" */ export class Context { - /** - * @abstract - * @returns {boolean} Whether this context is applicable - */ - // isContextActive() { - // throw new Error("Method not implemented"); - // } - /** * @abstract * @returns {Promise} @@ -102,6 +94,33 @@ export class Context { } } +export class LocalContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } +} + export class UserScriptContext extends Context { /** @@ -135,7 +154,7 @@ export class UserScriptContext extends Context { } } -class BrowserExtensionContext extends Context { +export class BrowserExtensionContext extends Context { /** * @override @@ -257,37 +276,6 @@ export class ObsidianContext extends Context { } } -const contextProcessingOrder = [ - new UserScriptContext(), - new ObsidianContext(), - new BrowserExtensionContext(), -]; - -const CONTEXTS_BY_KEY = { - // "local": LocalContext, - "userscript": UserScriptContext, - "browser-extension": BrowserExtensionContext, - "obsidian": ObsidianContext -}; - -/** - * Determines and returns the current context - * @returns {Context} - */ -// export function getContext() { -// if (CONTEXTS_BY_KEY[SET_CONTEXT]) { -// return new CONTEXTS_BY_KEY[SET_CONTEXT](); -// } -// for (const context of contextProcessingOrder) { -// if (context.isContextActive()) { -// return context; -// } -// } -// error("No applicable context found"); -// // return new LocalContext(); -// return null; -// } - /** * Parse URL parameters into a key-value map * @param {string} url diff --git a/src/platforms/browser.js b/src/platforms/browser.js deleted file mode 100644 index 1c73403..0000000 --- a/src/platforms/browser.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Context, SAVE_KEY } from "../context.js"; -import { log } from "../shared.js"; -import { initializeApplication } from "../application"; - -/** - * @typedef {import('../application.js').BirbSaveData} BirbSaveData - */ - -export class LocalContext extends Context { - - /** - * @override - * @returns {Promise} - */ - async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); - } - - /** - * @override - * @param {BirbSaveData} saveData - */ - async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - } - - /** @override */ - resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); - } -} - -initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/extension.js b/src/platforms/extension.js new file mode 100644 index 0000000..e92e3a5 --- /dev/null +++ b/src/platforms/extension.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { BrowserExtensionContext } from "../context.js"; + +initializeApplication(new BrowserExtensionContext()); \ No newline at end of file diff --git a/src/platforms/obsidian.js b/src/platforms/obsidian.js new file mode 100644 index 0000000..26bd0a9 --- /dev/null +++ b/src/platforms/obsidian.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { ObsidianContext } from "../context.js"; + +initializeApplication(new ObsidianContext()); \ No newline at end of file diff --git a/src/platforms/userscript.js b/src/platforms/userscript.js new file mode 100644 index 0000000..9a757b7 --- /dev/null +++ b/src/platforms/userscript.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { UserScriptContext } from "../context.js"; + +initializeApplication(new UserScriptContext()); \ No newline at end of file diff --git a/src/platforms/vencord.js b/src/platforms/vencord.js new file mode 100644 index 0000000..c4813ae --- /dev/null +++ b/src/platforms/vencord.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { LocalContext } from "../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/web.js b/src/platforms/web.js new file mode 100644 index 0000000..c4813ae --- /dev/null +++ b/src/platforms/web.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../application.js"; +import { LocalContext } from "../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file From c750bf55606aa4c64828e78d9b936d26be99d55e Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 10:34:43 -0500 Subject: [PATCH 4/6] Move files into src --- build.js | 152 ++---------------- dist/extension.zip | Bin 149002 -> 149002 bytes src/platforms/extension.js | 4 - src/platforms/extension/extension.js | 4 + .../platforms}/extension/manifest.json | 0 src/platforms/obsidian.js | 4 - .../platforms}/obsidian/manifest.json | 0 src/platforms/obsidian/obsidian.js | 4 + .../platforms}/obsidian/wrapper.js | 0 src/platforms/userscript.js | 4 - .../platforms}/userscript/header.txt | 0 src/platforms/userscript/userscript.js | 4 + src/platforms/vencord.js | 4 - src/platforms/vencord/vencord.js | 4 + .../platforms}/vencord/wrapper.js | 0 src/platforms/web.js | 4 - src/platforms/web/web.js | 4 + 17 files changed, 34 insertions(+), 158 deletions(-) delete mode 100644 src/platforms/extension.js create mode 100644 src/platforms/extension/extension.js rename {platform-specific => src/platforms}/extension/manifest.json (100%) delete mode 100644 src/platforms/obsidian.js rename {platform-specific => src/platforms}/obsidian/manifest.json (100%) create mode 100644 src/platforms/obsidian/obsidian.js rename {platform-specific => src/platforms}/obsidian/wrapper.js (100%) delete mode 100644 src/platforms/userscript.js rename {platform-specific => src/platforms}/userscript/header.txt (100%) create mode 100644 src/platforms/userscript/userscript.js delete mode 100644 src/platforms/vencord.js create mode 100644 src/platforms/vencord/vencord.js rename {platform-specific => src/platforms}/vencord/wrapper.js (100%) delete mode 100644 src/platforms/web.js create mode 100644 src/platforms/web/web.js diff --git a/build.js b/build.js index ff3324d..331430f 100644 --- a/build.js +++ b/build.js @@ -12,12 +12,6 @@ const IMAGES_DIR = "./images"; const FONTS_DIR = "./fonts"; const DIST_DIR = "./dist"; -const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; -const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; -const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; -const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; -const VENCORD_WRAPPER = "./platform-specific/vencord/wrapper.js"; - const USERSCRIPT_DIR = DIST_DIR + "/userscript"; const EXTENSION_DIR = DIST_DIR + "/extension"; const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; @@ -25,11 +19,17 @@ const VENCORD_DIR = DIST_DIR + "/vencord"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; -const WEB_ENTRY = SRC_DIR + "/platforms/web.js"; -const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript.js"; -const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension.js"; -const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian.js"; -const VENCORD_ENTRY = SRC_DIR + "/platforms/vencord.js"; +const WEB_ENTRY = SRC_DIR + "/platforms/web/web.js"; +const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript/userscript.js"; +const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension/extension.js"; +const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian/obsidian.js"; +const VENCORD_ENTRY = SRC_DIR + "/platforms/vencord/vencord.js"; + +const BROWSER_MANIFEST = SRC_DIR + "/platforms/extension/manifest.json"; +const OBSIDIAN_MANIFEST = SRC_DIR + "/platforms/obsidian/manifest.json"; +const USERSCRIPT_HEADER = SRC_DIR + "/platforms/userscript/header.txt"; +const OBSIDIAN_WRAPPER = SRC_DIR + "/platforms/obsidian/wrapper.js"; +const VENCORD_WRAPPER = SRC_DIR + "/platforms/vencord/wrapper.js"; const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const BIRB_OUTPUT = DIST_DIR + "/birb.js"; @@ -130,60 +130,11 @@ async function generateCode(entryPoint, embedFont = false) { return birbJs; } -// ============================================= -// Build JavaScript function -// ============================================= - async function buildWeb() { const birbJs = await generateCode(WEB_ENTRY); writeFileSync(BIRB_OUTPUT, birbJs); } -// Bundle with rollup -// const bundle = await rollup({ -// input: APPLICATION_ENTRY, -// }); - -// await bundle.write({ -// file: BUNDLED_OUTPUT, -// format: 'iife', -// }); - -// await bundle.close(); - -// let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); - -// // Delete bundled file -// unlinkSync(BUNDLED_OUTPUT); - -// // Replace version placeholder -// birbJs = birbJs.replaceAll(VERSION_KEY, version); - -// // Compile and insert sprite sheets -// for (const spriteSheet of spriteSheets) { -// const dataUri = readFileSync(spriteSheet.path, 'base64'); -// birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`); -// } - -// // Insert stylesheet -// const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8'); -// birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL); - - -// // Write bundled JavaScript function -// writeFileSync(BIRB_OUTPUT, birbJs); - -// ============================================= -// Build userscript -// ============================================= - -// Get userscript header -// const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); - -// mkdirSync(USERSCRIPT_DIR, { recursive: true }); -// const userScript = userScriptHeader + "\n" + birbJs; -// writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); - async function buildUserscript() { const birbJs = await generateCode(USERSCRIPT_ENTRY); // Get userscript header @@ -194,44 +145,6 @@ async function buildUserscript() { writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript); } -// ============================================= -// Build browser extension -// ============================================= - -// mkdirSync(EXTENSION_DIR, { recursive: true }); - -// // Copy birb.js -// writeFileSync(EXTENSION_DIR + '/birb.js', birbJs); - -// // Copy manifest.json -// let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8'); -// browserManifest = browserManifest.replace(VERSION_KEY, version); -// writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest); - -// // Copy icons folder -// mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true }); -// cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true }); - -// // Copy fonts folder -// mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true }); -// cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true }); - -// // Compress extension folder into zip -// const output = createWriteStream(DIST_DIR + "/extension.zip"); -// const archive = archiver('zip'); - -// output.on('close', () => { -// console.log(`Created zip file: ${archive.pointer()} total bytes`); -// }); - -// archive.on('error', (err) => { -// throw err; -// }); - -// archive.pipe(output); -// archive.directory(EXTENSION_DIR + '/', false); -// archive.finalize(); - async function buildExtension() { const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY); @@ -270,28 +183,6 @@ async function buildExtension() { archive.finalize(); } -// ============================================= -// Build Obsidian plugin -// ============================================= - -// mkdirSync(OBSIDIAN_DIR, { recursive: true }); - -// // Wrap birb.js with plugin boilerplate -// let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs); - -// // Encode font to data URI since Obsidian plugins can't have external font files -// const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64'); -// const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`; -// obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri); - -// // Create main.js with plugin code -// writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin); - -// // Copy manifest.json -// let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); -// obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); -// writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); - async function buildObsidian() { const birbJs = await generateCode(OBSIDIAN_ENTRY, true); @@ -309,23 +200,6 @@ async function buildObsidian() { writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); } -// ============================================= -// Build Vencord plugin -// ============================================= - -// mkdirSync(VENCORD_DIR, { recursive: true }); - -// // Wrap birb.js with plugin boilerplate -// let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); - -// // Set context to "local" -// vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); - -// // Create exported birb function -// writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); - -// console.log(`Build complete: ${version}`); - async function buildVencord() { const birbJs = await generateCode(VENCORD_ENTRY); @@ -347,4 +221,6 @@ await buildWeb(); await buildUserscript(); await buildExtension(); await buildObsidian(); -await buildVencord(); \ No newline at end of file +await buildVencord(); + +console.log("Build completed successfully!"); \ No newline at end of file diff --git a/dist/extension.zip b/dist/extension.zip index 9acdc6064d915f65be706250731f02a0f6d6e495..11750926b8986857311403319b4dd6492be70831 100644 GIT binary patch delta 684 zcmeC`;q2<++^~d|$-idvGFHh%AU*kDni7y|eww!ZX&R%N2ar9TcRiyiklyaHo{^0W z#PHe5xDcdi`o+JD+SAwVXXMzfw3{)W1tzo|D6|F4oA`lIdwc&8#-&UkIS_9;kjJ(? z{WxPHGfeq(T?ZzP?SD@(u7R5X65^fyO@K*Yd;1SY53prm-TXI!y7R6wmVkAGjRy+d zg7c>Te*jb8kG69*~<^)l614;u;2HQ{vr7t-{ui%Opze-j=M5B0{|)%?r;DA delta 684 zcmeC`;q2<++^~d|$)bAmGFHh%AU*kDni7y|eww!ZX&R%N2ar9TcRiyiklyaHo{^0W z#PHe5xDcdi`o+JD+SAwVXXMzfw3{)W1tzo|D6|F4oA`lIdwc&8#-&UkIS_9;kjJ(? z{WxPHGfeq(T?ZzP?SD@(u7R5X65^fyO@K*Yd;1SY53prm-TXI!y7R6wmVkAGjRy+d zg7c>Te*jb8kG69*~<^)l614;u;2HQ{vr7t-{ui%Opze-j=M5B0|16+=+FQF diff --git a/src/platforms/extension.js b/src/platforms/extension.js deleted file mode 100644 index e92e3a5..0000000 --- a/src/platforms/extension.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initializeApplication } from "../application.js"; -import { BrowserExtensionContext } from "../context.js"; - -initializeApplication(new BrowserExtensionContext()); \ No newline at end of file diff --git a/src/platforms/extension/extension.js b/src/platforms/extension/extension.js new file mode 100644 index 0000000..25cebf0 --- /dev/null +++ b/src/platforms/extension/extension.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../../application.js"; +import { BrowserExtensionContext } from "../../context.js"; + +initializeApplication(new BrowserExtensionContext()); \ No newline at end of file diff --git a/platform-specific/extension/manifest.json b/src/platforms/extension/manifest.json similarity index 100% rename from platform-specific/extension/manifest.json rename to src/platforms/extension/manifest.json diff --git a/src/platforms/obsidian.js b/src/platforms/obsidian.js deleted file mode 100644 index 26bd0a9..0000000 --- a/src/platforms/obsidian.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initializeApplication } from "../application.js"; -import { ObsidianContext } from "../context.js"; - -initializeApplication(new ObsidianContext()); \ No newline at end of file diff --git a/platform-specific/obsidian/manifest.json b/src/platforms/obsidian/manifest.json similarity index 100% rename from platform-specific/obsidian/manifest.json rename to src/platforms/obsidian/manifest.json diff --git a/src/platforms/obsidian/obsidian.js b/src/platforms/obsidian/obsidian.js new file mode 100644 index 0000000..fe95ae0 --- /dev/null +++ b/src/platforms/obsidian/obsidian.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../../application.js"; +import { ObsidianContext } from "../../context.js"; + +initializeApplication(new ObsidianContext()); \ No newline at end of file diff --git a/platform-specific/obsidian/wrapper.js b/src/platforms/obsidian/wrapper.js similarity index 100% rename from platform-specific/obsidian/wrapper.js rename to src/platforms/obsidian/wrapper.js diff --git a/src/platforms/userscript.js b/src/platforms/userscript.js deleted file mode 100644 index 9a757b7..0000000 --- a/src/platforms/userscript.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initializeApplication } from "../application.js"; -import { UserScriptContext } from "../context.js"; - -initializeApplication(new UserScriptContext()); \ No newline at end of file diff --git a/platform-specific/userscript/header.txt b/src/platforms/userscript/header.txt similarity index 100% rename from platform-specific/userscript/header.txt rename to src/platforms/userscript/header.txt diff --git a/src/platforms/userscript/userscript.js b/src/platforms/userscript/userscript.js new file mode 100644 index 0000000..cb440f3 --- /dev/null +++ b/src/platforms/userscript/userscript.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../../application.js"; +import { UserScriptContext } from "../../context.js"; + +initializeApplication(new UserScriptContext()); \ No newline at end of file diff --git a/src/platforms/vencord.js b/src/platforms/vencord.js deleted file mode 100644 index c4813ae..0000000 --- a/src/platforms/vencord.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initializeApplication } from "../application.js"; -import { LocalContext } from "../context.js"; - -initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/vencord/vencord.js b/src/platforms/vencord/vencord.js new file mode 100644 index 0000000..9b0133f --- /dev/null +++ b/src/platforms/vencord/vencord.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../../application.js"; +import { LocalContext } from "../../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/platform-specific/vencord/wrapper.js b/src/platforms/vencord/wrapper.js similarity index 100% rename from platform-specific/vencord/wrapper.js rename to src/platforms/vencord/wrapper.js diff --git a/src/platforms/web.js b/src/platforms/web.js deleted file mode 100644 index c4813ae..0000000 --- a/src/platforms/web.js +++ /dev/null @@ -1,4 +0,0 @@ -import { initializeApplication } from "../application.js"; -import { LocalContext } from "../context.js"; - -initializeApplication(new LocalContext()); \ No newline at end of file diff --git a/src/platforms/web/web.js b/src/platforms/web/web.js new file mode 100644 index 0000000..9b0133f --- /dev/null +++ b/src/platforms/web/web.js @@ -0,0 +1,4 @@ +import { initializeApplication } from "../../application.js"; +import { LocalContext } from "../../context.js"; + +initializeApplication(new LocalContext()); \ No newline at end of file From e7be2b7661a60464a77f42b499c0d90758b2ff9b Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 10:38:11 -0500 Subject: [PATCH 5/6] Clean up output --- build.js | 16 +++++++++------- dist/extension.zip | Bin 149002 -> 149002 bytes dist/vencord/{birb.export.js => birb.js} | 0 dist/{ => web}/birb.js | 0 4 files changed, 9 insertions(+), 7 deletions(-) rename dist/vencord/{birb.export.js => birb.js} (100%) rename dist/{ => web}/birb.js (100%) diff --git a/build.js b/build.js index 331430f..d9742ce 100644 --- a/build.js +++ b/build.js @@ -12,6 +12,7 @@ const IMAGES_DIR = "./images"; const FONTS_DIR = "./fonts"; const DIST_DIR = "./dist"; +const WEB_DIR = DIST_DIR + "/web"; const USERSCRIPT_DIR = DIST_DIR + "/userscript"; const EXTENSION_DIR = DIST_DIR + "/extension"; const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; @@ -31,8 +32,7 @@ const USERSCRIPT_HEADER = SRC_DIR + "/platforms/userscript/header.txt"; const OBSIDIAN_WRAPPER = SRC_DIR + "/platforms/obsidian/wrapper.js"; const VENCORD_WRAPPER = SRC_DIR + "/platforms/vencord/wrapper.js"; -const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; -const BIRB_OUTPUT = DIST_DIR + "/birb.js"; +const TEMP_BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js"; const MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf"; @@ -95,16 +95,16 @@ async function generateCode(entryPoint, embedFont = false) { }); await bundle.write({ - file: BUNDLED_OUTPUT, + file: TEMP_BUNDLED_OUTPUT, format: 'iife', }); await bundle.close(); - let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8'); + let birbJs = readFileSync(TEMP_BUNDLED_OUTPUT, 'utf8'); // Delete bundled file - unlinkSync(BUNDLED_OUTPUT); + unlinkSync(TEMP_BUNDLED_OUTPUT); // Replace version placeholder birbJs = birbJs.replaceAll(VERSION_KEY, version); @@ -132,11 +132,13 @@ async function generateCode(entryPoint, embedFont = false) { async function buildWeb() { const birbJs = await generateCode(WEB_ENTRY); - writeFileSync(BIRB_OUTPUT, birbJs); + mkdirSync(WEB_DIR, { recursive: true }); + writeFileSync(WEB_DIR + '/birb.js', birbJs); } async function buildUserscript() { const birbJs = await generateCode(USERSCRIPT_ENTRY); + // Get userscript header const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version); @@ -212,7 +214,7 @@ async function buildVencord() { vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); // Create exported birb function - writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); + writeFileSync(VENCORD_DIR + '/birb.js', vencordPlugin); } console.log("Starting build..."); diff --git a/dist/extension.zip b/dist/extension.zip index 11750926b8986857311403319b4dd6492be70831..5106474d8fcd805bb6423e1f5cde07fab9820ace 100644 GIT binary patch delta 629 zcmX|;KWI}?6vjEn9QeW~+8VyJx5?zF%S<;~(wCLEST7**Q z;BXNKMGP1aEBNOO9paGoK_?wbQ3xo+p%lSEB(${p-Fx%i^n2fT{@m~0x3t`|wA}Od zWt{5oYVlJQsz3fz9isY!c2(L{I#;B+s(9R|23HBTj<3pT9C^rS!q&@yFx)YG96ImNI0GL*_DG$dL2$HI49KK1SjNChl=_by+`$a z#pvBFx~E6YW;owMq5geG`E-juhK|5F8>J}yq^Ds;K{zt7L#@bZPS4SqGHG zem)!oA6{f%zts9Tj{$q*d>-z|B_1V6{D&*RE01pihaTSm${8*L&$66U%?w|__kM=2 z1Cv<;RCL2|9t{cKG+gyf<%z|vg?HJQD;7U2up_BTIg@5FXBxIGuurd@xA>AbwV?@v U`xb#kf5DhD1-^xWjRH^n2S|z23;+NC delta 629 zcmX|;Pe{{Y7{_^@wl%465FV5^Ap;TS4kkj({b_SLkl18(3Bq=nhagDw=%pHg2p&4T zM28|0I*1DXc@7@tVK(TL2N8pTf*oQ69z;g5)%Shh{dW5NzTfB1`+eWv;!?-rQpd(; zH)S?g+&y{9yxlAGQRdZILCy-)pQen;NBE}dT_@O*T&JrZaB`bIVMcv#P*AOv$Sb#Z zY1-9t1kRQg2IM<|d zP8Fv|b}#6q7P$1V%+~1%q6vf>FQNktiPyz-bO^+Y*2#nN@j3_EF={?ex5d*g)&Zs1 z&COo$^*xU3m)Z~WFz_eLcj1Ocd59qKAI5<%5q<fe#(G3GhH2C7CVIpoS?<{H-k^9D6wm7xG4!_DKOqycCG#puApI*CQaga2%zA=M& Ti;6{e%9zhm{1^jYQ#|$`>axA` diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.js similarity index 100% rename from dist/vencord/birb.export.js rename to dist/vencord/birb.js diff --git a/dist/birb.js b/dist/web/birb.js similarity index 100% rename from dist/birb.js rename to dist/web/birb.js From 37a30ea509d546efd17f5b9c26b0ad270ae1818b Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 11:05:44 -0500 Subject: [PATCH 6/6] Add Vencord save support --- dist/extension.zip | Bin 149002 -> 149044 bytes dist/extension/birb.js | 11 +++++++++-- dist/obsidian/main.js | 7 +++++++ dist/userscript/birb.user.js | 7 +++++++ dist/vencord/birb.js | 25 ++++++++++++++++--------- dist/web/birb.js | 7 +++++++ preview/index.html | 2 +- src/context.js | 31 +++++++++++++++++++++++++++++-- src/platforms/vencord/vencord.js | 4 ++-- src/stylesheet.css | 7 +++++++ 10 files changed, 85 insertions(+), 16 deletions(-) diff --git a/dist/extension.zip b/dist/extension.zip index 5106474d8fcd805bb6423e1f5cde07fab9820ace..a144f70da8e07d7864c244d60cbdcac2fddc74c3 100644 GIT binary patch delta 24716 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.^F(qs!^VS0?j#Cj*0V$Pg{{H& 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 KFmYra0{$OFIB>QA diff --git a/dist/extension/birb.js b/dist/extension/birb.js index 98b5112..e0bbfb6 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -976,9 +976,9 @@ // @ts-expect-error if (chrome.runtime.lastError) { // @ts-expect-error - console.error(chrome.runtime.lastError); + error(chrome.runtime.lastError); } else { - console.log("Settings saved successfully"); + log("Settings saved successfully"); } }); } @@ -1693,6 +1693,13 @@ border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important; diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 21fbfde..dcead4d 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -1736,6 +1736,13 @@ module.exports = class PocketBird extends Plugin { border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important; diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 80f2cf2..1f84ab5 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -1698,6 +1698,13 @@ border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important; diff --git a/dist/vencord/birb.js b/dist/vencord/birb.js index 9680483..d951702 100644 --- a/dist/vencord/birb.js +++ b/dist/vencord/birb.js @@ -950,15 +950,15 @@ export const Birb = () => { } } - class LocalContext extends Context { - + class VencordContext extends Context { + /** * @override * @returns {Promise} */ async getSaveData() { - log("Loading save data from localStorage"); - return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + // @ts-expect-error + return await Vencord.Api.DataStore.get(SAVE_KEY) ?? {}; } /** @@ -966,14 +966,14 @@ export const Birb = () => { * @param {BirbSaveData} saveData */ async putSaveData(saveData) { - log("Saving data to localStorage"); - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + // @ts-expect-error + await Vencord.Api.DataStore.set(SAVE_KEY, saveData); } /** @override */ resetSaveData() { - log("Resetting save data in localStorage"); - localStorage.removeItem(SAVE_KEY); + // @ts-expect-error + Vencord.Api.DataStore.del(SAVE_KEY); } } @@ -1679,6 +1679,13 @@ export const Birb = () => { border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important; @@ -2616,7 +2623,7 @@ export const Birb = () => { }); } - initializeApplication(new LocalContext()); + initializeApplication(new VencordContext()); })(); diff --git a/dist/web/birb.js b/dist/web/birb.js index 4af8e0b..e0c26be 100644 --- a/dist/web/birb.js +++ b/dist/web/birb.js @@ -1678,6 +1678,13 @@ border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important; diff --git a/preview/index.html b/preview/index.html index 7a16e7a..e74e7ea 100644 --- a/preview/index.html +++ b/preview/index.html @@ -26,6 +26,6 @@

- + \ No newline at end of file diff --git a/src/context.js b/src/context.js index d52f166..a135e6f 100644 --- a/src/context.js +++ b/src/context.js @@ -181,9 +181,9 @@ export class BrowserExtensionContext extends Context { // @ts-expect-error if (chrome.runtime.lastError) { // @ts-expect-error - console.error(chrome.runtime.lastError); + error(chrome.runtime.lastError); } else { - console.log("Settings saved successfully"); + log("Settings saved successfully"); } }); } @@ -276,6 +276,33 @@ export class ObsidianContext extends Context { } } +export class VencordContext extends Context { + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + // @ts-expect-error + return await Vencord.Api.DataStore.get(SAVE_KEY) ?? {}; + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + // @ts-expect-error + await Vencord.Api.DataStore.set(SAVE_KEY, saveData); + } + + /** @override */ + resetSaveData() { + // @ts-expect-error + Vencord.Api.DataStore.del(SAVE_KEY); + } +} + /** * Parse URL parameters into a key-value map * @param {string} url diff --git a/src/platforms/vencord/vencord.js b/src/platforms/vencord/vencord.js index 9b0133f..85645ad 100644 --- a/src/platforms/vencord/vencord.js +++ b/src/platforms/vencord/vencord.js @@ -1,4 +1,4 @@ import { initializeApplication } from "../../application.js"; -import { LocalContext } from "../../context.js"; +import { VencordContext } from "../../context.js"; -initializeApplication(new LocalContext()); \ No newline at end of file +initializeApplication(new VencordContext()); \ No newline at end of file diff --git a/src/stylesheet.css b/src/stylesheet.css index 9ccdd24..fd82706 100644 --- a/src/stylesheet.css +++ b/src/stylesheet.css @@ -357,6 +357,13 @@ border: none !important; } +.birb-sticky-note-input::placeholder { + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + background-color: transparent !important; + color: rgba(0, 0, 0, 0.35) !important; +} + .birb-sticky-note-input:focus { outline: none !important; box-shadow: none !important;