mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
/node_modules
|
||||
.DS_Store
|
||||
/dist/birb.bundled.js
|
||||
obsidian-test.sh
|
||||
build-cache.json
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Pocket Bird",
|
||||
"description": "It's a pet bird in your browser, what more could you want?",
|
||||
"version": "2025.11.3.10",
|
||||
"version": "__VERSION__",
|
||||
"homepage_url": "https://idreesinc.com",
|
||||
"icons": {
|
||||
"48": "images/icons/transparent/48x48x1.png",
|
||||
|
||||
194
build.js
194
build.js
@@ -4,55 +4,104 @@ import { rollup } from 'rollup';
|
||||
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, cpSync, createWriteStream } from 'fs';
|
||||
import archiver from 'archiver';
|
||||
|
||||
// Path constants
|
||||
const BUILD_CACHE_PATH = "./build-cache.json";
|
||||
const SRC_DIR = "./src";
|
||||
const SPRITES_DIR = "./sprites";
|
||||
const IMAGES_DIR = "./images";
|
||||
const FONTS_DIR = "./fonts";
|
||||
const DIST_DIR = "./dist";
|
||||
|
||||
const BROWSER_MANIFEST = "./browser-manifest.json";
|
||||
const OBSIDIAN_MANIFEST = "./obsidian-manifest.json";
|
||||
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
|
||||
const EXTENSION_DIR = DIST_DIR + "/extension";
|
||||
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
|
||||
|
||||
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
|
||||
const APPLICATION_ENTRY = SRC_DIR + "/application.js";
|
||||
const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
|
||||
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
|
||||
|
||||
const VERSION_KEY = "__VERSION__";
|
||||
const STYLESHEET_KEY = "___STYLESHEET___";
|
||||
|
||||
const spriteSheets = [
|
||||
{
|
||||
key: "__SPRITE_SHEET__",
|
||||
path: "./sprites/birb.png"
|
||||
path: SPRITES_DIR + "/birb.png"
|
||||
},
|
||||
{
|
||||
key: "__FEATHER_SPRITE_SHEET__",
|
||||
path: "./sprites/feather.png"
|
||||
path: SPRITES_DIR + "/feather.png"
|
||||
}
|
||||
];
|
||||
|
||||
const STYLESHEET_PATH = "./src/stylesheet.css";
|
||||
const STYLESHEET_KEY = "___STYLESHEET___";
|
||||
const BROWSER_MANIFEST = "./browser-manifest.json";
|
||||
/** @type {Record<string, any>} */
|
||||
let buildCache = {};
|
||||
try {
|
||||
const cacheContent = readFileSync(BUILD_CACHE_PATH, 'utf8');
|
||||
buildCache = JSON.parse(cacheContent);
|
||||
} catch (e) {
|
||||
console.warn("No build cache found, starting fresh");
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const versionDate = `${now.getFullYear()}.${now.getMonth() + 1}.${now.getDate()}`;
|
||||
|
||||
// Get current build number from the browser-manifest.json
|
||||
// Get current build number from the build cache
|
||||
let buildNumber = 0;
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(BROWSER_MANIFEST, 'utf8'));
|
||||
if (manifest.version) {
|
||||
if (manifest.version.startsWith(versionDate)) {
|
||||
// Same day, increment build number
|
||||
const parts = manifest.version.split('.');
|
||||
if (parts.length === 4) {
|
||||
buildNumber = parseInt(parts[3], 10) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (buildCache.version && buildCache.version.startsWith(versionDate)) {
|
||||
// Same day, increment build number
|
||||
const parts = buildCache.version.split('.');
|
||||
if (parts.length === 4) {
|
||||
buildNumber = parseInt(parts[3], 10) + 1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not read version from browser manifest");
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Update manifest.json with new version
|
||||
const version = `${versionDate}.${buildNumber}`;
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(BROWSER_MANIFEST, 'utf8'));
|
||||
manifest.version = version;
|
||||
writeFileSync(BROWSER_MANIFEST, JSON.stringify(manifest, null, 4), 'utf8');
|
||||
} catch (e) {
|
||||
console.error("Could not update version in browser manifest");
|
||||
throw e;
|
||||
|
||||
// Update build cache
|
||||
buildCache.version = version;
|
||||
writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
|
||||
|
||||
// Bundle with rollup
|
||||
const bundle = await rollup({
|
||||
input: APPLICATION_ENTRY,
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
file: BUNDLED_OUTPUT,
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
await bundle.close();
|
||||
|
||||
let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8');
|
||||
|
||||
// Delete bundled file
|
||||
unlinkSync(BUNDLED_OUTPUT);
|
||||
|
||||
// Replace version placeholder
|
||||
birbJs = birbJs.replaceAll(VERSION_KEY, version);
|
||||
|
||||
// Compile and insert sprite sheets
|
||||
for (const spriteSheet of spriteSheets) {
|
||||
const dataUri = readFileSync(spriteSheet.path, 'base64');
|
||||
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
|
||||
}
|
||||
|
||||
// Insert stylesheet
|
||||
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
|
||||
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
|
||||
|
||||
// Build standard javascript file
|
||||
writeFileSync(BIRB_OUTPUT, birbJs);
|
||||
|
||||
// Build user script
|
||||
const userScriptHeader =
|
||||
`// ==UserScript==
|
||||
`// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
// @namespace https://idreesinc.com
|
||||
// @version ${version}
|
||||
@@ -67,65 +116,31 @@ const userScriptHeader =
|
||||
// ==/UserScript==
|
||||
|
||||
`;
|
||||
|
||||
// Bundle with rollup
|
||||
const bundle = await rollup({
|
||||
input: 'src/application.js',
|
||||
});
|
||||
|
||||
await bundle.write({
|
||||
file: 'dist/birb.bundled.js',
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
await bundle.close();
|
||||
|
||||
let birbJs = readFileSync('dist/birb.bundled.js', 'utf8');
|
||||
|
||||
// Delete bundled file
|
||||
unlinkSync('./dist/birb.bundled.js');
|
||||
|
||||
// Replace version placeholder
|
||||
birbJs = birbJs.replaceAll('__VERSION__', version);
|
||||
|
||||
// Compile and insert sprite sheets
|
||||
for (const spriteSheet of spriteSheets) {
|
||||
const dataUri = readFileSync(spriteSheet.path, 'base64');
|
||||
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
|
||||
}
|
||||
|
||||
// Insert stylesheet
|
||||
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
|
||||
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
|
||||
|
||||
// Build standard javascript file
|
||||
writeFileSync('./dist/birb.js', birbJs);
|
||||
|
||||
// Build user script
|
||||
mkdirSync('./dist/userscript', { recursive: true });
|
||||
mkdirSync(USERSCRIPT_DIR, { recursive: true });
|
||||
const userScript = userScriptHeader + birbJs;
|
||||
writeFileSync('./dist/userscript/birb.user.js', userScript);
|
||||
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
|
||||
|
||||
// Build browser extension
|
||||
mkdirSync('./dist/extension', { recursive: true });
|
||||
mkdirSync(EXTENSION_DIR, { recursive: true });
|
||||
|
||||
// Copy birb.js
|
||||
writeFileSync('./dist/extension/birb.js', birbJs);
|
||||
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
|
||||
|
||||
// Copy manifest.json
|
||||
const manifestContent = readFileSync(BROWSER_MANIFEST, 'utf8');
|
||||
writeFileSync('./dist/extension/manifest.json', manifestContent);
|
||||
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
|
||||
browserManifest = browserManifest.replace(VERSION_KEY, version);
|
||||
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
|
||||
|
||||
// Copy icons folder
|
||||
mkdirSync('./dist/extension/images/icons', { recursive: true });
|
||||
cpSync('./images/icons/transparent', './dist/extension/images/icons/transparent', { recursive: true });
|
||||
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
|
||||
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
|
||||
|
||||
// Copy fonts folder
|
||||
mkdirSync('./dist/extension/fonts', { recursive: true });
|
||||
cpSync('./fonts', './dist/extension/fonts', { recursive: true });
|
||||
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
|
||||
// Compress extension folder into zip
|
||||
const output = createWriteStream('./dist/extension.zip');
|
||||
const output = createWriteStream(DIST_DIR + "/extension.zip");
|
||||
const archive = archiver('zip');
|
||||
|
||||
output.on('close', () => {
|
||||
@@ -137,7 +152,36 @@ archive.on('error', (err) => {
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.directory('./dist/extension/', false);
|
||||
archive.directory(EXTENSION_DIR + '/', false);
|
||||
archive.finalize();
|
||||
|
||||
// Build Obsidian plugin
|
||||
mkdirSync(OBSIDIAN_DIR, { recursive: true });
|
||||
|
||||
// Wrap birb.js with plugin boilerplate
|
||||
const obsidianPlugin = `
|
||||
const { Plugin, Notice } = require('obsidian');
|
||||
module.exports = class PocketBird extends Plugin {
|
||||
onload() {
|
||||
console.log("Loading Pocket Bird version ${version}...");
|
||||
const OBSIDIAN_PLUGIN = this;
|
||||
${birbJs}
|
||||
console.log("Pocket Bird loaded!");
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Remove the birb when the plugin is unloaded
|
||||
document.getElementById('birb')?.remove();
|
||||
console.log('Pocket Bird unloaded!');
|
||||
}
|
||||
};`
|
||||
|
||||
// Create main.js with plugin code
|
||||
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
|
||||
|
||||
// Copy manifest.json
|
||||
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
|
||||
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
|
||||
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
|
||||
|
||||
console.log(`Build complete: ${version}`);
|
||||
187
dist/birb.js
vendored
187
dist/birb.js
vendored
@@ -880,6 +880,55 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -1012,8 +1061,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.loadData() ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {BirbSaveData|{}} saveData
|
||||
*/
|
||||
async putSaveData(saveData) {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.saveData(saveData);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
resetSaveData() {
|
||||
this.putSaveData({});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusElementTopMargin() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusableElements() {
|
||||
const elements = [
|
||||
".workspace-leaf",
|
||||
".cm-callout",
|
||||
".HyperMD-codeblock-begin"
|
||||
];
|
||||
return super.getFocusableElements().concat(elements);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
areStickyNotesEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
@@ -1025,7 +1130,22 @@
|
||||
}
|
||||
}
|
||||
error("No applicable context found, defaulting to LocalContext");
|
||||
return CONTEXTS[0];
|
||||
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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1054,46 +1174,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @returns {boolean} Whether the given sticky note is applicable to the current site/page
|
||||
*/
|
||||
function isStickyNoteApplicable(stickyNote) {
|
||||
const stickyNoteUrl = stickyNote.site;
|
||||
const currentUrl = window.location.href;
|
||||
const stickyNoteWebsite = stickyNoteUrl.split("?")[0];
|
||||
const currentWebsite = currentUrl.split("?")[0];
|
||||
|
||||
if (stickyNoteWebsite !== currentWebsite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stickyNoteParams = parseUrlParams(stickyNoteUrl);
|
||||
const currentParams = parseUrlParams(currentUrl);
|
||||
|
||||
if (window.location.hostname === "www.youtube.com") {
|
||||
if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @param {() => void} onSave
|
||||
@@ -1176,8 +1256,9 @@
|
||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||
existingNotes.forEach(note => note.remove());
|
||||
// Render all sticky notes
|
||||
const context = getContext();
|
||||
for (let stickyNote of stickyNotes) {
|
||||
if (isStickyNoteApplicable(stickyNote)) {
|
||||
if (context.isPathApplicable(stickyNote.site)) {
|
||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
}
|
||||
}
|
||||
@@ -1190,7 +1271,7 @@
|
||||
*/
|
||||
function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
||||
const id = Date.now().toString();
|
||||
const site = window.location.href;
|
||||
const site = getContext().getPath();
|
||||
const stickyNote = new StickyNote(id, site, "");
|
||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
||||
@@ -1740,7 +1821,6 @@
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -1830,7 +1910,9 @@
|
||||
const menuItems = [
|
||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||
new MenuItem("Field Guide", insertFieldGuide),
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)),
|
||||
...(getContext().areStickyNotesEnabled() ? [
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote))
|
||||
] : []),
|
||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||
frozen = !frozen;
|
||||
@@ -1867,7 +1949,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2025.11.3.10", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.3.10"); }, false),
|
||||
new MenuItem("2025.11.13.27", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.13.27"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -2062,12 +2144,12 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
setInterval(() => {
|
||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||
if (currentUrl !== lastUrl) {
|
||||
log("URL changed, updating sticky notes");
|
||||
lastUrl = currentUrl;
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed, updating sticky notes");
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
@@ -2494,7 +2576,8 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
const elements = document.querySelectorAll("img, video, .birb-sticky-note");
|
||||
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
const inWindow = Array.from(elements).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight();
|
||||
|
||||
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
187
dist/extension/birb.js
vendored
187
dist/extension/birb.js
vendored
@@ -880,6 +880,55 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -1012,8 +1061,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.loadData() ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {BirbSaveData|{}} saveData
|
||||
*/
|
||||
async putSaveData(saveData) {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.saveData(saveData);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
resetSaveData() {
|
||||
this.putSaveData({});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusElementTopMargin() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusableElements() {
|
||||
const elements = [
|
||||
".workspace-leaf",
|
||||
".cm-callout",
|
||||
".HyperMD-codeblock-begin"
|
||||
];
|
||||
return super.getFocusableElements().concat(elements);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
areStickyNotesEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
@@ -1025,7 +1130,22 @@
|
||||
}
|
||||
}
|
||||
error("No applicable context found, defaulting to LocalContext");
|
||||
return CONTEXTS[0];
|
||||
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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1054,46 +1174,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @returns {boolean} Whether the given sticky note is applicable to the current site/page
|
||||
*/
|
||||
function isStickyNoteApplicable(stickyNote) {
|
||||
const stickyNoteUrl = stickyNote.site;
|
||||
const currentUrl = window.location.href;
|
||||
const stickyNoteWebsite = stickyNoteUrl.split("?")[0];
|
||||
const currentWebsite = currentUrl.split("?")[0];
|
||||
|
||||
if (stickyNoteWebsite !== currentWebsite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stickyNoteParams = parseUrlParams(stickyNoteUrl);
|
||||
const currentParams = parseUrlParams(currentUrl);
|
||||
|
||||
if (window.location.hostname === "www.youtube.com") {
|
||||
if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @param {() => void} onSave
|
||||
@@ -1176,8 +1256,9 @@
|
||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||
existingNotes.forEach(note => note.remove());
|
||||
// Render all sticky notes
|
||||
const context = getContext();
|
||||
for (let stickyNote of stickyNotes) {
|
||||
if (isStickyNoteApplicable(stickyNote)) {
|
||||
if (context.isPathApplicable(stickyNote.site)) {
|
||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
}
|
||||
}
|
||||
@@ -1190,7 +1271,7 @@
|
||||
*/
|
||||
function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
||||
const id = Date.now().toString();
|
||||
const site = window.location.href;
|
||||
const site = getContext().getPath();
|
||||
const stickyNote = new StickyNote(id, site, "");
|
||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
||||
@@ -1740,7 +1821,6 @@
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -1830,7 +1910,9 @@
|
||||
const menuItems = [
|
||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||
new MenuItem("Field Guide", insertFieldGuide),
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)),
|
||||
...(getContext().areStickyNotesEnabled() ? [
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote))
|
||||
] : []),
|
||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||
frozen = !frozen;
|
||||
@@ -1867,7 +1949,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2025.11.3.10", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.3.10"); }, false),
|
||||
new MenuItem("2025.11.13.27", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.13.27"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -2062,12 +2144,12 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
setInterval(() => {
|
||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||
if (currentUrl !== lastUrl) {
|
||||
log("URL changed, updating sticky notes");
|
||||
lastUrl = currentUrl;
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed, updating sticky notes");
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
@@ -2494,7 +2576,8 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
const elements = document.querySelectorAll("img, video, .birb-sticky-note");
|
||||
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
const inWindow = Array.from(elements).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight();
|
||||
|
||||
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "Pocket Bird",
|
||||
"description": "It's a pet bird in your browser, what more could you want?",
|
||||
"version": "2025.11.3.10",
|
||||
"version": "2025.11.13.27",
|
||||
"homepage_url": "https://idreesinc.com",
|
||||
"icons": {
|
||||
"48": "images/icons/transparent/48x48x1.png",
|
||||
|
||||
2758
dist/obsidian/main.js
vendored
Normal file
2758
dist/obsidian/main.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
10
dist/obsidian/manifest.json
vendored
Normal file
10
dist/obsidian/manifest.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "pocket-bird",
|
||||
"name": "Pocket Bird",
|
||||
"version": "2025.11.13.27",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "It's a pet bird in your Obsidian, what more could you want?",
|
||||
"author": "Idrees Hassan",
|
||||
"authorUrl": "https://idreesinc.com",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
189
dist/userscript/birb.user.js
vendored
189
dist/userscript/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
// @namespace https://idreesinc.com
|
||||
// @version 2025.11.3.10
|
||||
// @version 2025.11.13.27
|
||||
// @description It's a bird that hops around your web browser, the future is here
|
||||
// @author Idrees
|
||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
|
||||
@@ -894,6 +894,55 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
@@ -1026,8 +1075,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.loadData() ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {BirbSaveData|{}} saveData
|
||||
*/
|
||||
async putSaveData(saveData) {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.saveData(saveData);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
resetSaveData() {
|
||||
this.putSaveData({});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusElementTopMargin() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusableElements() {
|
||||
const elements = [
|
||||
".workspace-leaf",
|
||||
".cm-callout",
|
||||
".HyperMD-codeblock-begin"
|
||||
];
|
||||
return super.getFocusableElements().concat(elements);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
areStickyNotesEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
@@ -1039,7 +1144,22 @@
|
||||
}
|
||||
}
|
||||
error("No applicable context found, defaulting to LocalContext");
|
||||
return CONTEXTS[0];
|
||||
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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1068,46 +1188,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @returns {boolean} Whether the given sticky note is applicable to the current site/page
|
||||
*/
|
||||
function isStickyNoteApplicable(stickyNote) {
|
||||
const stickyNoteUrl = stickyNote.site;
|
||||
const currentUrl = window.location.href;
|
||||
const stickyNoteWebsite = stickyNoteUrl.split("?")[0];
|
||||
const currentWebsite = currentUrl.split("?")[0];
|
||||
|
||||
if (stickyNoteWebsite !== currentWebsite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stickyNoteParams = parseUrlParams(stickyNoteUrl);
|
||||
const currentParams = parseUrlParams(currentUrl);
|
||||
|
||||
if (window.location.hostname === "www.youtube.com") {
|
||||
if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @param {() => void} onSave
|
||||
@@ -1190,8 +1270,9 @@
|
||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||
existingNotes.forEach(note => note.remove());
|
||||
// Render all sticky notes
|
||||
const context = getContext();
|
||||
for (let stickyNote of stickyNotes) {
|
||||
if (isStickyNoteApplicable(stickyNote)) {
|
||||
if (context.isPathApplicable(stickyNote.site)) {
|
||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
}
|
||||
}
|
||||
@@ -1204,7 +1285,7 @@
|
||||
*/
|
||||
function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
||||
const id = Date.now().toString();
|
||||
const site = window.location.href;
|
||||
const site = getContext().getPath();
|
||||
const stickyNote = new StickyNote(id, site, "");
|
||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
||||
@@ -1754,7 +1835,6 @@
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -1844,7 +1924,9 @@
|
||||
const menuItems = [
|
||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||
new MenuItem("Field Guide", insertFieldGuide),
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)),
|
||||
...(getContext().areStickyNotesEnabled() ? [
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote))
|
||||
] : []),
|
||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||
frozen = !frozen;
|
||||
@@ -1881,7 +1963,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2025.11.3.10", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.3.10"); }, false),
|
||||
new MenuItem("2025.11.13.27", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.13.27"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -2076,12 +2158,12 @@
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
setInterval(() => {
|
||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||
if (currentUrl !== lastUrl) {
|
||||
log("URL changed, updating sticky notes");
|
||||
lastUrl = currentUrl;
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed, updating sticky notes");
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
@@ -2508,7 +2590,8 @@
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
const elements = document.querySelectorAll("img, video, .birb-sticky-note");
|
||||
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
const inWindow = Array.from(elements).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight();
|
||||
|
||||
10
obsidian-manifest.json
Normal file
10
obsidian-manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "pocket-bird",
|
||||
"name": "Pocket Bird",
|
||||
"version": "__VERSION__",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "It's a pet bird in your Obsidian, what more could you want?",
|
||||
"author": "Idrees Hassan",
|
||||
"authorUrl": "https://idreesinc.com",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev": "nodemon --watch src --watch stylesheet.css --watch build.js --exec \"npm run build\""
|
||||
"dev": "nodemon --watch src --watch package.json --watch stylesheet.css --watch build.js --watch obsidian-manifest.json --watch browser-manifest.json --exec \"npm run build\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
|
||||
@@ -104,7 +104,6 @@ const PET_FEATHER_BOOST = 2;
|
||||
|
||||
// Focus element constraints
|
||||
const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
const MIN_FOCUS_ELEMENT_TOP = 80;
|
||||
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
@@ -194,7 +193,9 @@ Promise.all([
|
||||
const menuItems = [
|
||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||
new MenuItem("Field Guide", insertFieldGuide),
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote)),
|
||||
...(getContext().areStickyNotesEnabled() ? [
|
||||
new MenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote))
|
||||
] : []),
|
||||
new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)),
|
||||
new DebugMenuItem("Freeze/Unfreeze", () => {
|
||||
frozen = !frozen;
|
||||
@@ -426,12 +427,12 @@ Promise.all([
|
||||
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
|
||||
let lastUrl = (window.location.href ?? "").split("?")[0];
|
||||
let lastPath = getContext().getPath().split("?")[0];
|
||||
setInterval(() => {
|
||||
const currentUrl = (window.location.href ?? "").split("?")[0];
|
||||
if (currentUrl !== lastUrl) {
|
||||
log("URL changed, updating sticky notes");
|
||||
lastUrl = currentUrl;
|
||||
const currentPath = getContext().getPath().split("?")[0];
|
||||
if (currentPath !== lastPath) {
|
||||
log("Path changed, updating sticky notes");
|
||||
lastPath = currentPath;
|
||||
drawStickyNotes(stickyNotes, save, deleteStickyNote);
|
||||
}
|
||||
}, URL_CHECK_INTERVAL);
|
||||
@@ -862,7 +863,8 @@ Promise.all([
|
||||
if (frozen) {
|
||||
return false;
|
||||
}
|
||||
const elements = document.querySelectorAll("img, video, .birb-sticky-note");
|
||||
const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin();
|
||||
const elements = document.querySelectorAll(getContext().getFocusableElements().join(", "));
|
||||
const inWindow = Array.from(elements).filter((img) => {
|
||||
const rect = img.getBoundingClientRect();
|
||||
return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight();
|
||||
|
||||
123
src/context.js
123
src/context.js
@@ -1,4 +1,3 @@
|
||||
|
||||
import { debug, log, error } from "./shared.js";
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
@@ -42,6 +41,55 @@ export class Context {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalContext extends Context {
|
||||
@@ -174,8 +222,64 @@ class BrowserExtensionContext extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.loadData() ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {BirbSaveData|{}} saveData
|
||||
*/
|
||||
async putSaveData(saveData) {
|
||||
// @ts-expect-error
|
||||
return await OBSIDIAN_PLUGIN.saveData(saveData);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
resetSaveData() {
|
||||
this.putSaveData({});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusElementTopMargin() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getFocusableElements() {
|
||||
const elements = [
|
||||
".workspace-leaf",
|
||||
".cm-callout",
|
||||
".HyperMD-codeblock-begin"
|
||||
];
|
||||
return super.getFocusableElements().concat(elements);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
areStickyNotesEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
@@ -187,5 +291,20 @@ export function getContext() {
|
||||
}
|
||||
}
|
||||
error("No applicable context found, defaulting to LocalContext");
|
||||
return CONTEXTS[0];
|
||||
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 };
|
||||
}, {});
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
makeDraggable,
|
||||
makeClosable
|
||||
} from './shared.js';
|
||||
import { getContext } from './context.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedStickyNote
|
||||
@@ -30,46 +31,6 @@ export class StickyNote {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse URL parameters into a key-value map
|
||||
* @param {string} url
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export 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 };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @returns {boolean} Whether the given sticky note is applicable to the current site/page
|
||||
*/
|
||||
export function isStickyNoteApplicable(stickyNote) {
|
||||
const stickyNoteUrl = stickyNote.site;
|
||||
const currentUrl = window.location.href;
|
||||
const stickyNoteWebsite = stickyNoteUrl.split("?")[0];
|
||||
const currentWebsite = currentUrl.split("?")[0];
|
||||
|
||||
if (stickyNoteWebsite !== currentWebsite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stickyNoteParams = parseUrlParams(stickyNoteUrl);
|
||||
const currentParams = parseUrlParams(currentUrl);
|
||||
|
||||
if (window.location.hostname === "www.youtube.com") {
|
||||
if (currentParams.v !== undefined && currentParams.v !== stickyNoteParams.v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {StickyNote} stickyNote
|
||||
* @param {() => void} onSave
|
||||
@@ -152,8 +113,9 @@ export function drawStickyNotes(stickyNotes, onSave, onDelete) {
|
||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
||||
existingNotes.forEach(note => note.remove());
|
||||
// Render all sticky notes
|
||||
const context = getContext();
|
||||
for (let stickyNote of stickyNotes) {
|
||||
if (isStickyNoteApplicable(stickyNote)) {
|
||||
if (context.isPathApplicable(stickyNote.site)) {
|
||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
}
|
||||
}
|
||||
@@ -166,7 +128,7 @@ export function drawStickyNotes(stickyNotes, onSave, onDelete) {
|
||||
*/
|
||||
export function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
||||
const id = Date.now().toString();
|
||||
const site = window.location.href;
|
||||
const site = getContext().getPath();
|
||||
const stickyNote = new StickyNote(id, site, "");
|
||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
||||
|
||||
Reference in New Issue
Block a user