diff --git a/dist/birb.js b/dist/birb.js
index 100612c..fa30024 100644
--- a/dist/birb.js
+++ b/dist/birb.js
@@ -393,6 +393,258 @@
return true;
}
+ /**
+ * Create an HTML element with the specified parameters
+ * @param {string} className
+ * @param {string} [textContent]
+ * @param {string} [id]
+ * @returns {HTMLElement}
+ */
+ function makeElement$1(className, textContent, id) {
+ const element = document.createElement("div");
+ element.classList.add(className);
+ return element;
+ }
+
+ /**
+ * @param {Document|Element} element
+ * @param {(e: Event) => void} action
+ */
+ function onClick$1(element, action) {
+ element.addEventListener("click", (e) => action(e));
+ element.addEventListener("touchend", (e) => {
+ if (e instanceof TouchEvent === false) {
+ return;
+ } else if (element instanceof HTMLElement === false) {
+ return;
+ }
+ const touch = e.changedTouches[0];
+ const rect = element.getBoundingClientRect();
+ if (
+ touch.clientX >= rect.left &&
+ touch.clientX <= rect.right &&
+ touch.clientY >= rect.top &&
+ touch.clientY <= rect.bottom
+ ) {
+ action(e);
+ }
+ });
+ }
+
+ /**
+ * @param {HTMLElement|null} element The element to detect drag events on
+ * @param {boolean} [parent] Whether to move the parent element when the child is dragged
+ * @param {(top: number, left: number) => void} [callback] Callback for when element is moved
+ */
+ function makeDraggable$1(element, parent = true, callback = () => { }) {
+ if (!element) {
+ return;
+ }
+
+ let isMouseDown = false;
+ let offsetX = 0;
+ let offsetY = 0;
+ let elementToMove = parent ? element.parentElement : element;
+
+ if (!elementToMove) {
+ console.error("Birb: Parent element not found");
+ return;
+ }
+
+ element.addEventListener("mousedown", (e) => {
+ isMouseDown = true;
+ offsetX = e.clientX - elementToMove.offsetLeft;
+ offsetY = e.clientY - elementToMove.offsetTop;
+ });
+
+ element.addEventListener("touchstart", (e) => {
+ isMouseDown = true;
+ const touch = e.touches[0];
+ offsetX = touch.clientX - elementToMove.offsetLeft;
+ offsetY = touch.clientY - elementToMove.offsetTop;
+ e.preventDefault();
+ });
+
+ document.addEventListener("mouseup", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("touchend", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("mousemove", (e) => {
+ if (isMouseDown) {
+ elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
+ }
+ });
+
+ document.addEventListener("touchmove", (e) => {
+ if (isMouseDown) {
+ const touch = e.touches[0];
+ elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`;
+ }
+ });
+ }
+
+ /**
+ * @param {() => void} func
+ * @param {Element} [closeButton]
+ */
+ function makeClosable$1(func, closeButton) {
+ if (closeButton) {
+ onClick$1(closeButton, func);
+ }
+ document.addEventListener("keydown", (e) => {
+ if (closeButton && !document.body.contains(closeButton)) {
+ return;
+ }
+ if (e.key === "Escape") {
+ func();
+ }
+ });
+ }
+
+ /**
+ * @param {StickyNote} stickyNote
+ * @param {() => void} onSave
+ * @param {() => void} onDelete
+ * @returns {HTMLElement}
+ */
+ function renderStickyNote(stickyNote, onSave, onDelete) {
+ let html = `
+
+
+
+
`;
+ const noteElement = makeElement$1("birb-window");
+ noteElement.classList.add("birb-sticky-note");
+ noteElement.innerHTML = html;
+
+ noteElement.style.top = `${stickyNote.top}px`;
+ noteElement.style.left = `${stickyNote.left}px`;
+ document.body.appendChild(noteElement);
+
+ makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => {
+ stickyNote.top = top;
+ stickyNote.left = left;
+ onSave();
+ });
+
+ const closeButton = noteElement.querySelector(".birb-window-close");
+ if (closeButton) {
+ makeClosable$1(() => {
+ if (confirm("Are you sure you want to delete this sticky note?")) {
+ onDelete();
+ noteElement.remove();
+ }
+ }, closeButton);
+ }
+
+ const textarea = noteElement.querySelector(".birb-sticky-note-input");
+ if (textarea && textarea instanceof HTMLTextAreaElement) {
+ let saveTimeout;
+ // Save after debounce
+ textarea.addEventListener("input", () => {
+ stickyNote.content = textarea.value;
+ if (saveTimeout) {
+ clearTimeout(saveTimeout);
+ }
+ saveTimeout = setTimeout(() => {
+ onSave();
+ }, 250);
+ });
+ }
+
+ // On window resize
+ window.addEventListener("resize", () => {
+ const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`;
+ const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`;
+ noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`;
+ });
+
+ return noteElement;
+ }
+
+ /**
+ * @param {StickyNote[]} stickyNotes
+ * @param {() => void} onSave
+ * @param {(note: StickyNote) => void} onDelete
+ */
+ function drawStickyNotes(stickyNotes, onSave, onDelete) {
+ // Remove all existing sticky notes
+ const existingNotes = document.querySelectorAll(".birb-sticky-note");
+ existingNotes.forEach(note => note.remove());
+ // Render all sticky notes
+ for (let stickyNote of stickyNotes) {
+ if (isStickyNoteApplicable(stickyNote)) {
+ renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ }
+ }
+ }
+
+ /**
+ * @param {StickyNote[]} stickyNotes
+ * @param {() => void} onSave
+ * @param {(note: StickyNote) => void} onDelete
+ */
+ function createNewStickyNote(stickyNotes, onSave, onDelete) {
+ const id = Date.now().toString();
+ const site = window.location.href;
+ const stickyNote = new StickyNote(id, site, "");
+ const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
+ element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
+ stickyNote.top = parseInt(element.style.top, 10);
+ stickyNote.left = parseInt(element.style.left, 10);
+ stickyNotes.push(stickyNote);
+ onSave();
+ }
+
+ class MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ * @param {boolean} [removeMenu]
+ * @param {boolean} [isDebug]
+ */
+ constructor(text, action, removeMenu = true, isDebug = false) {
+ this.text = text;
+ this.action = action;
+ this.removeMenu = removeMenu;
+ this.isDebug = isDebug;
+ }
+ }
+
+ class DebugMenuItem extends MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ */
+ constructor(text, action, removeMenu = true) {
+ super(text, action, removeMenu, true);
+ }
+ }
+
+ class Separator extends MenuItem {
+ constructor() {
+ super("", () => { });
+ }
+ }
+
/**
* Create an HTML element with the specified parameters
* @param {string} className
@@ -403,6 +655,12 @@
function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
+ if (textContent) {
+ element.textContent = textContent;
+ }
+ if (id) {
+ element.id = id;
+ }
return element;
}
@@ -502,13 +760,7 @@
* @param {Element} [closeButton]
*/
function makeClosable(func, closeButton) {
- if (closeButton) {
- onClick(closeButton, func);
- }
document.addEventListener("keydown", (e) => {
- if (closeButton && !document.body.contains(closeButton)) {
- return;
- }
if (e.key === "Escape") {
func();
}
@@ -516,102 +768,108 @@
}
/**
- * @param {StickyNote} stickyNote
- * @param {() => void} onSave
- * @param {() => void} onDelete
+ * @param {MenuItem} item
+ * @param {() => void} removeMenuCallback
* @returns {HTMLElement}
*/
- function renderStickyNote(stickyNote, onSave, onDelete) {
- let html = `
-
-
-
-
`;
- const noteElement = makeElement("birb-window");
- noteElement.classList.add("birb-sticky-note");
- noteElement.innerHTML = html;
-
- noteElement.style.top = `${stickyNote.top}px`;
- noteElement.style.left = `${stickyNote.left}px`;
- document.body.appendChild(noteElement);
-
- makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => {
- stickyNote.top = top;
- stickyNote.left = left;
- onSave();
- });
-
- const closeButton = noteElement.querySelector(".birb-window-close");
- if (closeButton) {
- makeClosable(() => {
- if (confirm("Are you sure you want to delete this sticky note?")) {
- onDelete();
- noteElement.remove();
- }
- }, closeButton);
+ function makeMenuItem(item, removeMenuCallback) {
+ if (item instanceof Separator) {
+ return makeElement("birb-window-separator");
}
-
- const textarea = noteElement.querySelector(".birb-sticky-note-input");
- if (textarea && textarea instanceof HTMLTextAreaElement) {
- let saveTimeout;
- // Save after debounce
- textarea.addEventListener("input", () => {
- stickyNote.content = textarea.value;
- if (saveTimeout) {
- clearTimeout(saveTimeout);
- }
- saveTimeout = setTimeout(() => {
- onSave();
- }, 250);
- });
- }
-
- // On window resize
- window.addEventListener("resize", () => {
- const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`;
- const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`;
- noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`;
+ let menuItem = makeElement("birb-menu-item", item.text);
+ onClick(menuItem, () => {
+ if (item.removeMenu) {
+ removeMenuCallback();
+ }
+ item.action();
});
-
- return noteElement;
+ return menuItem;
}
/**
- * @param {StickyNote[]} stickyNotes
- * @param {() => void} onSave
- * @param {(note: StickyNote) => void} onDelete
+ * Add the menu to the page if it doesn't already exist
+ * @param {string} menuId
+ * @param {string} menuExitId
+ * @param {MenuItem[]} menuItems
+ * @param {string} title
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
*/
- function drawStickyNotes(stickyNotes, onSave, onDelete) {
- // Remove all existing sticky notes
- const existingNotes = document.querySelectorAll(".birb-sticky-note");
- existingNotes.forEach(note => note.remove());
- // Render all sticky notes
- for (let stickyNote of stickyNotes) {
- if (isStickyNoteApplicable(stickyNote)) {
- renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
+ if (document.querySelector("#" + menuId)) {
+ return;
+ }
+ let menu = makeElement("birb-window", undefined, menuId);
+ let header = makeElement("birb-window-header");
+ header.innerHTML = `${title}
`;
+ let content = makeElement("birb-window-content");
+ const removeCallback = () => removeMenu(menuId, menuExitId);
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
}
}
+ menu.appendChild(header);
+ menu.appendChild(content);
+ document.body.appendChild(menu);
+ makeDraggable(document.querySelector(".birb-window-header"));
+
+ let menuExit = makeElement("birb-window-exit", undefined, menuExitId);
+ onClick(menuExit, removeCallback);
+ document.body.appendChild(menuExit);
+ makeClosable(removeCallback);
+
+ updateLocationCallback(menu);
}
/**
- * @param {StickyNote[]} stickyNotes
- * @param {() => void} onSave
- * @param {(note: StickyNote) => void} onDelete
+ * Remove the menu from the page
+ * @param {string} menuId
+ * @param {string} menuExitId
*/
- function createNewStickyNote(stickyNotes, onSave, onDelete) {
- const id = Date.now().toString();
- const site = window.location.href;
- const stickyNote = new StickyNote(id, site, "");
- const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
- element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
- element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
- stickyNote.top = parseInt(element.style.top, 10);
- stickyNote.left = parseInt(element.style.left, 10);
- stickyNotes.push(stickyNote);
- onSave();
+ function removeMenu(menuId, menuExitId) {
+ const menu = document.querySelector("#" + menuId);
+ if (menu) {
+ menu.remove();
+ }
+ const exitMenu = document.querySelector("#" + menuExitId);
+ if (exitMenu) {
+ exitMenu.remove();
+ }
+ }
+
+ /**
+ * @param {string} menuId
+ * @returns {boolean} Whether the menu element is on the page
+ */
+ function isMenuOpen(menuId) {
+ return document.querySelector("#" + menuId) !== null;
+ }
+
+ /**
+ * @param {string} menuId
+ * @param {MenuItem[]} menuItems
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
+ */
+ function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) {
+ const menu = document.querySelector("#" + menuId);
+ if (!menu || !(menu instanceof HTMLElement)) {
+ return;
+ }
+ const content = menu.querySelector(".birb-window-content");
+ if (!content) {
+ console.error("Birb: Content not found");
+ return;
+ }
+ content.innerHTML = "";
+ const removeCallback = () => removeMenu(menuId, menuId + "-exit");
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
+ }
+ }
+ updateLocationCallback(menu);
}
// @ts-ignore
@@ -1206,37 +1464,6 @@
]),
};
- class MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- * @param {boolean} [removeMenu]
- * @param {boolean} [isDebug]
- */
- constructor(text, action, removeMenu = true, isDebug = false) {
- this.text = text;
- this.action = action;
- this.removeMenu = removeMenu;
- this.isDebug = isDebug;
- }
- }
-
- class DebugMenuItem extends MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- */
- constructor(text, action, removeMenu = true) {
- super(text, action, removeMenu, true);
- }
- }
-
- class Separator extends MenuItem {
- constructor() {
- super("", () => { });
- }
- }
-
const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide),
@@ -1255,11 +1482,11 @@
debugMode = false;
}),
new Separator(),
- new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
+ new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
];
const settingsItems = [
- new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
+ new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
new Separator(),
new MenuItem("Toggle Birb Mode", () => {
userSettings.birbMode = !userSettings.birbMode;
@@ -1464,7 +1691,7 @@
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
- removeMenu();
+ removeMenu(MENU_ID, MENU_EXIT_ID);
}
});
@@ -1473,7 +1700,7 @@
// Currently being pet, don't open menu
return;
}
- insertMenu();
+ insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
});
canvas.addEventListener("mouseover", () => {
@@ -1520,7 +1747,7 @@
// Won't be restored on fullscreen exit
}
- if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
+ if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
@@ -1609,9 +1836,6 @@
function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
- if (textContent) {
- element.textContent = textContent;
- }
if (id) {
element.id = id;
}
@@ -1750,6 +1974,30 @@
centerElement(modal);
}
+ /**
+ * @param {HTMLElement} menu
+ */
+ function updateMenuLocation(menu) {
+ let x = birdX;
+ let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
+ const offset = 20;
+ if (x < window.innerWidth / 2) {
+ // Left side
+ x += offset;
+ } else {
+ // Right side
+ x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
+ }
+ if (y > window.innerHeight / 2) {
+ // Top side
+ y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
+ } else {
+ // Bottom side
+ y += offset;
+ }
+ menu.style.left = `${x}px`;
+ menu.style.top = `${y}px`;
+ }
function insertFieldGuide() {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
@@ -1865,103 +2113,6 @@
save();
}
- /**
- * Add the menu to the page if it doesn't already exist
- */
- function insertMenu() {
- if (document.querySelector("#" + MENU_ID)) {
- return;
- }
- let menu = makeElement("birb-window", undefined, MENU_ID);
- let header = makeElement("birb-window-header");
- header.innerHTML = `${birdBirb().toLowerCase()}OS
`;
- let content = makeElement("birb-window-content");
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- menu.appendChild(header);
- menu.appendChild(content);
- document.body.appendChild(menu);
- makeDraggable(document.querySelector(".birb-window-header"));
-
- let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
- onClick(menuExit, () => {
- removeMenu();
- });
- document.body.appendChild(menuExit);
- makeClosable(removeMenu);
-
- updateMenuLocation(menu);
- }
-
- /**
- * Update the menu's location based on the bird's position
- * @param {HTMLElement} menu
- */
- function updateMenuLocation(menu) {
- let x = birdX;
- let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
- const offset = 20;
- if (x < window.innerWidth / 2) {
- // Left side
- x += offset;
- } else {
- // Right side
- x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
- }
- if (y > window.innerHeight / 2) {
- // Top side
- y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
- } else {
- // Bottom side
- y += offset;
- }
- menu.style.left = `${x}px`;
- menu.style.top = `${y}px`;
- }
-
- /**
- * @param {MenuItem[]} menuItems
- */
- function switchMenuItems(menuItems) {
- const menu = document.querySelector("#" + MENU_ID);
- if (!menu || !(menu instanceof HTMLElement)) {
- return;
- }
- const content = menu.querySelector(".birb-window-content");
- if (!content) {
- error("Content not found");
- return;
- }
- content.innerHTML = "";
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- updateMenuLocation(menu);
- }
-
- /**
- * @param {MenuItem} item
- * @returns {HTMLElement}
- */
- function makeMenuItem(item) {
- if (item instanceof Separator) {
- return makeElement("birb-window-separator");
- }
- let menuItem = makeElement("birb-menu-item", item.text);
- onClick(menuItem, () => {
- if (item.removeMenu) {
- removeMenu();
- }
- item.action();
- });
- return menuItem;
- }
-
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
@@ -1987,27 +2138,6 @@
});
}
- /**
- * Remove the menu from the page
- */
- function removeMenu() {
- const menu = document.querySelector("#" + MENU_ID);
- if (menu) {
- menu.remove();
- }
- const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
- if (exitMenu) {
- exitMenu.remove();
- }
- }
-
- /**
- * @returns {boolean} Whether the menu element is on the page
- */
- function isMenuOpen() {
- return document.querySelector("#" + MENU_ID) !== null;
- }
-
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged
diff --git a/dist/birb.user.js b/dist/birb.user.js
index 54a20e1..2800cc6 100644
--- a/dist/birb.user.js
+++ b/dist/birb.user.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Pocket Bird
// @namespace https://idreesinc.com
-// @version 2025.10.26.76
+// @version 2025.10.26.101
// @description birb
// @author Idrees
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
@@ -407,6 +407,258 @@
return true;
}
+ /**
+ * Create an HTML element with the specified parameters
+ * @param {string} className
+ * @param {string} [textContent]
+ * @param {string} [id]
+ * @returns {HTMLElement}
+ */
+ function makeElement$1(className, textContent, id) {
+ const element = document.createElement("div");
+ element.classList.add(className);
+ return element;
+ }
+
+ /**
+ * @param {Document|Element} element
+ * @param {(e: Event) => void} action
+ */
+ function onClick$1(element, action) {
+ element.addEventListener("click", (e) => action(e));
+ element.addEventListener("touchend", (e) => {
+ if (e instanceof TouchEvent === false) {
+ return;
+ } else if (element instanceof HTMLElement === false) {
+ return;
+ }
+ const touch = e.changedTouches[0];
+ const rect = element.getBoundingClientRect();
+ if (
+ touch.clientX >= rect.left &&
+ touch.clientX <= rect.right &&
+ touch.clientY >= rect.top &&
+ touch.clientY <= rect.bottom
+ ) {
+ action(e);
+ }
+ });
+ }
+
+ /**
+ * @param {HTMLElement|null} element The element to detect drag events on
+ * @param {boolean} [parent] Whether to move the parent element when the child is dragged
+ * @param {(top: number, left: number) => void} [callback] Callback for when element is moved
+ */
+ function makeDraggable$1(element, parent = true, callback = () => { }) {
+ if (!element) {
+ return;
+ }
+
+ let isMouseDown = false;
+ let offsetX = 0;
+ let offsetY = 0;
+ let elementToMove = parent ? element.parentElement : element;
+
+ if (!elementToMove) {
+ console.error("Birb: Parent element not found");
+ return;
+ }
+
+ element.addEventListener("mousedown", (e) => {
+ isMouseDown = true;
+ offsetX = e.clientX - elementToMove.offsetLeft;
+ offsetY = e.clientY - elementToMove.offsetTop;
+ });
+
+ element.addEventListener("touchstart", (e) => {
+ isMouseDown = true;
+ const touch = e.touches[0];
+ offsetX = touch.clientX - elementToMove.offsetLeft;
+ offsetY = touch.clientY - elementToMove.offsetTop;
+ e.preventDefault();
+ });
+
+ document.addEventListener("mouseup", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("touchend", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("mousemove", (e) => {
+ if (isMouseDown) {
+ elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
+ }
+ });
+
+ document.addEventListener("touchmove", (e) => {
+ if (isMouseDown) {
+ const touch = e.touches[0];
+ elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`;
+ }
+ });
+ }
+
+ /**
+ * @param {() => void} func
+ * @param {Element} [closeButton]
+ */
+ function makeClosable$1(func, closeButton) {
+ if (closeButton) {
+ onClick$1(closeButton, func);
+ }
+ document.addEventListener("keydown", (e) => {
+ if (closeButton && !document.body.contains(closeButton)) {
+ return;
+ }
+ if (e.key === "Escape") {
+ func();
+ }
+ });
+ }
+
+ /**
+ * @param {StickyNote} stickyNote
+ * @param {() => void} onSave
+ * @param {() => void} onDelete
+ * @returns {HTMLElement}
+ */
+ function renderStickyNote(stickyNote, onSave, onDelete) {
+ let html = `
+
+
+
+
`;
+ const noteElement = makeElement$1("birb-window");
+ noteElement.classList.add("birb-sticky-note");
+ noteElement.innerHTML = html;
+
+ noteElement.style.top = `${stickyNote.top}px`;
+ noteElement.style.left = `${stickyNote.left}px`;
+ document.body.appendChild(noteElement);
+
+ makeDraggable$1(noteElement.querySelector(".birb-window-header"), true, (top, left) => {
+ stickyNote.top = top;
+ stickyNote.left = left;
+ onSave();
+ });
+
+ const closeButton = noteElement.querySelector(".birb-window-close");
+ if (closeButton) {
+ makeClosable$1(() => {
+ if (confirm("Are you sure you want to delete this sticky note?")) {
+ onDelete();
+ noteElement.remove();
+ }
+ }, closeButton);
+ }
+
+ const textarea = noteElement.querySelector(".birb-sticky-note-input");
+ if (textarea && textarea instanceof HTMLTextAreaElement) {
+ let saveTimeout;
+ // Save after debounce
+ textarea.addEventListener("input", () => {
+ stickyNote.content = textarea.value;
+ if (saveTimeout) {
+ clearTimeout(saveTimeout);
+ }
+ saveTimeout = setTimeout(() => {
+ onSave();
+ }, 250);
+ });
+ }
+
+ // On window resize
+ window.addEventListener("resize", () => {
+ const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`;
+ const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`;
+ noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`;
+ });
+
+ return noteElement;
+ }
+
+ /**
+ * @param {StickyNote[]} stickyNotes
+ * @param {() => void} onSave
+ * @param {(note: StickyNote) => void} onDelete
+ */
+ function drawStickyNotes(stickyNotes, onSave, onDelete) {
+ // Remove all existing sticky notes
+ const existingNotes = document.querySelectorAll(".birb-sticky-note");
+ existingNotes.forEach(note => note.remove());
+ // Render all sticky notes
+ for (let stickyNote of stickyNotes) {
+ if (isStickyNoteApplicable(stickyNote)) {
+ renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ }
+ }
+ }
+
+ /**
+ * @param {StickyNote[]} stickyNotes
+ * @param {() => void} onSave
+ * @param {(note: StickyNote) => void} onDelete
+ */
+ function createNewStickyNote(stickyNotes, onSave, onDelete) {
+ const id = Date.now().toString();
+ const site = window.location.href;
+ const stickyNote = new StickyNote(id, site, "");
+ const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
+ element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
+ stickyNote.top = parseInt(element.style.top, 10);
+ stickyNote.left = parseInt(element.style.left, 10);
+ stickyNotes.push(stickyNote);
+ onSave();
+ }
+
+ class MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ * @param {boolean} [removeMenu]
+ * @param {boolean} [isDebug]
+ */
+ constructor(text, action, removeMenu = true, isDebug = false) {
+ this.text = text;
+ this.action = action;
+ this.removeMenu = removeMenu;
+ this.isDebug = isDebug;
+ }
+ }
+
+ class DebugMenuItem extends MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ */
+ constructor(text, action, removeMenu = true) {
+ super(text, action, removeMenu, true);
+ }
+ }
+
+ class Separator extends MenuItem {
+ constructor() {
+ super("", () => { });
+ }
+ }
+
/**
* Create an HTML element with the specified parameters
* @param {string} className
@@ -417,6 +669,12 @@
function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
+ if (textContent) {
+ element.textContent = textContent;
+ }
+ if (id) {
+ element.id = id;
+ }
return element;
}
@@ -516,13 +774,7 @@
* @param {Element} [closeButton]
*/
function makeClosable(func, closeButton) {
- if (closeButton) {
- onClick(closeButton, func);
- }
document.addEventListener("keydown", (e) => {
- if (closeButton && !document.body.contains(closeButton)) {
- return;
- }
if (e.key === "Escape") {
func();
}
@@ -530,102 +782,108 @@
}
/**
- * @param {StickyNote} stickyNote
- * @param {() => void} onSave
- * @param {() => void} onDelete
+ * @param {MenuItem} item
+ * @param {() => void} removeMenuCallback
* @returns {HTMLElement}
*/
- function renderStickyNote(stickyNote, onSave, onDelete) {
- let html = `
-
-
-
-
`;
- const noteElement = makeElement("birb-window");
- noteElement.classList.add("birb-sticky-note");
- noteElement.innerHTML = html;
-
- noteElement.style.top = `${stickyNote.top}px`;
- noteElement.style.left = `${stickyNote.left}px`;
- document.body.appendChild(noteElement);
-
- makeDraggable(noteElement.querySelector(".birb-window-header"), true, (top, left) => {
- stickyNote.top = top;
- stickyNote.left = left;
- onSave();
- });
-
- const closeButton = noteElement.querySelector(".birb-window-close");
- if (closeButton) {
- makeClosable(() => {
- if (confirm("Are you sure you want to delete this sticky note?")) {
- onDelete();
- noteElement.remove();
- }
- }, closeButton);
+ function makeMenuItem(item, removeMenuCallback) {
+ if (item instanceof Separator) {
+ return makeElement("birb-window-separator");
}
-
- const textarea = noteElement.querySelector(".birb-sticky-note-input");
- if (textarea && textarea instanceof HTMLTextAreaElement) {
- let saveTimeout;
- // Save after debounce
- textarea.addEventListener("input", () => {
- stickyNote.content = textarea.value;
- if (saveTimeout) {
- clearTimeout(saveTimeout);
- }
- saveTimeout = setTimeout(() => {
- onSave();
- }, 250);
- });
- }
-
- // On window resize
- window.addEventListener("resize", () => {
- const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`;
- const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`;
- noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`;
+ let menuItem = makeElement("birb-menu-item", item.text);
+ onClick(menuItem, () => {
+ if (item.removeMenu) {
+ removeMenuCallback();
+ }
+ item.action();
});
-
- return noteElement;
+ return menuItem;
}
/**
- * @param {StickyNote[]} stickyNotes
- * @param {() => void} onSave
- * @param {(note: StickyNote) => void} onDelete
+ * Add the menu to the page if it doesn't already exist
+ * @param {string} menuId
+ * @param {string} menuExitId
+ * @param {MenuItem[]} menuItems
+ * @param {string} title
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
*/
- function drawStickyNotes(stickyNotes, onSave, onDelete) {
- // Remove all existing sticky notes
- const existingNotes = document.querySelectorAll(".birb-sticky-note");
- existingNotes.forEach(note => note.remove());
- // Render all sticky notes
- for (let stickyNote of stickyNotes) {
- if (isStickyNoteApplicable(stickyNote)) {
- renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
+ function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
+ if (document.querySelector("#" + menuId)) {
+ return;
+ }
+ let menu = makeElement("birb-window", undefined, menuId);
+ let header = makeElement("birb-window-header");
+ header.innerHTML = `${title}
`;
+ let content = makeElement("birb-window-content");
+ const removeCallback = () => removeMenu(menuId, menuExitId);
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
}
}
+ menu.appendChild(header);
+ menu.appendChild(content);
+ document.body.appendChild(menu);
+ makeDraggable(document.querySelector(".birb-window-header"));
+
+ let menuExit = makeElement("birb-window-exit", undefined, menuExitId);
+ onClick(menuExit, removeCallback);
+ document.body.appendChild(menuExit);
+ makeClosable(removeCallback);
+
+ updateLocationCallback(menu);
}
/**
- * @param {StickyNote[]} stickyNotes
- * @param {() => void} onSave
- * @param {(note: StickyNote) => void} onDelete
+ * Remove the menu from the page
+ * @param {string} menuId
+ * @param {string} menuExitId
*/
- function createNewStickyNote(stickyNotes, onSave, onDelete) {
- const id = Date.now().toString();
- const site = window.location.href;
- const stickyNote = new StickyNote(id, site, "");
- const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
- element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
- element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
- stickyNote.top = parseInt(element.style.top, 10);
- stickyNote.left = parseInt(element.style.left, 10);
- stickyNotes.push(stickyNote);
- onSave();
+ function removeMenu(menuId, menuExitId) {
+ const menu = document.querySelector("#" + menuId);
+ if (menu) {
+ menu.remove();
+ }
+ const exitMenu = document.querySelector("#" + menuExitId);
+ if (exitMenu) {
+ exitMenu.remove();
+ }
+ }
+
+ /**
+ * @param {string} menuId
+ * @returns {boolean} Whether the menu element is on the page
+ */
+ function isMenuOpen(menuId) {
+ return document.querySelector("#" + menuId) !== null;
+ }
+
+ /**
+ * @param {string} menuId
+ * @param {MenuItem[]} menuItems
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
+ */
+ function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) {
+ const menu = document.querySelector("#" + menuId);
+ if (!menu || !(menu instanceof HTMLElement)) {
+ return;
+ }
+ const content = menu.querySelector(".birb-window-content");
+ if (!content) {
+ console.error("Birb: Content not found");
+ return;
+ }
+ content.innerHTML = "";
+ const removeCallback = () => removeMenu(menuId, menuId + "-exit");
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
+ }
+ }
+ updateLocationCallback(menu);
}
// @ts-ignore
@@ -1220,37 +1478,6 @@
]),
};
- class MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- * @param {boolean} [removeMenu]
- * @param {boolean} [isDebug]
- */
- constructor(text, action, removeMenu = true, isDebug = false) {
- this.text = text;
- this.action = action;
- this.removeMenu = removeMenu;
- this.isDebug = isDebug;
- }
- }
-
- class DebugMenuItem extends MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- */
- constructor(text, action, removeMenu = true) {
- super(text, action, removeMenu, true);
- }
- }
-
- class Separator extends MenuItem {
- constructor() {
- super("", () => { });
- }
- }
-
const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide),
@@ -1269,11 +1496,11 @@
debugMode = false;
}),
new Separator(),
- new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
+ new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
];
const settingsItems = [
- new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
+ new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
new Separator(),
new MenuItem("Toggle Birb Mode", () => {
userSettings.birbMode = !userSettings.birbMode;
@@ -1478,7 +1705,7 @@
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
- removeMenu();
+ removeMenu(MENU_ID, MENU_EXIT_ID);
}
});
@@ -1487,7 +1714,7 @@
// Currently being pet, don't open menu
return;
}
- insertMenu();
+ insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
});
canvas.addEventListener("mouseover", () => {
@@ -1534,7 +1761,7 @@
// Won't be restored on fullscreen exit
}
- if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
+ if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
@@ -1623,9 +1850,6 @@
function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
- if (textContent) {
- element.textContent = textContent;
- }
if (id) {
element.id = id;
}
@@ -1764,6 +1988,30 @@
centerElement(modal);
}
+ /**
+ * @param {HTMLElement} menu
+ */
+ function updateMenuLocation(menu) {
+ let x = birdX;
+ let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
+ const offset = 20;
+ if (x < window.innerWidth / 2) {
+ // Left side
+ x += offset;
+ } else {
+ // Right side
+ x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
+ }
+ if (y > window.innerHeight / 2) {
+ // Top side
+ y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
+ } else {
+ // Bottom side
+ y += offset;
+ }
+ menu.style.left = `${x}px`;
+ menu.style.top = `${y}px`;
+ }
function insertFieldGuide() {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
@@ -1879,103 +2127,6 @@
save();
}
- /**
- * Add the menu to the page if it doesn't already exist
- */
- function insertMenu() {
- if (document.querySelector("#" + MENU_ID)) {
- return;
- }
- let menu = makeElement("birb-window", undefined, MENU_ID);
- let header = makeElement("birb-window-header");
- header.innerHTML = `${birdBirb().toLowerCase()}OS
`;
- let content = makeElement("birb-window-content");
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- menu.appendChild(header);
- menu.appendChild(content);
- document.body.appendChild(menu);
- makeDraggable(document.querySelector(".birb-window-header"));
-
- let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
- onClick(menuExit, () => {
- removeMenu();
- });
- document.body.appendChild(menuExit);
- makeClosable(removeMenu);
-
- updateMenuLocation(menu);
- }
-
- /**
- * Update the menu's location based on the bird's position
- * @param {HTMLElement} menu
- */
- function updateMenuLocation(menu) {
- let x = birdX;
- let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
- const offset = 20;
- if (x < window.innerWidth / 2) {
- // Left side
- x += offset;
- } else {
- // Right side
- x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
- }
- if (y > window.innerHeight / 2) {
- // Top side
- y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
- } else {
- // Bottom side
- y += offset;
- }
- menu.style.left = `${x}px`;
- menu.style.top = `${y}px`;
- }
-
- /**
- * @param {MenuItem[]} menuItems
- */
- function switchMenuItems(menuItems) {
- const menu = document.querySelector("#" + MENU_ID);
- if (!menu || !(menu instanceof HTMLElement)) {
- return;
- }
- const content = menu.querySelector(".birb-window-content");
- if (!content) {
- error("Content not found");
- return;
- }
- content.innerHTML = "";
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- updateMenuLocation(menu);
- }
-
- /**
- * @param {MenuItem} item
- * @returns {HTMLElement}
- */
- function makeMenuItem(item) {
- if (item instanceof Separator) {
- return makeElement("birb-window-separator");
- }
- let menuItem = makeElement("birb-menu-item", item.text);
- onClick(menuItem, () => {
- if (item.removeMenu) {
- removeMenu();
- }
- item.action();
- });
- return menuItem;
- }
-
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
@@ -2001,27 +2152,6 @@
});
}
- /**
- * Remove the menu from the page
- */
- function removeMenu() {
- const menu = document.querySelector("#" + MENU_ID);
- if (menu) {
- menu.remove();
- }
- const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
- if (exitMenu) {
- exitMenu.remove();
- }
- }
-
- /**
- * @returns {boolean} Whether the menu element is on the page
- */
- function isMenuOpen() {
- return document.querySelector("#" + MENU_ID) !== null;
- }
-
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged
diff --git a/manifest.json b/manifest.json
index 894f595..e11775f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Pocket Bird",
"description": "It's a bird, in your browser. What more could you want?",
- "version": "2025.10.26.76",
+ "version": "2025.10.26.101",
"homepage_url": "https://idreesinc.com",
"content_scripts": [
{
diff --git a/src/birb.js b/src/birb.js
index 2484b25..aa3d425 100644
--- a/src/birb.js
+++ b/src/birb.js
@@ -13,6 +13,7 @@ import Frame from './frame.js';
import Layer from './layer.js';
import Anim from './anim.js';
import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js';
+import { MenuItem, DebugMenuItem, Separator, insertMenu, removeMenu, isMenuOpen, switchMenuItems } from './menu.js';
// @ts-ignore
const SHARED_CONFIG = {
@@ -271,37 +272,6 @@ Promise.all([
]),
};
- class MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- * @param {boolean} [removeMenu]
- * @param {boolean} [isDebug]
- */
- constructor(text, action, removeMenu = true, isDebug = false) {
- this.text = text;
- this.action = action;
- this.removeMenu = removeMenu;
- this.isDebug = isDebug;
- }
- }
-
- class DebugMenuItem extends MenuItem {
- /**
- * @param {string} text
- * @param {() => void} action
- */
- constructor(text, action, removeMenu = true) {
- super(text, action, removeMenu, true);
- }
- }
-
- class Separator extends MenuItem {
- constructor() {
- super("", () => { });
- }
- }
-
const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide),
@@ -320,11 +290,11 @@ Promise.all([
debugMode = false;
}),
new Separator(),
- new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
+ new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
];
const settingsItems = [
- new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
+ new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
new Separator(),
new MenuItem("Toggle Birb Mode", () => {
userSettings.birbMode = !userSettings.birbMode;
@@ -529,7 +499,7 @@ Promise.all([
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
- removeMenu();
+ removeMenu(MENU_ID, MENU_EXIT_ID);
}
});
@@ -538,7 +508,7 @@ Promise.all([
// Currently being pet, don't open menu
return;
}
- insertMenu();
+ insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
});
canvas.addEventListener("mouseover", () => {
@@ -585,7 +555,7 @@ Promise.all([
// Won't be restored on fullscreen exit
}
- if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
+ if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
@@ -835,6 +805,31 @@ Promise.all([
centerElement(modal);
}
+ /**
+ * @param {HTMLElement} menu
+ */
+ function updateMenuLocation(menu) {
+ let x = birdX;
+ let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
+ const offset = 20;
+ if (x < window.innerWidth / 2) {
+ // Left side
+ x += offset;
+ } else {
+ // Right side
+ x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
+ }
+ if (y > window.innerHeight / 2) {
+ // Top side
+ y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
+ } else {
+ // Bottom side
+ y += offset;
+ }
+ menu.style.left = `${x}px`;
+ menu.style.top = `${y}px`;
+ };
+
function insertFieldGuide() {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
@@ -954,103 +949,6 @@ Promise.all([
save();
}
- /**
- * Add the menu to the page if it doesn't already exist
- */
- function insertMenu() {
- if (document.querySelector("#" + MENU_ID)) {
- return;
- }
- let menu = makeElement("birb-window", undefined, MENU_ID);
- let header = makeElement("birb-window-header");
- header.innerHTML = `${birdBirb().toLowerCase()}OS
`;
- let content = makeElement("birb-window-content");
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- menu.appendChild(header);
- menu.appendChild(content);
- document.body.appendChild(menu);
- makeDraggable(document.querySelector(".birb-window-header"));
-
- let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
- onClick(menuExit, () => {
- removeMenu();
- });
- document.body.appendChild(menuExit);
- makeClosable(removeMenu);
-
- updateMenuLocation(menu);
- }
-
- /**
- * Update the menu's location based on the bird's position
- * @param {HTMLElement} menu
- */
- function updateMenuLocation(menu) {
- let x = birdX;
- let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
- const offset = 20;
- if (x < window.innerWidth / 2) {
- // Left side
- x += offset;
- } else {
- // Right side
- x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
- }
- if (y > window.innerHeight / 2) {
- // Top side
- y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
- } else {
- // Bottom side
- y += offset;
- }
- menu.style.left = `${x}px`;
- menu.style.top = `${y}px`;
- }
-
- /**
- * @param {MenuItem[]} menuItems
- */
- function switchMenuItems(menuItems) {
- const menu = document.querySelector("#" + MENU_ID);
- if (!menu || !(menu instanceof HTMLElement)) {
- return;
- }
- const content = menu.querySelector(".birb-window-content");
- if (!content) {
- error("Content not found");
- return;
- }
- content.innerHTML = "";
- for (const item of menuItems) {
- if (!item.isDebug || debugMode) {
- content.appendChild(makeMenuItem(item));
- }
- }
- updateMenuLocation(menu);
- }
-
- /**
- * @param {MenuItem} item
- * @returns {HTMLElement}
- */
- function makeMenuItem(item) {
- if (item instanceof Separator) {
- return makeElement("birb-window-separator");
- }
- let menuItem = makeElement("birb-menu-item", item.text);
- onClick(menuItem, () => {
- if (item.removeMenu) {
- removeMenu();
- }
- item.action();
- });
- return menuItem;
- }
-
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
@@ -1076,27 +974,6 @@ Promise.all([
});
}
- /**
- * Remove the menu from the page
- */
- function removeMenu() {
- const menu = document.querySelector("#" + MENU_ID);
- if (menu) {
- menu.remove();
- }
- const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
- if (exitMenu) {
- exitMenu.remove();
- }
- }
-
- /**
- * @returns {boolean} Whether the menu element is on the page
- */
- function isMenuOpen() {
- return document.querySelector("#" + MENU_ID) !== null;
- }
-
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged
diff --git a/src/menu.js b/src/menu.js
new file mode 100644
index 0000000..73f4e6f
--- /dev/null
+++ b/src/menu.js
@@ -0,0 +1,263 @@
+export class MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ * @param {boolean} [removeMenu]
+ * @param {boolean} [isDebug]
+ */
+ constructor(text, action, removeMenu = true, isDebug = false) {
+ this.text = text;
+ this.action = action;
+ this.removeMenu = removeMenu;
+ this.isDebug = isDebug;
+ }
+}
+
+export class DebugMenuItem extends MenuItem {
+ /**
+ * @param {string} text
+ * @param {() => void} action
+ */
+ constructor(text, action, removeMenu = true) {
+ super(text, action, removeMenu, true);
+ }
+}
+
+export class Separator extends MenuItem {
+ constructor() {
+ super("", () => { });
+ }
+}
+
+/**
+ * Create an HTML element with the specified parameters
+ * @param {string} className
+ * @param {string} [textContent]
+ * @param {string} [id]
+ * @returns {HTMLElement}
+ */
+function makeElement(className, textContent, id) {
+ const element = document.createElement("div");
+ element.classList.add(className);
+ if (textContent) {
+ element.textContent = textContent;
+ }
+ if (id) {
+ element.id = id;
+ }
+ return element;
+}
+
+/**
+ * @param {Document|Element} element
+ * @param {(e: Event) => void} action
+ */
+function onClick(element, action) {
+ element.addEventListener("click", (e) => action(e));
+ element.addEventListener("touchend", (e) => {
+ if (e instanceof TouchEvent === false) {
+ return;
+ } else if (element instanceof HTMLElement === false) {
+ return;
+ }
+ const touch = e.changedTouches[0];
+ const rect = element.getBoundingClientRect();
+ if (
+ touch.clientX >= rect.left &&
+ touch.clientX <= rect.right &&
+ touch.clientY >= rect.top &&
+ touch.clientY <= rect.bottom
+ ) {
+ action(e);
+ }
+ });
+}
+
+/**
+ * @param {HTMLElement|null} element The element to detect drag events on
+ * @param {boolean} [parent] Whether to move the parent element when the child is dragged
+ * @param {(top: number, left: number) => void} [callback] Callback for when element is moved
+ */
+function makeDraggable(element, parent = true, callback = () => { }) {
+ if (!element) {
+ return;
+ }
+
+ let isMouseDown = false;
+ let offsetX = 0;
+ let offsetY = 0;
+ let elementToMove = parent ? element.parentElement : element;
+
+ if (!elementToMove) {
+ console.error("Birb: Parent element not found");
+ return;
+ }
+
+ element.addEventListener("mousedown", (e) => {
+ isMouseDown = true;
+ offsetX = e.clientX - elementToMove.offsetLeft;
+ offsetY = e.clientY - elementToMove.offsetTop;
+ });
+
+ element.addEventListener("touchstart", (e) => {
+ isMouseDown = true;
+ const touch = e.touches[0];
+ offsetX = touch.clientX - elementToMove.offsetLeft;
+ offsetY = touch.clientY - elementToMove.offsetTop;
+ e.preventDefault();
+ });
+
+ document.addEventListener("mouseup", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("touchend", (e) => {
+ if (isMouseDown) {
+ callback(elementToMove.offsetTop, elementToMove.offsetLeft);
+ e.preventDefault();
+ }
+ isMouseDown = false;
+ });
+
+ document.addEventListener("mousemove", (e) => {
+ if (isMouseDown) {
+ elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
+ }
+ });
+
+ document.addEventListener("touchmove", (e) => {
+ if (isMouseDown) {
+ const touch = e.touches[0];
+ elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`;
+ elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`;
+ }
+ });
+}
+
+/**
+ * @param {() => void} func
+ * @param {Element} [closeButton]
+ */
+function makeClosable(func, closeButton) {
+ if (closeButton) {
+ onClick(closeButton, func);
+ }
+ document.addEventListener("keydown", (e) => {
+ if (closeButton && !document.body.contains(closeButton)) {
+ return;
+ }
+ if (e.key === "Escape") {
+ func();
+ }
+ });
+}
+
+/**
+ * @param {MenuItem} item
+ * @param {() => void} removeMenuCallback
+ * @returns {HTMLElement}
+ */
+function makeMenuItem(item, removeMenuCallback) {
+ if (item instanceof Separator) {
+ return makeElement("birb-window-separator");
+ }
+ let menuItem = makeElement("birb-menu-item", item.text);
+ onClick(menuItem, () => {
+ if (item.removeMenu) {
+ removeMenuCallback();
+ }
+ item.action();
+ });
+ return menuItem;
+}
+
+/**
+ * Add the menu to the page if it doesn't already exist
+ * @param {string} menuId
+ * @param {string} menuExitId
+ * @param {MenuItem[]} menuItems
+ * @param {string} title
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
+ */
+export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
+ if (document.querySelector("#" + menuId)) {
+ return;
+ }
+ let menu = makeElement("birb-window", undefined, menuId);
+ let header = makeElement("birb-window-header");
+ header.innerHTML = `${title}
`;
+ let content = makeElement("birb-window-content");
+ const removeCallback = () => removeMenu(menuId, menuExitId);
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
+ }
+ }
+ menu.appendChild(header);
+ menu.appendChild(content);
+ document.body.appendChild(menu);
+ makeDraggable(document.querySelector(".birb-window-header"));
+
+ let menuExit = makeElement("birb-window-exit", undefined, menuExitId);
+ onClick(menuExit, removeCallback);
+ document.body.appendChild(menuExit);
+ makeClosable(removeCallback);
+
+ updateLocationCallback(menu);
+}
+
+/**
+ * Remove the menu from the page
+ * @param {string} menuId
+ * @param {string} menuExitId
+ */
+export function removeMenu(menuId, menuExitId) {
+ const menu = document.querySelector("#" + menuId);
+ if (menu) {
+ menu.remove();
+ }
+ const exitMenu = document.querySelector("#" + menuExitId);
+ if (exitMenu) {
+ exitMenu.remove();
+ }
+}
+
+/**
+ * @param {string} menuId
+ * @returns {boolean} Whether the menu element is on the page
+ */
+export function isMenuOpen(menuId) {
+ return document.querySelector("#" + menuId) !== null;
+}
+
+/**
+ * @param {string} menuId
+ * @param {MenuItem[]} menuItems
+ * @param {boolean} debugMode
+ * @param {(menu: HTMLElement) => void} updateLocationCallback
+ */
+export function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) {
+ const menu = document.querySelector("#" + menuId);
+ if (!menu || !(menu instanceof HTMLElement)) {
+ return;
+ }
+ const content = menu.querySelector(".birb-window-content");
+ if (!content) {
+ console.error("Birb: Content not found");
+ return;
+ }
+ content.innerHTML = "";
+ const removeCallback = () => removeMenu(menuId, menuId + "-exit");
+ for (const item of menuItems) {
+ if (!item.isDebug || debugMode) {
+ content.appendChild(makeMenuItem(item, removeCallback));
+ }
+ }
+ updateLocationCallback(menu);
+}