mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-25 12:17:22 +00:00
Add browser-specific entry point
This commit is contained in:
3
build.js
3
build.js
@@ -24,7 +24,8 @@ const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
|
|||||||
const VENCORD_DIR = DIST_DIR + "/vencord";
|
const VENCORD_DIR = DIST_DIR + "/vencord";
|
||||||
|
|
||||||
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
|
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 BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
|
||||||
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
|
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
|
||||||
|
|
||||||
|
|||||||
694
dist/birb.js
vendored
694
dist/birb.js
vendored
@@ -1,4 +1,4 @@
|
|||||||
(function () {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Directions = {
|
const Directions = {
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -22,6 +23,17 @@
|
|||||||
debugMode = value;
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -214,6 +226,139 @@
|
|||||||
return document.documentElement.clientHeight;
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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<string, string>}
|
||||||
|
*/
|
||||||
|
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 */
|
/** Indicators for parts of the base bird sprite sheet */
|
||||||
const Sprite = {
|
const Sprite = {
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<string, string>}
|
|
||||||
*/
|
|
||||||
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
|
* @typedef {Object} SavedStickyNote
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -1940,68 +1718,24 @@
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -2062,7 +1796,7 @@
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
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");
|
const styleElement = document.createElement("style");
|
||||||
@@ -2678,16 +2412,11 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
return style.position !== "fixed" && style.position !== "sticky";
|
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2827,8 +2556,99 @@
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
})({});
|
||||||
|
|||||||
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
694
dist/extension/birb.js
vendored
694
dist/extension/birb.js
vendored
@@ -1,4 +1,4 @@
|
|||||||
(function () {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Directions = {
|
const Directions = {
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -22,6 +23,17 @@
|
|||||||
debugMode = value;
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -214,6 +226,139 @@
|
|||||||
return document.documentElement.clientHeight;
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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<string, string>}
|
||||||
|
*/
|
||||||
|
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 */
|
/** Indicators for parts of the base bird sprite sheet */
|
||||||
const Sprite = {
|
const Sprite = {
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<string, string>}
|
|
||||||
*/
|
|
||||||
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
|
* @typedef {Object} SavedStickyNote
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -1940,68 +1718,24 @@
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -2062,7 +1796,7 @@
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
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");
|
const styleElement = document.createElement("style");
|
||||||
@@ -2678,16 +2412,11 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
return style.position !== "fixed" && style.position !== "sticky";
|
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2827,8 +2556,99 @@
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
})({});
|
||||||
|
|||||||
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"description": "It's a pet bird in your browser, what more could you want?",
|
"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",
|
"homepage_url": "https://idreesinc.com",
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "images/icons/transparent/48x48x1.png",
|
"48": "images/icons/transparent/48x48x1.png",
|
||||||
|
|||||||
696
dist/obsidian/main.js
vendored
696
dist/obsidian/main.js
vendored
@@ -1,9 +1,9 @@
|
|||||||
const { Plugin, Notice } = require('obsidian');
|
const { Plugin, Notice } = require('obsidian');
|
||||||
module.exports = class PocketBird extends Plugin {
|
module.exports = class PocketBird extends Plugin {
|
||||||
onload() {
|
onload() {
|
||||||
console.log("Loading Pocket Bird version 2025.11.15...");
|
console.log("Loading Pocket Bird version 2025.11.16...");
|
||||||
const OBSIDIAN_PLUGIN = this;
|
const OBSIDIAN_PLUGIN = this;
|
||||||
(function () {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Directions = {
|
const Directions = {
|
||||||
@@ -12,6 +12,7 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -27,6 +28,17 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
debugMode = value;
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -219,6 +231,139 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
return document.documentElement.clientHeight;
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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<string, string>}
|
||||||
|
*/
|
||||||
|
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 */
|
/** Indicators for parts of the base bird sprite sheet */
|
||||||
const Sprite = {
|
const Sprite = {
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<string, string>}
|
|
||||||
*/
|
|
||||||
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
|
* @typedef {Object} SavedStickyNote
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -1945,68 +1723,24 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -2067,7 +1801,7 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
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");
|
const styleElement = document.createElement("style");
|
||||||
@@ -2683,16 +2417,11 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
return style.position !== "fixed" && style.position !== "sticky";
|
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2832,11 +2561,102 @@ module.exports = class PocketBird extends Plugin {
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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!");
|
console.log("Pocket Bird loaded!");
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/obsidian/manifest.json
vendored
2
dist/obsidian/manifest.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "pocket-bird",
|
"id": "pocket-bird",
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"version": "2025.11.15",
|
"version": "2025.11.16",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Add a pet bird to fly around your notes and keep you company!",
|
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||||
"author": "Idrees Hassan",
|
"author": "Idrees Hassan",
|
||||||
|
|||||||
696
dist/userscript/birb.user.js
vendored
696
dist/userscript/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Pocket Bird
|
// @name Pocket Bird
|
||||||
// @namespace https://idreesinc.com
|
// @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?
|
// @description It's a pet bird in your browser, what more could you want?
|
||||||
// @author Idrees
|
// @author Idrees
|
||||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
|
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// @grant GM_deleteValue
|
// @grant GM_deleteValue
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
(function () {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Directions = {
|
const Directions = {
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -36,6 +37,17 @@
|
|||||||
debugMode = value;
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -228,6 +240,139 @@
|
|||||||
return document.documentElement.clientHeight;
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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<string, string>}
|
||||||
|
*/
|
||||||
|
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 */
|
/** Indicators for parts of the base bird sprite sheet */
|
||||||
const Sprite = {
|
const Sprite = {
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<string, string>}
|
|
||||||
*/
|
|
||||||
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
|
* @typedef {Object} SavedStickyNote
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -1954,68 +1732,24 @@
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -2076,7 +1810,7 @@
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
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");
|
const styleElement = document.createElement("style");
|
||||||
@@ -2692,16 +2426,11 @@
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
return style.position !== "fixed" && style.position !== "sticky";
|
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2841,8 +2570,99 @@
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
})({});
|
||||||
|
|||||||
694
dist/vencord/birb.export.js
vendored
694
dist/vencord/birb.export.js
vendored
@@ -1,5 +1,5 @@
|
|||||||
export const Birb = () => {
|
export const Birb = () => {
|
||||||
(function () {
|
(function (exports) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const Directions = {
|
const Directions = {
|
||||||
@@ -8,6 +8,7 @@ export const Birb = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -23,6 +24,17 @@ export const Birb = () => {
|
|||||||
debugMode = value;
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -215,6 +227,139 @@ export const Birb = () => {
|
|||||||
return document.documentElement.clientHeight;
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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<string, string>}
|
||||||
|
*/
|
||||||
|
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 */
|
/** Indicators for parts of the base bird sprite sheet */
|
||||||
const Sprite = {
|
const Sprite = {
|
||||||
THEME_HIGHLIGHT: "theme-highlight",
|
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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<string, string>}
|
|
||||||
*/
|
|
||||||
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
|
* @typedef {Object} SavedStickyNote
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
@@ -1941,68 +1719,24 @@ export const Birb = () => {
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -2063,7 +1797,7 @@ export const Birb = () => {
|
|||||||
insertModal(`${birdBirb()} Mode`, message);
|
insertModal(`${birdBirb()} Mode`, message);
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
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");
|
const styleElement = document.createElement("style");
|
||||||
@@ -2679,16 +2413,11 @@ export const Birb = () => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const style = window.getComputedStyle(el);
|
|
||||||
return style.position !== "fixed" && style.position !== "sticky";
|
|
||||||
});
|
});
|
||||||
if (nonFixedElements.length === 0) {
|
if (nonFixedElements.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2828,10 +2557,101 @@ export const Birb = () => {
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
})({});
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -2,9 +2,11 @@ import Frame from './frame.js';
|
|||||||
import Layer from './layer.js';
|
import Layer from './layer.js';
|
||||||
import Anim from './anim.js';
|
import Anim from './anim.js';
|
||||||
import { Birb, Animations } from './birb.js';
|
import { Birb, Animations } from './birb.js';
|
||||||
import { getContext, ObsidianContext } from './context.js';
|
import { Context, ObsidianContext } from './context.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getContext,
|
||||||
|
setContext,
|
||||||
Directions,
|
Directions,
|
||||||
isDebug,
|
isDebug,
|
||||||
setDebug,
|
setDebug,
|
||||||
@@ -109,68 +111,24 @@ const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
|||||||
/** @type {Partial<Settings>} */
|
/** @type {Partial<Settings>} */
|
||||||
let userSettings = {};
|
let userSettings = {};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the sprite sheet and return the pixel-map template
|
* @param {Context} context
|
||||||
* @param {string} dataUri
|
|
||||||
* @param {boolean} [templateColors]
|
|
||||||
* @returns {Promise<string[][]>}
|
|
||||||
*/
|
*/
|
||||||
function loadSpriteSheetPixels(dataUri, templateColors = true) {
|
export async function initializeApplication(context) {
|
||||||
return new Promise((resolve, reject) => {
|
log("birbOS booting up...");
|
||||||
const img = new Image();
|
setContext(context);
|
||||||
img.src = dataUri;
|
log("Loading sprite sheets...");
|
||||||
img.onload = () => {
|
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||||
const canvas = document.createElement('canvas');
|
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||||
canvas.width = img.width;
|
startApplication(birbPixels, featherPixels);
|
||||||
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...");
|
/**
|
||||||
|
* @param {string[][]} birbPixels
|
||||||
Promise.all([
|
* @param {string[][]} featherPixels
|
||||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
*/
|
||||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
function startApplication(birbPixels, featherPixels) {
|
||||||
]).then(([birbPixels, featherPixels]) => {
|
|
||||||
|
|
||||||
const SPRITE_SHEET = birbPixels;
|
const SPRITE_SHEET = birbPixels;
|
||||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||||
@@ -851,10 +809,11 @@ Promise.all([
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
// @ts-expect-error
|
|
||||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
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
|
// 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) => {
|
const nonFixedElements = largeElements.filter((el) => {
|
||||||
if (fixedAllowed) {
|
if (fixedAllowed) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1008,6 +967,60 @@ Promise.all([
|
|||||||
// Run the birb
|
// Run the birb
|
||||||
init();
|
init();
|
||||||
draw();
|
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<string[][]>}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
100
src/context.js
100
src/context.js
@@ -1,6 +1,6 @@
|
|||||||
import { debug, log, error } from "./shared.js";
|
import { debug, log, error } from "./shared.js";
|
||||||
|
|
||||||
const SAVE_KEY = "birbSaveData";
|
export const SAVE_KEY = "birbSaveData";
|
||||||
const ROOT_PATH = "";
|
const ROOT_PATH = "";
|
||||||
const SET_CONTEXT = "__CONTEXT__"
|
const SET_CONTEXT = "__CONTEXT__"
|
||||||
|
|
||||||
@@ -17,9 +17,9 @@ export class Context {
|
|||||||
* @abstract
|
* @abstract
|
||||||
* @returns {boolean} Whether this context is applicable
|
* @returns {boolean} Whether this context is applicable
|
||||||
*/
|
*/
|
||||||
isContextActive() {
|
// isContextActive() {
|
||||||
throw new Error("Method not implemented");
|
// throw new Error("Method not implemented");
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @abstract
|
* @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<BirbSaveData|{}>}
|
|
||||||
*/
|
|
||||||
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 {
|
export class UserScriptContext extends Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
// @ts-expect-error
|
|
||||||
return typeof GM_getValue === "function";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -183,15 +137,6 @@ export class UserScriptContext extends Context {
|
|||||||
|
|
||||||
class BrowserExtensionContext 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
|
* @override
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -234,15 +179,6 @@ class BrowserExtensionContext extends Context {
|
|||||||
|
|
||||||
export class ObsidianContext extends Context {
|
export class ObsidianContext extends Context {
|
||||||
|
|
||||||
/**
|
|
||||||
* @override
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isContextActive() {
|
|
||||||
// @ts-expect-error
|
|
||||||
return typeof app !== "undefined" && typeof app.vault !== "undefined";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
* @returns {Promise<BirbSaveData|{}>}
|
* @returns {Promise<BirbSaveData|{}>}
|
||||||
@@ -325,11 +261,10 @@ const contextProcessingOrder = [
|
|||||||
new UserScriptContext(),
|
new UserScriptContext(),
|
||||||
new ObsidianContext(),
|
new ObsidianContext(),
|
||||||
new BrowserExtensionContext(),
|
new BrowserExtensionContext(),
|
||||||
new LocalContext()
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const CONTEXTS_BY_KEY = {
|
const CONTEXTS_BY_KEY = {
|
||||||
"local": LocalContext,
|
// "local": LocalContext,
|
||||||
"userscript": UserScriptContext,
|
"userscript": UserScriptContext,
|
||||||
"browser-extension": BrowserExtensionContext,
|
"browser-extension": BrowserExtensionContext,
|
||||||
"obsidian": ObsidianContext
|
"obsidian": ObsidianContext
|
||||||
@@ -339,18 +274,19 @@ const CONTEXTS_BY_KEY = {
|
|||||||
* Determines and returns the current context
|
* Determines and returns the current context
|
||||||
* @returns {Context}
|
* @returns {Context}
|
||||||
*/
|
*/
|
||||||
export function getContext() {
|
// export function getContext() {
|
||||||
if (CONTEXTS_BY_KEY[SET_CONTEXT]) {
|
// if (CONTEXTS_BY_KEY[SET_CONTEXT]) {
|
||||||
return new CONTEXTS_BY_KEY[SET_CONTEXT]();
|
// return new CONTEXTS_BY_KEY[SET_CONTEXT]();
|
||||||
}
|
// }
|
||||||
for (const context of contextProcessingOrder) {
|
// for (const context of contextProcessingOrder) {
|
||||||
if (context.isContextActive()) {
|
// if (context.isContextActive()) {
|
||||||
return context;
|
// return context;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
error("No applicable context found, defaulting to LocalContext");
|
// error("No applicable context found");
|
||||||
return new LocalContext();
|
// // return new LocalContext();
|
||||||
}
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse URL parameters into a key-value map
|
* Parse URL parameters into a key-value map
|
||||||
|
|||||||
36
src/platforms/browser.js
Normal file
36
src/platforms/browser.js
Normal file
@@ -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<BirbSaveData|{}>}
|
||||||
|
*/
|
||||||
|
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());
|
||||||
@@ -4,6 +4,7 @@ export const Directions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let debugMode = location.hostname === "127.0.0.1";
|
let debugMode = location.hostname === "127.0.0.1";
|
||||||
|
let context = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} Whether debug mode is enabled
|
* @returns {boolean} Whether debug mode is enabled
|
||||||
@@ -19,6 +20,17 @@ export function setDebug(value) {
|
|||||||
debugMode = 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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
getContext,
|
||||||
makeElement,
|
makeElement,
|
||||||
makeDraggable,
|
makeDraggable,
|
||||||
makeClosable
|
makeClosable
|
||||||
} from './shared.js';
|
} from './shared.js';
|
||||||
import { getContext } from './context.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} SavedStickyNote
|
* @typedef {Object} SavedStickyNote
|
||||||
|
|||||||
Reference in New Issue
Block a user