mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-25 19:59:38 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3ff28706 |
12
.vscode/launch.json
vendored
Normal file
12
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}/dist/vscode"]
|
||||
}
|
||||
]
|
||||
}
|
||||
23
README.md
23
README.md
@@ -9,14 +9,10 @@
|
||||
|
||||
It's a pet bird that hops around your computer, what more could you want?
|
||||
|
||||
### Get it for [Google Chrome](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
|
||||
### Get it for [Google Chrome](https://chromewebstore.google.com/detail/pocket-bird/lbbdngkbbgaecefacpnhnhleggabghak)
|
||||
|
||||
### Get it for [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/pocket-bird/)
|
||||
|
||||
### Get it for [Obsidian (beta)](https://github.com/IdreesInc/Pocket-Bird#Obsidian)
|
||||
|
||||
### Get it for [TamperMonkey](https://github.com/IdreesInc/Pocket-Bird#Userscript)
|
||||
|
||||
#### Join the [Discord](https://discord.gg/6yxE9prcNc) to help me beta test new features and suggest ideas!
|
||||
|
||||
## Features
|
||||
@@ -56,14 +52,6 @@ It's a pet bird that hops around your computer, what more could you want?
|
||||
3. Install my Pocket Bird script by going to this link and clicking install: [https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js](https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js)
|
||||
4. Now any websites you visit will have a little bird hopping around!
|
||||
|
||||
### Your Own Website
|
||||
|
||||
Pocket Bird can also be embedded directly into your own website! Just include the following code snippet anywhere in your HTML:
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/gh/IdreesInc/Pocket-Bird@main/dist/web/birb.embed.js"></script>
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### How do I pet the bird?
|
||||
@@ -90,15 +78,6 @@ Open the Pocket Bird menu by clicking the bird and select "Settings". From there
|
||||
|
||||
If you are running Pocket bird on a browser, the extension needs these permissions in order to insert the bird and sticky notes into your webpages. Pocket Bird does not collect any of your data or browsing history and all data is stored locally on your device!
|
||||
|
||||
## Sites With Pocket Bird
|
||||
|
||||
Here are some websites where you can find Pocket Bird hopping around:
|
||||
|
||||
- [https://grepjason.sh](https://grepjason.sh)
|
||||
|
||||
*If you've added Pocket Bird to your website, let me know and I'll add it to this list!*
|
||||
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
If you'd like to get in touch, check out the [Discord](https://discord.gg/6yxE9prcNc) to suggest features, report bugs, and stay updated on development!
|
||||
|
||||
219
build.js
219
build.js
@@ -12,24 +12,22 @@ const IMAGES_DIR = "./images";
|
||||
const FONTS_DIR = "./fonts";
|
||||
const DIST_DIR = "./dist";
|
||||
|
||||
const WEB_DIR = DIST_DIR + "/web";
|
||||
const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json";
|
||||
const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json";
|
||||
const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt";
|
||||
const VSCODE_PACKAGE = "./platform-specific/vscode/package.json";
|
||||
const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js";
|
||||
const VSCODE_WRAPPER = "./platform-specific/vscode/extension.js";
|
||||
|
||||
const USERSCRIPT_DIR = DIST_DIR + "/userscript";
|
||||
const EXTENSION_DIR = DIST_DIR + "/extension";
|
||||
const OBSIDIAN_DIR = DIST_DIR + "/obsidian";
|
||||
const VSCODE_DIR = DIST_DIR + "/vscode";
|
||||
|
||||
const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css";
|
||||
|
||||
const WEB_ENTRY = SRC_DIR + "/platforms/web/web.js";
|
||||
const USERSCRIPT_ENTRY = SRC_DIR + "/platforms/userscript/userscript.js";
|
||||
const BROWSER_EXTENSION_ENTRY = SRC_DIR + "/platforms/extension/extension.js";
|
||||
const OBSIDIAN_ENTRY = SRC_DIR + "/platforms/obsidian/obsidian.js";
|
||||
|
||||
const BROWSER_MANIFEST = SRC_DIR + "/platforms/extension/manifest.json";
|
||||
const OBSIDIAN_MANIFEST = SRC_DIR + "/platforms/obsidian/manifest.json";
|
||||
const USERSCRIPT_HEADER = SRC_DIR + "/platforms/userscript/header.txt";
|
||||
const OBSIDIAN_WRAPPER = SRC_DIR + "/platforms/obsidian/wrapper.js";
|
||||
|
||||
const TEMP_BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
|
||||
const APPLICATION_ENTRY = SRC_DIR + "/application.js";
|
||||
const BUNDLED_OUTPUT = DIST_DIR + "/birb.bundled.js";
|
||||
const BIRB_OUTPUT = DIST_DIR + "/birb.js";
|
||||
|
||||
const MONOCRAFT_URL = "https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf";
|
||||
|
||||
@@ -79,131 +77,130 @@ const version = `${versionDate}`; // Disable build number for now
|
||||
buildCache.version = version;
|
||||
writeFileSync(BUILD_CACHE_PATH, JSON.stringify(buildCache), 'utf8');
|
||||
|
||||
/**
|
||||
* @param {string} entryPoint
|
||||
* @param {boolean} [embedFont]
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function generateCode(entryPoint, embedFont = false) {
|
||||
// Bundle with rollup
|
||||
const bundle = await rollup({
|
||||
input: entryPoint,
|
||||
});
|
||||
// =============================================
|
||||
// Build JavaScript function
|
||||
// =============================================
|
||||
|
||||
await bundle.write({
|
||||
file: TEMP_BUNDLED_OUTPUT,
|
||||
format: 'iife',
|
||||
});
|
||||
// Bundle with rollup
|
||||
const bundle = await rollup({
|
||||
input: APPLICATION_ENTRY,
|
||||
});
|
||||
|
||||
await bundle.close();
|
||||
await bundle.write({
|
||||
file: BUNDLED_OUTPUT,
|
||||
format: 'iife',
|
||||
});
|
||||
|
||||
let birbJs = readFileSync(TEMP_BUNDLED_OUTPUT, 'utf8');
|
||||
await bundle.close();
|
||||
|
||||
// Delete bundled file
|
||||
unlinkSync(TEMP_BUNDLED_OUTPUT);
|
||||
let birbJs = readFileSync(BUNDLED_OUTPUT, 'utf8');
|
||||
|
||||
// Replace version placeholder
|
||||
birbJs = birbJs.replaceAll(VERSION_KEY, version);
|
||||
// Delete bundled file
|
||||
unlinkSync(BUNDLED_OUTPUT);
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
// Replace version placeholder
|
||||
birbJs = birbJs.replaceAll(VERSION_KEY, version);
|
||||
|
||||
// Insert stylesheet
|
||||
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
|
||||
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent);
|
||||
|
||||
if (embedFont) {
|
||||
// Encode font to data URI
|
||||
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
|
||||
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
|
||||
birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, monocraftDataUri);
|
||||
} else {
|
||||
birbJs = birbJs.replaceAll(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
|
||||
}
|
||||
return birbJs;
|
||||
// Compile and insert sprite sheets
|
||||
for (const spriteSheet of spriteSheets) {
|
||||
const dataUri = readFileSync(spriteSheet.path, 'base64');
|
||||
birbJs = birbJs.replaceAll(spriteSheet.key, `data:image/png;base64,${dataUri}`);
|
||||
}
|
||||
|
||||
async function buildWeb() {
|
||||
const birbJs = await generateCode(WEB_ENTRY);
|
||||
mkdirSync(WEB_DIR, { recursive: true });
|
||||
writeFileSync(WEB_DIR + '/birb.js', birbJs);
|
||||
writeFileSync(WEB_DIR + '/birb.embed.js', birbJs);
|
||||
}
|
||||
// Insert stylesheet
|
||||
const stylesheetContent = readFileSync(STYLESHEET_PATH, 'utf8');
|
||||
birbJs = birbJs.replace(STYLESHEET_KEY, stylesheetContent).replace(MONOCRAFT_SRC_KEY, MONOCRAFT_URL);
|
||||
|
||||
async function buildUserscript() {
|
||||
const birbJs = await generateCode(USERSCRIPT_ENTRY);
|
||||
|
||||
// Get userscript header
|
||||
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
|
||||
// Write bundled JavaScript function
|
||||
writeFileSync(BIRB_OUTPUT, birbJs);
|
||||
|
||||
mkdirSync(USERSCRIPT_DIR, { recursive: true });
|
||||
const userScript = userScriptHeader + "\n" + birbJs;
|
||||
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
|
||||
}
|
||||
// =============================================
|
||||
// Build userscript
|
||||
// =============================================
|
||||
|
||||
async function buildExtension() {
|
||||
const birbJs = await generateCode(BROWSER_EXTENSION_ENTRY);
|
||||
// Get userscript header
|
||||
const userScriptHeader = readFileSync(USERSCRIPT_HEADER, 'utf8').replaceAll(VERSION_KEY, version);
|
||||
|
||||
mkdirSync(EXTENSION_DIR, { recursive: true });
|
||||
mkdirSync(USERSCRIPT_DIR, { recursive: true });
|
||||
const userScript = userScriptHeader + "\n" + birbJs;
|
||||
writeFileSync(USERSCRIPT_DIR + '/birb.user.js', userScript);
|
||||
|
||||
// Copy birb.js
|
||||
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
|
||||
// =============================================
|
||||
// Build browser extension
|
||||
// =============================================
|
||||
|
||||
// Copy manifest.json
|
||||
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
|
||||
browserManifest = browserManifest.replace(VERSION_KEY, version);
|
||||
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
|
||||
mkdirSync(EXTENSION_DIR, { recursive: true });
|
||||
|
||||
// Copy icons folder
|
||||
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
|
||||
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
|
||||
// Copy birb.js
|
||||
writeFileSync(EXTENSION_DIR + '/birb.js', birbJs);
|
||||
|
||||
// Copy fonts folder
|
||||
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
// Copy manifest.json
|
||||
let browserManifest = readFileSync(BROWSER_MANIFEST, 'utf8');
|
||||
browserManifest = browserManifest.replace(VERSION_KEY, version);
|
||||
writeFileSync(EXTENSION_DIR + '/manifest.json', browserManifest);
|
||||
|
||||
// Compress extension folder into zip
|
||||
const output = createWriteStream(DIST_DIR + "/extension.zip");
|
||||
const archive = archiver('zip');
|
||||
// Copy icons folder
|
||||
mkdirSync(EXTENSION_DIR + '/images/icons', { recursive: true });
|
||||
cpSync(IMAGES_DIR + '/icons/transparent', EXTENSION_DIR + '/images/icons/transparent', { recursive: true });
|
||||
|
||||
output.on('close', () => {
|
||||
console.log(`Created zip file: ${archive.pointer()} total bytes`);
|
||||
});
|
||||
// Copy fonts folder
|
||||
mkdirSync(EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
cpSync(FONTS_DIR, EXTENSION_DIR + '/fonts', { recursive: true });
|
||||
|
||||
archive.on('error', (err) => {
|
||||
throw err;
|
||||
});
|
||||
// Compress extension folder into zip
|
||||
const output = createWriteStream(DIST_DIR + "/extension.zip");
|
||||
const archive = archiver('zip');
|
||||
|
||||
archive.pipe(output);
|
||||
archive.directory(EXTENSION_DIR + '/', false);
|
||||
archive.finalize();
|
||||
}
|
||||
output.on('close', () => {
|
||||
console.log(`Created zip file: ${archive.pointer()} total bytes`);
|
||||
});
|
||||
|
||||
async function buildObsidian() {
|
||||
const birbJs = await generateCode(OBSIDIAN_ENTRY, true);
|
||||
archive.on('error', (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
mkdirSync(OBSIDIAN_DIR, { recursive: true });
|
||||
archive.pipe(output);
|
||||
archive.directory(EXTENSION_DIR + '/', false);
|
||||
archive.finalize();
|
||||
|
||||
// Wrap birb.js with plugin boilerplate
|
||||
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
|
||||
// =============================================
|
||||
// Build Obsidian plugin
|
||||
// =============================================
|
||||
|
||||
// Create main.js with plugin code
|
||||
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
|
||||
mkdirSync(OBSIDIAN_DIR, { recursive: true });
|
||||
|
||||
// Copy manifest.json
|
||||
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
|
||||
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
|
||||
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
|
||||
}
|
||||
// Wrap birb.js with plugin boilerplate
|
||||
let obsidianPlugin = readFileSync(OBSIDIAN_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
|
||||
|
||||
console.log("Starting build...");
|
||||
// Encode font to data URI since Obsidian plugins can't have external font files
|
||||
const monocraftFontData = readFileSync(FONTS_DIR + '/Monocraft.otf', 'base64');
|
||||
const monocraftDataUri = `data:font/otf;base64,${monocraftFontData}`;
|
||||
obsidianPlugin = obsidianPlugin.replace(MONOCRAFT_URL, monocraftDataUri);
|
||||
|
||||
await buildWeb();
|
||||
await buildUserscript();
|
||||
await buildExtension();
|
||||
await buildObsidian();
|
||||
// Create main.js with plugin code
|
||||
writeFileSync(OBSIDIAN_DIR + '/main.js', obsidianPlugin);
|
||||
|
||||
console.log("Build completed successfully!");
|
||||
// Copy manifest.json
|
||||
let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8');
|
||||
obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`);
|
||||
writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest);
|
||||
|
||||
// =============================================
|
||||
// Build VSCode extension
|
||||
// =============================================
|
||||
|
||||
mkdirSync(VSCODE_DIR, { recursive: true });
|
||||
|
||||
// Wrap birb.js with VSCode extension boilerplate
|
||||
let vscodeExtension = readFileSync(VSCODE_WRAPPER, 'utf8').replace(VERSION_KEY, version).replace(CODE_KEY, birbJs);
|
||||
|
||||
// Create extension.js with extension code
|
||||
writeFileSync(VSCODE_DIR + '/extension.js', vscodeExtension);
|
||||
|
||||
// Copy package.json
|
||||
let vscodePackage = readFileSync(VSCODE_PACKAGE, 'utf8');
|
||||
vscodePackage = vscodePackage.replace(VERSION_KEY, version);
|
||||
writeFileSync(VSCODE_DIR + '/package.json', vscodePackage);
|
||||
|
||||
console.log(`Build complete: ${version}`);
|
||||
454
dist/web/birb.js → dist/birb.js
vendored
454
dist/web/birb.js → dist/birb.js
vendored
@@ -6,8 +6,7 @@
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -23,17 +22,6 @@
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
@@ -857,51 +845,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
|
||||
/**
|
||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||
@@ -912,6 +857,14 @@
|
||||
*/
|
||||
class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -995,6 +948,16 @@
|
||||
|
||||
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|{}>}
|
||||
@@ -1020,6 +983,204 @@
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
@@ -1194,7 +1355,7 @@
|
||||
|
||||
class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -1243,7 +1404,7 @@
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
@@ -1350,8 +1511,7 @@
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -1564,7 +1724,6 @@
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -1724,13 +1883,6 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -1772,24 +1924,68 @@
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -1837,16 +2033,12 @@
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -1854,7 +2046,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false),
|
||||
new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -1868,8 +2060,6 @@
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -1960,8 +2150,8 @@
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -2472,11 +2662,16 @@
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
{
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.position !== "fixed" && style.position !== "sticky";
|
||||
});
|
||||
if (nonFixedElements.length === 0) {
|
||||
return false;
|
||||
@@ -2550,9 +2745,6 @@
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -2619,64 +2811,8 @@
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initializeApplication(new LocalContext());
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
|
||||
})();
|
||||
BIN
dist/extension.zip
vendored
BIN
dist/extension.zip
vendored
Binary file not shown.
443
dist/extension/birb.js
vendored
443
dist/extension/birb.js
vendored
@@ -6,8 +6,7 @@
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -23,17 +22,6 @@
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
@@ -857,51 +845,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
|
||||
/**
|
||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||
@@ -912,6 +857,14 @@
|
||||
*/
|
||||
class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -993,8 +946,96 @@
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -1020,9 +1061,9 @@
|
||||
// @ts-expect-error
|
||||
if (chrome.runtime.lastError) {
|
||||
// @ts-expect-error
|
||||
error(chrome.runtime.lastError);
|
||||
console.error(chrome.runtime.lastError);
|
||||
} else {
|
||||
log("Settings saved successfully");
|
||||
console.log("Settings saved successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1035,6 +1076,111 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
@@ -1209,7 +1355,7 @@
|
||||
|
||||
class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -1258,7 +1404,7 @@
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
@@ -1365,8 +1511,7 @@
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -1579,7 +1724,6 @@
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -1739,13 +1883,6 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -1787,24 +1924,68 @@
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -1852,16 +2033,12 @@
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -1869,7 +2046,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false),
|
||||
new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -1883,8 +2060,6 @@
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -1975,8 +2150,8 @@
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -2487,11 +2662,16 @@
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
{
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.position !== "fixed" && style.position !== "sticky";
|
||||
});
|
||||
if (nonFixedElements.length === 0) {
|
||||
return false;
|
||||
@@ -2565,9 +2745,6 @@
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -2634,64 +2811,8 @@
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initializeApplication(new BrowserExtensionContext());
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
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": "2026.1.4",
|
||||
"version": "2025.11.15",
|
||||
"homepage_url": "https://idreesinc.com",
|
||||
"icons": {
|
||||
"48": "images/icons/transparent/48x48x1.png",
|
||||
|
||||
403
dist/obsidian/main.js
vendored
403
dist/obsidian/main.js
vendored
@@ -1,7 +1,7 @@
|
||||
const { Plugin, Notice } = require('obsidian');
|
||||
module.exports = class PocketBird extends Plugin {
|
||||
onload() {
|
||||
console.log("Loading Pocket Bird version 2026.1.4...");
|
||||
console.log("Loading Pocket Bird version 2025.11.15...");
|
||||
const OBSIDIAN_PLUGIN = this;
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -11,8 +11,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -28,17 +27,6 @@ module.exports = class PocketBird extends Plugin {
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
@@ -862,50 +850,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
|
||||
/**
|
||||
@@ -917,6 +862,14 @@ module.exports = class PocketBird extends Plugin {
|
||||
*/
|
||||
class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -998,7 +951,145 @@ module.exports = class PocketBird extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -1078,6 +1169,23 @@ module.exports = class PocketBird extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
@@ -1252,7 +1360,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
|
||||
class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -1301,7 +1409,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
@@ -1408,8 +1516,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -1622,7 +1729,6 @@ module.exports = class PocketBird extends Plugin {
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -1782,13 +1888,6 @@ module.exports = class PocketBird extends Plugin {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -1830,24 +1929,68 @@ module.exports = class PocketBird extends Plugin {
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -1895,16 +2038,12 @@ module.exports = class PocketBird extends Plugin {
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -1912,7 +2051,7 @@ module.exports = class PocketBird extends Plugin {
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false),
|
||||
new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -1926,8 +2065,6 @@ module.exports = class PocketBird extends Plugin {
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -2018,8 +2155,8 @@ module.exports = class PocketBird extends Plugin {
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -2530,11 +2667,16 @@ module.exports = class PocketBird extends Plugin {
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
{
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.position !== "fixed" && style.position !== "sticky";
|
||||
});
|
||||
if (nonFixedElements.length === 0) {
|
||||
return false;
|
||||
@@ -2608,9 +2750,6 @@ module.exports = class PocketBird extends Plugin {
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -2677,65 +2816,9 @@ module.exports = class PocketBird extends Plugin {
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initializeApplication(new ObsidianContext());
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
|
||||
2
dist/obsidian/manifest.json
vendored
2
dist/obsidian/manifest.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "pocket-bird",
|
||||
"name": "Pocket Bird",
|
||||
"version": "2026.1.4",
|
||||
"version": "2025.11.15",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Add a pet bird to fly around your notes and keep you company!",
|
||||
"author": "Idrees Hassan",
|
||||
|
||||
450
dist/userscript/birb.user.js
vendored
450
dist/userscript/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Pocket Bird
|
||||
// @namespace https://idreesinc.com
|
||||
// @version 2026.1.4
|
||||
// @version 2025.11.15
|
||||
// @description It's a pet bird in your browser, what more could you want?
|
||||
// @author Idrees
|
||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/userscript/birb.user.js
|
||||
@@ -20,8 +20,7 @@
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -37,17 +36,6 @@
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
@@ -871,51 +859,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
|
||||
/**
|
||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||
@@ -926,6 +871,14 @@
|
||||
*/
|
||||
class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -1007,8 +960,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
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|{}>}
|
||||
@@ -1040,6 +1039,162 @@
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserExtensionContext extends Context {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isContextActive() {
|
||||
// @ts-expect-error
|
||||
return typeof chrome !== "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 CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
@@ -1214,7 +1369,7 @@
|
||||
|
||||
class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -1263,7 +1418,7 @@
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
@@ -1370,8 +1525,7 @@
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -1584,7 +1738,6 @@
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -1744,13 +1897,6 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -1792,24 +1938,68 @@
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -1857,16 +2047,12 @@
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -1874,7 +2060,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false),
|
||||
new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -1888,8 +2074,6 @@
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -1980,8 +2164,8 @@
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -2492,11 +2676,16 @@
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
{
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.position !== "fixed" && style.position !== "sticky";
|
||||
});
|
||||
if (nonFixedElements.length === 0) {
|
||||
return false;
|
||||
@@ -2570,9 +2759,6 @@
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -2639,64 +2825,8 @@
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initializeApplication(new UserScriptContext());
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
472
dist/web/birb.embed.js → dist/vscode/extension.js
vendored
472
dist/web/birb.embed.js → dist/vscode/extension.js
vendored
@@ -1,4 +1,14 @@
|
||||
(function () {
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
const vscode = require("vscode");
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
};
|
||||
|
||||
function activate(context) {
|
||||
console.log("Loading Pocket Bird...");
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const Directions = {
|
||||
@@ -6,8 +16,7 @@
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -23,17 +32,6 @@
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
@@ -857,51 +855,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-check
|
||||
|
||||
class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
|
||||
/**
|
||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||
@@ -912,6 +867,14 @@
|
||||
*/
|
||||
class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -995,6 +958,16 @@
|
||||
|
||||
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|{}>}
|
||||
@@ -1020,6 +993,204 @@
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
@@ -1194,7 +1365,7 @@
|
||||
|
||||
class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -1243,7 +1414,7 @@
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
@@ -1350,8 +1521,7 @@
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -1564,7 +1734,6 @@
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -1724,13 +1893,6 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
@@ -1772,24 +1934,68 @@
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -1837,16 +2043,12 @@
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -1854,7 +2056,7 @@
|
||||
insertModal(`${birdBirb()} Mode`, message);
|
||||
}),
|
||||
new Separator(),
|
||||
new MenuItem("2026.1.4", () => { alert("Thank you for using Pocket Bird! You are on version: 2026.1.4"); }, false),
|
||||
new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false),
|
||||
];
|
||||
|
||||
const styleElement = document.createElement("style");
|
||||
@@ -1868,8 +2070,6 @@
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -1960,8 +2160,8 @@
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -2472,11 +2672,16 @@
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
{
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
return style.position !== "fixed" && style.position !== "sticky";
|
||||
});
|
||||
if (nonFixedElements.length === 0) {
|
||||
return false;
|
||||
@@ -2550,9 +2755,6 @@
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -2619,64 +2821,14 @@
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initializeApplication(new LocalContext());
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
console.log("Pocket Bird loaded!");
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
21
dist/vscode/package.json
vendored
Normal file
21
dist/vscode/package.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "pocket-bird",
|
||||
"version": "2025.11.15",
|
||||
"engines": {
|
||||
"vscode": "^1.32.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "pocket-bird.helloWorld",
|
||||
"title": "Hello World",
|
||||
"category": "Example"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
16
platform-specific/vscode/extension.js
Normal file
16
platform-specific/vscode/extension.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
const vscode = require("vscode");
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
};
|
||||
|
||||
function activate(context) {
|
||||
console.log("Loading Pocket Bird...");
|
||||
__CODE__
|
||||
console.log("Pocket Bird loaded!");
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
5
platform-specific/vscode/inject.sh
Normal file
5
platform-specific/vscode/inject.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
# my-vscode
|
||||
export NODE_OPTIONS="--require /Users/idrees/Documents/Programs/JavaScript/Birb/platform-specific/vscode/patch.js"
|
||||
"/Applications/Visual Studio Code.app/Contents/MacOS/Electron" \
|
||||
"$@"
|
||||
21
platform-specific/vscode/package.json
Normal file
21
platform-specific/vscode/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "pocket-bird",
|
||||
"version": "__VERSION__",
|
||||
"engines": {
|
||||
"vscode": "^1.32.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onStartupFinished"
|
||||
],
|
||||
"main": "extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "pocket-bird.helloWorld",
|
||||
"title": "Hello World",
|
||||
"category": "Example"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
platform-specific/vscode/patch.js
Normal file
1
platform-specific/vscode/patch.js
Normal file
@@ -0,0 +1 @@
|
||||
console.log("Birb patch for VSCode loaded.");
|
||||
@@ -26,6 +26,6 @@
|
||||
</div>
|
||||
<div id="spacer"></div>
|
||||
<!-- <script type="module" src="spritesheet-compiler.js"></script> -->
|
||||
<script src="../dist/web/birb.js"></script>
|
||||
<script src="../dist/birb.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,12 +2,9 @@ import Frame from './frame.js';
|
||||
import Layer from './layer.js';
|
||||
import Anim from './anim.js';
|
||||
import { Birb, Animations } from './birb.js';
|
||||
import { Birdsong } from './sound.js';
|
||||
import { Context, ObsidianContext } from './context.js';
|
||||
import { getContext, ObsidianContext } from './context.js';
|
||||
|
||||
import {
|
||||
getContext,
|
||||
setContext,
|
||||
Directions,
|
||||
isDebug,
|
||||
setDebug,
|
||||
@@ -61,8 +58,7 @@ import {
|
||||
* @typedef {typeof DEFAULT_SETTINGS} Settings
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
birbMode: false,
|
||||
soundEnabled: true
|
||||
birbMode: false
|
||||
};
|
||||
|
||||
// Rendering constants
|
||||
@@ -113,24 +109,68 @@ const MIN_FOCUS_ELEMENT_WIDTH = 100;
|
||||
/** @type {Partial<Settings>} */
|
||||
let userSettings = {};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Context} context
|
||||
/**
|
||||
* Load the sprite sheet and return the pixel-map template
|
||||
* @param {string} dataUri
|
||||
* @param {boolean} [templateColors]
|
||||
* @returns {Promise<string[][]>}
|
||||
*/
|
||||
export async function initializeApplication(context) {
|
||||
log("birbOS booting up...");
|
||||
setContext(context);
|
||||
log("Loading sprite sheets...");
|
||||
const birbPixels = await loadSpriteSheetPixels(SPRITE_SHEET);
|
||||
const featherPixels = await loadSpriteSheetPixels(FEATHER_SPRITE_SHEET);
|
||||
startApplication(birbPixels, featherPixels);
|
||||
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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[][]} birbPixels
|
||||
* @param {string[][]} featherPixels
|
||||
*/
|
||||
function startApplication(birbPixels, featherPixels) {
|
||||
log("Loading sprite sheets...");
|
||||
|
||||
Promise.all([
|
||||
loadSpriteSheetPixels(SPRITE_SHEET),
|
||||
loadSpriteSheetPixels(FEATHER_SPRITE_SHEET)
|
||||
]).then(([birbPixels, featherPixels]) => {
|
||||
|
||||
const SPRITE_SHEET = birbPixels;
|
||||
const FEATHER_SPRITE_SHEET = featherPixels;
|
||||
@@ -178,16 +218,12 @@ function startApplication(birbPixels, featherPixels) {
|
||||
const settingsItems = [
|
||||
new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false),
|
||||
new Separator(),
|
||||
new MenuItem(() => `${settings().soundEnabled ? "Disable" : "Enable"} Sound`, () => {
|
||||
userSettings.soundEnabled = !settings().soundEnabled;
|
||||
save();
|
||||
}),
|
||||
new MenuItem(() => `Toggle ${birdBirb(true)} Mode`, () => {
|
||||
userSettings.birbMode = !settings().birbMode;
|
||||
new MenuItem("Toggle Birb Mode", () => {
|
||||
userSettings.birbMode = !userSettings.birbMode;
|
||||
save();
|
||||
const message = makeElement("birb-message-content");
|
||||
message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`));
|
||||
if (settings().birbMode) {
|
||||
if (userSettings.birbMode) {
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createElement("br"));
|
||||
message.appendChild(document.createTextNode("Welcome back to 2012"));
|
||||
@@ -209,8 +245,6 @@ function startApplication(birbPixels, featherPixels) {
|
||||
FLYING: "flying",
|
||||
};
|
||||
|
||||
const birdsong = new Birdsong();
|
||||
|
||||
let frozen = false;
|
||||
let stateStart = Date.now();
|
||||
let currentState = States.IDLE;
|
||||
@@ -301,8 +335,8 @@ function startApplication(birbPixels, featherPixels) {
|
||||
/**
|
||||
* Bird or birb, you decide
|
||||
*/
|
||||
function birdBirb(invert = false) {
|
||||
return settings().birbMode !== invert ? "Birb" : "Bird";
|
||||
function birdBirb() {
|
||||
return settings().birbMode ? "Birb" : "Bird";
|
||||
}
|
||||
|
||||
function init() {
|
||||
@@ -817,11 +851,10 @@ function startApplication(birbPixels, featherPixels) {
|
||||
return true;
|
||||
});
|
||||
/** @type {HTMLElement[]} */
|
||||
// @ts-expect-error
|
||||
const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH);
|
||||
// Ensure the bird doesn't land on fixed or sticky elements
|
||||
// const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
// TODO: FIX
|
||||
const fixedAllowed = true;
|
||||
const fixedAllowed = getContext() instanceof ObsidianContext;
|
||||
const nonFixedElements = largeElements.filter((el) => {
|
||||
if (fixedAllowed) {
|
||||
return true;
|
||||
@@ -905,9 +938,6 @@ function startApplication(birbPixels, featherPixels) {
|
||||
|
||||
function pet() {
|
||||
if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) {
|
||||
if (settings().soundEnabled) {
|
||||
birdsong.chirp();
|
||||
}
|
||||
birb.setAnimation(Animations.HEART);
|
||||
lastPetTimestamp = Date.now();
|
||||
}
|
||||
@@ -978,60 +1008,6 @@ function startApplication(birbPixels, featherPixels) {
|
||||
// Run the birb
|
||||
init();
|
||||
draw();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
}).catch((e) => {
|
||||
error("Error while loading sprite sheets: ", e);
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
import { debug, log, error } from "./shared.js";
|
||||
|
||||
export const SAVE_KEY = "birbSaveData";
|
||||
const SAVE_KEY = "birbSaveData";
|
||||
const ROOT_PATH = "";
|
||||
const SET_CONTEXT = "__CONTEXT__"
|
||||
|
||||
/**
|
||||
* @typedef {import('./application.js').BirbSaveData} BirbSaveData
|
||||
@@ -13,6 +12,14 @@ const SET_CONTEXT = "__CONTEXT__"
|
||||
*/
|
||||
export class Context {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {boolean} Whether this context is applicable
|
||||
*/
|
||||
isContextActive() {
|
||||
throw new Error("Method not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -96,6 +103,16 @@ 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|{}>}
|
||||
@@ -123,6 +140,15 @@ export class LocalContext extends Context {
|
||||
|
||||
export class UserScriptContext extends Context {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isContextActive() {
|
||||
// @ts-expect-error
|
||||
return typeof GM_getValue === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {Promise<BirbSaveData|{}>}
|
||||
@@ -154,7 +180,16 @@ export class UserScriptContext extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowserExtensionContext extends Context {
|
||||
class BrowserExtensionContext extends Context {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isContextActive() {
|
||||
// @ts-expect-error
|
||||
return typeof chrome !== "undefined";
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -181,9 +216,9 @@ export class BrowserExtensionContext extends Context {
|
||||
// @ts-expect-error
|
||||
if (chrome.runtime.lastError) {
|
||||
// @ts-expect-error
|
||||
error(chrome.runtime.lastError);
|
||||
console.error(chrome.runtime.lastError);
|
||||
} else {
|
||||
log("Settings saved successfully");
|
||||
console.log("Settings saved successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -197,6 +232,14 @@ export class BrowserExtensionContext extends Context {
|
||||
}
|
||||
|
||||
export class ObsidianContext extends Context {
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isContextActive() {
|
||||
// @ts-expect-error
|
||||
return typeof app !== "undefined" && typeof app.vault !== "undefined";
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -276,6 +319,23 @@ export class ObsidianContext extends Context {
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXTS = [
|
||||
new UserScriptContext(),
|
||||
new ObsidianContext(),
|
||||
new BrowserExtensionContext(),
|
||||
new LocalContext()
|
||||
];
|
||||
|
||||
export function getContext() {
|
||||
for (const context of CONTEXTS) {
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ export const MENU_EXIT_ID = "birb-menu-exit";
|
||||
|
||||
export class MenuItem {
|
||||
/**
|
||||
* @param {string|(() => string)} text
|
||||
* @param {string} text
|
||||
* @param {() => void} action
|
||||
* @param {boolean} [removeMenu]
|
||||
*/
|
||||
@@ -61,7 +61,7 @@ function makeMenuItem(item, removeMenuCallback) {
|
||||
if (item instanceof Separator) {
|
||||
return makeElement("birb-window-separator");
|
||||
}
|
||||
let menuItem = makeElement("birb-menu-item", typeof item.text === "function" ? item.text() : item.text);
|
||||
let menuItem = makeElement("birb-menu-item", item.text);
|
||||
onClick(menuItem, () => {
|
||||
if (item.removeMenu) {
|
||||
removeMenuCallback();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { initializeApplication } from "../../application.js";
|
||||
import { BrowserExtensionContext } from "../../context.js";
|
||||
|
||||
initializeApplication(new BrowserExtensionContext());
|
||||
@@ -1,4 +0,0 @@
|
||||
import { initializeApplication } from "../../application.js";
|
||||
import { ObsidianContext } from "../../context.js";
|
||||
|
||||
initializeApplication(new ObsidianContext());
|
||||
@@ -1,4 +0,0 @@
|
||||
import { initializeApplication } from "../../application.js";
|
||||
import { UserScriptContext } from "../../context.js";
|
||||
|
||||
initializeApplication(new UserScriptContext());
|
||||
@@ -1,4 +0,0 @@
|
||||
import { initializeApplication } from "../../application.js";
|
||||
import { LocalContext } from "../../context.js";
|
||||
|
||||
initializeApplication(new LocalContext());
|
||||
@@ -3,8 +3,7 @@ export const Directions = {
|
||||
RIGHT: 1,
|
||||
};
|
||||
|
||||
let debugMode = location.hostname === "127.0.0.1";
|
||||
let context = null;
|
||||
let debugMode = window.location.hostname === "127.0.0.1";
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether debug mode is enabled
|
||||
@@ -20,17 +19,6 @@ export function setDebug(value) {
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
export function getContext() {
|
||||
if (!context) {
|
||||
throw new Error("Context requested before being set");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function setContext(newContext) {
|
||||
context = newContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an HTML element with the specified parameters
|
||||
* @param {string} className
|
||||
|
||||
43
src/sound.js
43
src/sound.js
@@ -1,43 +0,0 @@
|
||||
// @ts-check
|
||||
|
||||
export class Birdsong {
|
||||
|
||||
/**
|
||||
* @type {AudioContext}
|
||||
*/
|
||||
audioContext;
|
||||
|
||||
chirp() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
|
||||
const TIMES = [0, 0.06, 0.10, 0.15];
|
||||
const FREQUENCIES = [2200,
|
||||
3500 + Math.random() * 600,
|
||||
2100 + Math.random() * 200,
|
||||
1600 + Math.random() * 400];
|
||||
const VOLUMES = [0.0001, 0.3, 0.3, 0.0001];
|
||||
|
||||
const oscillator = this.audioContext.createOscillator();
|
||||
oscillator.type = "sine";
|
||||
const gain = this.audioContext.createGain();
|
||||
oscillator.connect(gain);
|
||||
gain.connect(this.audioContext.destination);
|
||||
|
||||
const now = this.audioContext.currentTime;
|
||||
for (let i = 0; i < TIMES.length; i++) {
|
||||
const time = TIMES[i] + now;
|
||||
if (i === 0) {
|
||||
oscillator.frequency.setValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.setValueAtTime(VOLUMES[i], time);
|
||||
} else {
|
||||
oscillator.frequency.exponentialRampToValueAtTime(FREQUENCIES[i], time);
|
||||
gain.gain.exponentialRampToValueAtTime(VOLUMES[i], time);
|
||||
}
|
||||
}
|
||||
|
||||
oscillator.start(now);
|
||||
oscillator.stop(now + TIMES[TIMES.length - 1]);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
getContext,
|
||||
makeElement,
|
||||
makeDraggable,
|
||||
makeClosable
|
||||
} from './shared.js';
|
||||
import { getContext } from './context.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} SavedStickyNote
|
||||
|
||||
@@ -198,7 +198,6 @@
|
||||
|
||||
.birb-menu-item {
|
||||
width: calc(100% - var(--birb-double-border-size));
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
@@ -358,13 +357,6 @@
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input::placeholder {
|
||||
font-family: "Monocraft", monospace !important;
|
||||
font-size: 14px !important;
|
||||
background-color: transparent !important;
|
||||
color: rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
.birb-sticky-note-input:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
Reference in New Issue
Block a user