mirror of
https://github.com/NohamR/Pocket-Bird.git
synced 2026-05-24 19:59:36 +00:00
Separate menu components
This commit is contained in:
620
dist/birb.js
vendored
620
dist/birb.js
vendored
@@ -393,6 +393,258 @@
|
|||||||
return true;
|
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 = `
|
||||||
|
<div class="birb-window-header">
|
||||||
|
<div class="birb-window-title">Sticky Note</div>
|
||||||
|
<div class="birb-window-close">x</div>
|
||||||
|
</div>
|
||||||
|
<div class="birb-window-content">
|
||||||
|
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
||||||
|
</div>`;
|
||||||
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -403,6 +655,12 @@
|
|||||||
function makeElement(className, textContent, id) {
|
function makeElement(className, textContent, id) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.classList.add(className);
|
element.classList.add(className);
|
||||||
|
if (textContent) {
|
||||||
|
element.textContent = textContent;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
element.id = id;
|
||||||
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,13 +760,7 @@
|
|||||||
* @param {Element} [closeButton]
|
* @param {Element} [closeButton]
|
||||||
*/
|
*/
|
||||||
function makeClosable(func, closeButton) {
|
function makeClosable(func, closeButton) {
|
||||||
if (closeButton) {
|
|
||||||
onClick(closeButton, func);
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (closeButton && !document.body.contains(closeButton)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
@@ -516,102 +768,108 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StickyNote} stickyNote
|
* @param {MenuItem} item
|
||||||
* @param {() => void} onSave
|
* @param {() => void} removeMenuCallback
|
||||||
* @param {() => void} onDelete
|
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
function renderStickyNote(stickyNote, onSave, onDelete) {
|
function makeMenuItem(item, removeMenuCallback) {
|
||||||
let html = `
|
if (item instanceof Separator) {
|
||||||
<div class="birb-window-header">
|
return makeElement("birb-window-separator");
|
||||||
<div class="birb-window-title">Sticky Note</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content">
|
|
||||||
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
|
||||||
</div>`;
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
let menuItem = makeElement("birb-menu-item", item.text);
|
||||||
const textarea = noteElement.querySelector(".birb-sticky-note-input");
|
onClick(menuItem, () => {
|
||||||
if (textarea && textarea instanceof HTMLTextAreaElement) {
|
if (item.removeMenu) {
|
||||||
let saveTimeout;
|
removeMenuCallback();
|
||||||
// Save after debounce
|
}
|
||||||
textarea.addEventListener("input", () => {
|
item.action();
|
||||||
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 menuItem;
|
||||||
return noteElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StickyNote[]} stickyNotes
|
* Add the menu to the page if it doesn't already exist
|
||||||
* @param {() => void} onSave
|
* @param {string} menuId
|
||||||
* @param {(note: StickyNote) => void} onDelete
|
* @param {string} menuExitId
|
||||||
|
* @param {MenuItem[]} menuItems
|
||||||
|
* @param {string} title
|
||||||
|
* @param {boolean} debugMode
|
||||||
|
* @param {(menu: HTMLElement) => void} updateLocationCallback
|
||||||
*/
|
*/
|
||||||
function drawStickyNotes(stickyNotes, onSave, onDelete) {
|
function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
|
||||||
// Remove all existing sticky notes
|
if (document.querySelector("#" + menuId)) {
|
||||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
return;
|
||||||
existingNotes.forEach(note => note.remove());
|
}
|
||||||
// Render all sticky notes
|
let menu = makeElement("birb-window", undefined, menuId);
|
||||||
for (let stickyNote of stickyNotes) {
|
let header = makeElement("birb-window-header");
|
||||||
if (isStickyNoteApplicable(stickyNote)) {
|
header.innerHTML = `<div class="birb-window-title">${title}</div>`;
|
||||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
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
|
* Remove the menu from the page
|
||||||
* @param {() => void} onSave
|
* @param {string} menuId
|
||||||
* @param {(note: StickyNote) => void} onDelete
|
* @param {string} menuExitId
|
||||||
*/
|
*/
|
||||||
function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
function removeMenu(menuId, menuExitId) {
|
||||||
const id = Date.now().toString();
|
const menu = document.querySelector("#" + menuId);
|
||||||
const site = window.location.href;
|
if (menu) {
|
||||||
const stickyNote = new StickyNote(id, site, "");
|
menu.remove();
|
||||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
}
|
||||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
const exitMenu = document.querySelector("#" + menuExitId);
|
||||||
element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
|
if (exitMenu) {
|
||||||
stickyNote.top = parseInt(element.style.top, 10);
|
exitMenu.remove();
|
||||||
stickyNote.left = parseInt(element.style.left, 10);
|
}
|
||||||
stickyNotes.push(stickyNote);
|
}
|
||||||
onSave();
|
|
||||||
|
/**
|
||||||
|
* @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
|
// @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 = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
@@ -1255,11 +1482,11 @@
|
|||||||
debugMode = false;
|
debugMode = false;
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
|
new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
|
||||||
];
|
];
|
||||||
|
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Toggle Birb Mode", () => {
|
new MenuItem("Toggle Birb Mode", () => {
|
||||||
userSettings.birbMode = !userSettings.birbMode;
|
userSettings.birbMode = !userSettings.birbMode;
|
||||||
@@ -1464,7 +1691,7 @@
|
|||||||
onClick(document, (e) => {
|
onClick(document, (e) => {
|
||||||
lastActionTimestamp = Date.now();
|
lastActionTimestamp = Date.now();
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
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
|
// Currently being pet, don't open menu
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
insertMenu();
|
insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
canvas.addEventListener("mouseover", () => {
|
||||||
@@ -1520,7 +1747,7 @@
|
|||||||
// Won't be restored on fullscreen exit
|
// 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) {
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
hop();
|
hop();
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
@@ -1609,9 +1836,6 @@
|
|||||||
function makeElement(className, textContent, id) {
|
function makeElement(className, textContent, id) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.classList.add(className);
|
element.classList.add(className);
|
||||||
if (textContent) {
|
|
||||||
element.textContent = textContent;
|
|
||||||
}
|
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
}
|
}
|
||||||
@@ -1750,6 +1974,30 @@
|
|||||||
centerElement(modal);
|
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() {
|
function insertFieldGuide() {
|
||||||
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
||||||
return;
|
return;
|
||||||
@@ -1865,103 +2113,6 @@
|
|||||||
save();
|
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 = `<div class="birb-window-title">${birdBirb().toLowerCase()}OS</div>`;
|
|
||||||
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 {Document|Element} element
|
||||||
* @param {(e: Event) => void} action
|
* @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 {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 {boolean} [parent] Whether to move the parent element when the child is dragged
|
||||||
|
|||||||
622
dist/birb.user.js
vendored
622
dist/birb.user.js
vendored
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Pocket Bird
|
// @name Pocket Bird
|
||||||
// @namespace https://idreesinc.com
|
// @namespace https://idreesinc.com
|
||||||
// @version 2025.10.26.76
|
// @version 2025.10.26.101
|
||||||
// @description birb
|
// @description birb
|
||||||
// @author Idrees
|
// @author Idrees
|
||||||
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
|
// @downloadURL https://github.com/IdreesInc/Pocket-Bird/raw/refs/heads/main/dist/birb.user.js
|
||||||
@@ -407,6 +407,258 @@
|
|||||||
return true;
|
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 = `
|
||||||
|
<div class="birb-window-header">
|
||||||
|
<div class="birb-window-title">Sticky Note</div>
|
||||||
|
<div class="birb-window-close">x</div>
|
||||||
|
</div>
|
||||||
|
<div class="birb-window-content">
|
||||||
|
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
||||||
|
</div>`;
|
||||||
|
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
|
* Create an HTML element with the specified parameters
|
||||||
* @param {string} className
|
* @param {string} className
|
||||||
@@ -417,6 +669,12 @@
|
|||||||
function makeElement(className, textContent, id) {
|
function makeElement(className, textContent, id) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.classList.add(className);
|
element.classList.add(className);
|
||||||
|
if (textContent) {
|
||||||
|
element.textContent = textContent;
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
element.id = id;
|
||||||
|
}
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,13 +774,7 @@
|
|||||||
* @param {Element} [closeButton]
|
* @param {Element} [closeButton]
|
||||||
*/
|
*/
|
||||||
function makeClosable(func, closeButton) {
|
function makeClosable(func, closeButton) {
|
||||||
if (closeButton) {
|
|
||||||
onClick(closeButton, func);
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (closeButton && !document.body.contains(closeButton)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
@@ -530,102 +782,108 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StickyNote} stickyNote
|
* @param {MenuItem} item
|
||||||
* @param {() => void} onSave
|
* @param {() => void} removeMenuCallback
|
||||||
* @param {() => void} onDelete
|
|
||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
function renderStickyNote(stickyNote, onSave, onDelete) {
|
function makeMenuItem(item, removeMenuCallback) {
|
||||||
let html = `
|
if (item instanceof Separator) {
|
||||||
<div class="birb-window-header">
|
return makeElement("birb-window-separator");
|
||||||
<div class="birb-window-title">Sticky Note</div>
|
|
||||||
<div class="birb-window-close">x</div>
|
|
||||||
</div>
|
|
||||||
<div class="birb-window-content">
|
|
||||||
<textarea class="birb-sticky-note-input" style="width: 150px;" placeholder="Write your notes here and they'll stick to the page!">${stickyNote.content}</textarea>
|
|
||||||
</div>`;
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
let menuItem = makeElement("birb-menu-item", item.text);
|
||||||
const textarea = noteElement.querySelector(".birb-sticky-note-input");
|
onClick(menuItem, () => {
|
||||||
if (textarea && textarea instanceof HTMLTextAreaElement) {
|
if (item.removeMenu) {
|
||||||
let saveTimeout;
|
removeMenuCallback();
|
||||||
// Save after debounce
|
}
|
||||||
textarea.addEventListener("input", () => {
|
item.action();
|
||||||
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 menuItem;
|
||||||
return noteElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {StickyNote[]} stickyNotes
|
* Add the menu to the page if it doesn't already exist
|
||||||
* @param {() => void} onSave
|
* @param {string} menuId
|
||||||
* @param {(note: StickyNote) => void} onDelete
|
* @param {string} menuExitId
|
||||||
|
* @param {MenuItem[]} menuItems
|
||||||
|
* @param {string} title
|
||||||
|
* @param {boolean} debugMode
|
||||||
|
* @param {(menu: HTMLElement) => void} updateLocationCallback
|
||||||
*/
|
*/
|
||||||
function drawStickyNotes(stickyNotes, onSave, onDelete) {
|
function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
|
||||||
// Remove all existing sticky notes
|
if (document.querySelector("#" + menuId)) {
|
||||||
const existingNotes = document.querySelectorAll(".birb-sticky-note");
|
return;
|
||||||
existingNotes.forEach(note => note.remove());
|
}
|
||||||
// Render all sticky notes
|
let menu = makeElement("birb-window", undefined, menuId);
|
||||||
for (let stickyNote of stickyNotes) {
|
let header = makeElement("birb-window-header");
|
||||||
if (isStickyNoteApplicable(stickyNote)) {
|
header.innerHTML = `<div class="birb-window-title">${title}</div>`;
|
||||||
renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
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
|
* Remove the menu from the page
|
||||||
* @param {() => void} onSave
|
* @param {string} menuId
|
||||||
* @param {(note: StickyNote) => void} onDelete
|
* @param {string} menuExitId
|
||||||
*/
|
*/
|
||||||
function createNewStickyNote(stickyNotes, onSave, onDelete) {
|
function removeMenu(menuId, menuExitId) {
|
||||||
const id = Date.now().toString();
|
const menu = document.querySelector("#" + menuId);
|
||||||
const site = window.location.href;
|
if (menu) {
|
||||||
const stickyNote = new StickyNote(id, site, "");
|
menu.remove();
|
||||||
const element = renderStickyNote(stickyNote, onSave, () => onDelete(stickyNote));
|
}
|
||||||
element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`;
|
const exitMenu = document.querySelector("#" + menuExitId);
|
||||||
element.style.top = `${window.scrollY + window.innerHeight / 2 - element.offsetHeight / 2}px`;
|
if (exitMenu) {
|
||||||
stickyNote.top = parseInt(element.style.top, 10);
|
exitMenu.remove();
|
||||||
stickyNote.left = parseInt(element.style.left, 10);
|
}
|
||||||
stickyNotes.push(stickyNote);
|
}
|
||||||
onSave();
|
|
||||||
|
/**
|
||||||
|
* @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
|
// @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 = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
@@ -1269,11 +1496,11 @@
|
|||||||
debugMode = false;
|
debugMode = false;
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
|
new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
|
||||||
];
|
];
|
||||||
|
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Toggle Birb Mode", () => {
|
new MenuItem("Toggle Birb Mode", () => {
|
||||||
userSettings.birbMode = !userSettings.birbMode;
|
userSettings.birbMode = !userSettings.birbMode;
|
||||||
@@ -1478,7 +1705,7 @@
|
|||||||
onClick(document, (e) => {
|
onClick(document, (e) => {
|
||||||
lastActionTimestamp = Date.now();
|
lastActionTimestamp = Date.now();
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
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
|
// Currently being pet, don't open menu
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
insertMenu();
|
insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
canvas.addEventListener("mouseover", () => {
|
||||||
@@ -1534,7 +1761,7 @@
|
|||||||
// Won't be restored on fullscreen exit
|
// 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) {
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
hop();
|
hop();
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
@@ -1623,9 +1850,6 @@
|
|||||||
function makeElement(className, textContent, id) {
|
function makeElement(className, textContent, id) {
|
||||||
const element = document.createElement("div");
|
const element = document.createElement("div");
|
||||||
element.classList.add(className);
|
element.classList.add(className);
|
||||||
if (textContent) {
|
|
||||||
element.textContent = textContent;
|
|
||||||
}
|
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
}
|
}
|
||||||
@@ -1764,6 +1988,30 @@
|
|||||||
centerElement(modal);
|
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() {
|
function insertFieldGuide() {
|
||||||
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
||||||
return;
|
return;
|
||||||
@@ -1879,103 +2127,6 @@
|
|||||||
save();
|
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 = `<div class="birb-window-title">${birdBirb().toLowerCase()}OS</div>`;
|
|
||||||
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 {Document|Element} element
|
||||||
* @param {(e: Event) => void} action
|
* @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 {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 {boolean} [parent] Whether to move the parent element when the child is dragged
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Pocket Bird",
|
"name": "Pocket Bird",
|
||||||
"description": "It's a bird, in your browser. What more could you want?",
|
"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",
|
"homepage_url": "https://idreesinc.com",
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
|
|||||||
185
src/birb.js
185
src/birb.js
@@ -13,6 +13,7 @@ import Frame from './frame.js';
|
|||||||
import Layer from './layer.js';
|
import Layer from './layer.js';
|
||||||
import Anim from './anim.js';
|
import Anim from './anim.js';
|
||||||
import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js';
|
import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js';
|
||||||
|
import { MenuItem, DebugMenuItem, Separator, insertMenu, removeMenu, isMenuOpen, switchMenuItems } from './menu.js';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const SHARED_CONFIG = {
|
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 = [
|
const menuItems = [
|
||||||
new MenuItem(`Pet ${birdBirb()}`, pet),
|
new MenuItem(`Pet ${birdBirb()}`, pet),
|
||||||
new MenuItem("Field Guide", insertFieldGuide),
|
new MenuItem("Field Guide", insertFieldGuide),
|
||||||
@@ -320,11 +290,11 @@ Promise.all([
|
|||||||
debugMode = false;
|
debugMode = false;
|
||||||
}),
|
}),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
|
new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
|
||||||
];
|
];
|
||||||
|
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
|
new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
|
||||||
new Separator(),
|
new Separator(),
|
||||||
new MenuItem("Toggle Birb Mode", () => {
|
new MenuItem("Toggle Birb Mode", () => {
|
||||||
userSettings.birbMode = !userSettings.birbMode;
|
userSettings.birbMode = !userSettings.birbMode;
|
||||||
@@ -529,7 +499,7 @@ Promise.all([
|
|||||||
onClick(document, (e) => {
|
onClick(document, (e) => {
|
||||||
lastActionTimestamp = Date.now();
|
lastActionTimestamp = Date.now();
|
||||||
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
|
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
|
// Currently being pet, don't open menu
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
insertMenu();
|
insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mouseover", () => {
|
canvas.addEventListener("mouseover", () => {
|
||||||
@@ -585,7 +555,7 @@ Promise.all([
|
|||||||
// Won't be restored on fullscreen exit
|
// 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) {
|
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
|
||||||
hop();
|
hop();
|
||||||
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
|
||||||
@@ -835,6 +805,31 @@ Promise.all([
|
|||||||
centerElement(modal);
|
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() {
|
function insertFieldGuide() {
|
||||||
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
|
||||||
return;
|
return;
|
||||||
@@ -954,103 +949,6 @@ Promise.all([
|
|||||||
save();
|
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 = `<div class="birb-window-title">${birdBirb().toLowerCase()}OS</div>`;
|
|
||||||
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 {Document|Element} element
|
||||||
* @param {(e: Event) => void} action
|
* @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 {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 {boolean} [parent] Whether to move the parent element when the child is dragged
|
||||||
|
|||||||
263
src/menu.js
Normal file
263
src/menu.js
Normal file
@@ -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 = `<div class="birb-window-title">${title}</div>`;
|
||||||
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user