Refresh Blue Marble UI

This commit is contained in:
Alexey 2026-04-21 09:31:11 +05:00
parent 49727505e0
commit 83c3f2d296
13 changed files with 1889 additions and 638 deletions

File diff suppressed because it is too large Load diff

View file

@ -2300,65 +2300,8 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
customElements.define("confetti-piece", BlueMarbleConfettiPiece);
// src/WindowCredits.js
var WindowCredts = class extends Overlay {
/** Constructor for the Credits window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @since 0.90.9
* @see {@link Overlay#constructor} for examples
*/
constructor(name2, version2) {
super(name2, version2);
this.window = null;
this.windowID = "bm-window-credits";
this.windowParent = document.body;
}
/** Spawns a Credits window.
* If another credits window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.90.9
*/
buildWindow() {
const ascii = `
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557
\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D
\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D
`;
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window" }, (instance, div) => {
}).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Credits"', "data-button-status": "expanded" }, (instance, button) => {
button.onclick = () => instance.handleMinimization(button);
button.ontouchend = () => {
button.click();
};
}).buildElement().addDiv().buildElement().addButton({ "class": "bm-button-circle", "textContent": "\u2716", "aria-label": 'Close window "Credits"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
};
button.ontouchend = () => {
button.click();
};
}).buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically" }).addHeader(1, { "textContent": "Credits" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-scrollable" }).addSpan({ "role": "img", "aria-label": this.name }).addSpan({ "innerHTML": ascii, "class": "bm-ascii", "aria-hidden": "true" }).buildElement().buildElement().addBr().buildElement().addHr().buildElement().addBr().buildElement().addSpan({ "textContent": '"Blue Marble" userscript is made by SwingTheVine.' }).buildElement().addBr().buildElement().addSpan({ "innerHTML": 'The <a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer">Blue Marble Website</a> is made by <a href="https://github.com/crqch" target="_blank" rel="noopener noreferrer">crqch</a>.' }).buildElement().addBr().buildElement().addSpan({ "textContent": `The Blue Marble Website used until ${localizeDate(new Date(1756069320 * 1e3))} was made by Camille Daguin.` }).buildElement().addBr().buildElement().addSpan({ "textContent": 'The favicon "Blue Marble" is owned by NASA. (The image of the Earth is owned by NASA)' }).buildElement().addBr().buildElement().addSpan({ "textContent": "Special Thanks:" }).buildElement().addUl().addLi({ "textContent": "Espresso, Meqa, and Robot for moderating SwingTheVine's community." }).buildElement().addLi({ "innerHTML": 'nof, <a href="https://github.com/TouchedByDarkness" target="_blank" rel="noopener noreferrer">darkness</a> for creating similar userscripts!' }).buildElement().addLi({ "innerHTML": '<a href="https://wondapon.net/" target="_blank" rel="noopener noreferrer">Wonda</a> for the Blue Marble banner image!' }).buildElement().addLi({ "innerHTML": '<a href="https://github.com/BullStein" target="_blank" rel="noopener noreferrer">BullStein</a>, <a href="https://github.com/allanf181" target="_blank" rel="noopener noreferrer">allanf181</a> for being early beta testers!' }).buildElement().addLi({ "innerHTML": 'guidu_ and <a href="https://github.com/Nick-machado" target="_blank" rel="noopener noreferrer">Nick-machado</a> for the original "Minimize" Button code!' }).buildElement().addLi({ "innerHTML": 'Nomad and <a href="https://www.youtube.com/@gustav_vv" target="_blank" rel="noopener noreferrer">Gustav</a> for the tutorials!' }).buildElement().addLi({ "innerHTML": '<a href="https://github.com/cfpwastaken" target="_blank" rel="noopener noreferrer">cfp</a> for creating the template overlay that Blue Marble was based on!' }).buildElement().addLi({ "innerHTML": '<a href="https://forcenetwork.cloud/" target="_blank" rel="noopener noreferrer">Force Network</a> for hosting the <a href="https://github.com/SwingTheVine/Wplace-TelemetryServer" target="_blank" rel="noopener noreferrer">telemetry server</a>!' }).buildElement().addLi({ "innerHTML": '<a href="https://thebluecorner.net" target="_blank" rel="noopener noreferrer">TheBlueCorner</a> for getting me interested in online pixel canvases!' }).buildElement().buildElement().addBr().buildElement().addSpan({ "innerHTML": '<a href="https://ko-fi.com/swingthevine" target="_blank" rel="noopener noreferrer">Donators</a>:' }).buildElement().addUl().addLi({ "textContent": "Soultree" }).buildElement().addLi({ "textContent": "Espresso" }).buildElement().addLi({ "textContent": "BEST FAN" }).buildElement().addLi({ "textContent": "FuchsDresden" }).buildElement().addLi({ "textContent": "Jack" }).buildElement().addLi({ "textContent": "raiken_au" }).buildElement().addLi({ "textContent": "Jacob" }).buildElement().addLi({ "textContent": "StupidOne" }).buildElement().addLi({ "textContent": "2 Anonymous Supporters" }).buildElement().buildElement().buildElement().buildElement().buildElement().buildOverlay(this.windowParent);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}
};
// src/WindowFilter.js
var _WindowFilter_instances, getWindowState_fn, prefersWindowedMode_fn, setWindowModePreference_fn, syncSortFormControls_fn, closeWindow_fn, cleanupWindowPersistence_fn, clampWindowDimension_fn, clampWindowPosition_fn, restoreWindowState_fn, saveWindowState_fn, scheduleWindowStateSave_fn, initializeWindowedPersistence_fn, buildColorList_fn, sortColorList_fn, selectColorList_fn, calculatePixelStatistics_fn;
var _WindowFilter_instances, getWindowState_fn, prefersWindowedMode_fn, setWindowModePreference_fn, syncSortFormControls_fn, closeWindow_fn, startAutoRefresh_fn, stopAutoRefresh_fn, cleanupWindowPersistence_fn, clampWindowDimension_fn, clampWindowPosition_fn, restoreWindowState_fn, saveWindowState_fn, scheduleWindowStateSave_fn, initializeWindowedPersistence_fn, buildColorList_fn, sortColorList_fn, selectColorList_fn, calculatePixelStatistics_fn;
var WindowFilter = class extends Overlay {
/** Constructor for the color filter window
* @param {*} executor - The executing class
@ -2378,6 +2321,8 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.windowResizeObserver = null;
this.windowViewportResizeHandler = null;
this.windowSaveTimeout = null;
this.colorRefreshInterval = null;
this.colorRefreshIntervalMS = 1e4;
this.windowMinWidth = 260;
this.windowMinHeight = 220;
this.windowMaxWidth = 1e3;
@ -2439,17 +2384,11 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
button.ontouchend = () => {
button.click();
};
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically" }).addHeader(1, { "textContent": "Color Filter" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-flex-between bm-center-vertically", "style": "gap: 1.5ch;" }).addButton({ "textContent": "Hide All Colors" }, (instance, button) => {
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically bm-filter-header" }).addHeader(1, { "textContent": "Color Filter" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-flex-between bm-center-vertically bm-filter-toolbar", "style": "gap: 1.5ch;" }).addButton({ "class": "bm-button-secondary", "textContent": "Hide All Colors" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _WindowFilter_instances, selectColorList_fn).call(this, false);
}).buildElement().addButton({ "textContent": "Refresh Data" }, (instance, button) => {
button.onclick = () => {
button.disabled = true;
this.updateColorList();
button.disabled = false;
};
}).buildElement().addButton({ "textContent": "Show All Colors" }, (instance, button) => {
}).buildElement().addButton({ "class": "bm-button-secondary", "textContent": "Show All Colors" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _WindowFilter_instances, selectColorList_fn).call(this, true);
}).buildElement().buildElement().addDiv({ "class": "bm-container bm-scrollable" }).addDiv({ "class": "bm-container", "style": "margin-left: 2.5ch; margin-right: 2.5ch;" }).addDiv({ "class": "bm-container" }).addSpan({ "id": "bm-filter-tile-load", "innerHTML": "<b>Tiles Loaded:</b> 0 / ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-correct", "innerHTML": "<b>Correct Pixels:</b> ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-total", "innerHTML": "<b>Total Pixels:</b> ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-remaining", "innerHTML": "<b>Complete:</b> ??? (???)" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-completed", "innerHTML": "??? ???" }).buildElement().buildElement().addDiv({ "class": "bm-container" }).addP({ "innerHTML": `Press the \u{1F5D7} button to make this window smaller. Colors with the icon ${this.eyeOpen.replace("<svg", '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace("<svg", '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.` }).buildElement().buildElement().addHr().buildElement().addForm({ "class": "bm-container" }).addFieldset().addLegend({ "textContent": "Sort Options:", "style": "font-weight: 700;" }).buildElement().addDiv({ "class": "bm-container" }).addSelect({ "id": "bm-filter-sort-primary", "name": "sortPrimary", "textContent": "I want to view " }).addOption({ "value": "id", "textContent": "color IDs" }).buildElement().addOption({ "value": "name", "textContent": "color names" }).buildElement().addOption({ "value": "premium", "textContent": "premium colors" }).buildElement().addOption({ "value": "percent", "textContent": "percentage" }).buildElement().addOption({ "value": "correct", "textContent": "correct pixels" }).buildElement().addOption({ "value": "incorrect", "textContent": "incorrect pixels" }).buildElement().addOption({ "value": "total", "textContent": "total pixels" }).buildElement().buildElement().addSelect({ "id": "bm-filter-sort-secondary", "name": "sortSecondary", "textContent": " in " }).addOption({ "value": "ascending", "textContent": "ascending" }).buildElement().addOption({ "value": "descending", "textContent": "descending" }).buildElement().buildElement().addSpan({ "textContent": " order." }).buildElement().buildElement().addDiv({ "class": "bm-container" }).addCheckbox({ "id": "bm-filter-show-unused", "name": "showUnused", "textContent": "Show unused colors" }).buildElement().buildElement().buildElement().addDiv({ "class": "bm-container" }).addButton({ "textContent": "Sort Colors", "type": "submit" }, (instance, button) => {
}).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-scrollable bm-filter-scrollable" }).addDiv({ "class": "bm-container bm-filter-insights", "style": "margin-left: 2.5ch; margin-right: 2.5ch;" }).addDiv({ "class": "bm-container bm-filter-stats-card" }).addSpan({ "id": "bm-filter-tile-load", "innerHTML": "<b>Tiles Loaded:</b> 0 / ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-correct", "innerHTML": "<b>Correct Pixels:</b> ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-total", "innerHTML": "<b>Total Pixels:</b> ???" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-remaining", "innerHTML": "<b>Complete:</b> ??? (???)" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-filter-tot-completed", "innerHTML": "??? ???" }).buildElement().buildElement().addDiv({ "class": "bm-container bm-filter-note" }).addP({ "innerHTML": `Press the \u{1F5D7} button to make this window smaller. Colors with the icon ${this.eyeOpen.replace("<svg", '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace("<svg", '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.` }).buildElement().buildElement().addHr().buildElement().addForm({ "class": "bm-container bm-filter-sort-panel" }).addFieldset().addLegend({ "textContent": "Sort Options:", "style": "font-weight: 700;" }).buildElement().addDiv({ "class": "bm-container" }).addSelect({ "id": "bm-filter-sort-primary", "name": "sortPrimary", "textContent": "I want to view " }).addOption({ "value": "id", "textContent": "color IDs" }).buildElement().addOption({ "value": "name", "textContent": "color names" }).buildElement().addOption({ "value": "premium", "textContent": "premium colors" }).buildElement().addOption({ "value": "percent", "textContent": "percentage" }).buildElement().addOption({ "value": "correct", "textContent": "correct pixels" }).buildElement().addOption({ "value": "incorrect", "textContent": "incorrect pixels" }).buildElement().addOption({ "value": "total", "textContent": "total pixels" }).buildElement().buildElement().addSelect({ "id": "bm-filter-sort-secondary", "name": "sortSecondary", "textContent": " in " }).addOption({ "value": "ascending", "textContent": "ascending" }).buildElement().addOption({ "value": "descending", "textContent": "descending" }).buildElement().buildElement().addSpan({ "textContent": " order." }).buildElement().buildElement().addDiv({ "class": "bm-container" }).addCheckbox({ "id": "bm-filter-show-unused", "name": "showUnused", "textContent": "Show unused colors" }).buildElement().buildElement().buildElement().addDiv({ "class": "bm-container bm-filter-sort-actions" }).addButton({ "class": "bm-button-primary", "textContent": "Sort Colors", "type": "submit" }, (instance, button) => {
button.onclick = (event) => {
event.preventDefault();
const formData = new FormData(document.querySelector(`#${this.windowID} form`));
@ -2471,6 +2410,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.updateInnerHTML("#bm-filter-tot-total", `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
this.updateInnerHTML("#bm-filter-tot-remaining", `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
this.updateInnerHTML("#bm-filter-tot-completed", `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, "Z")}">${this.timeRemainingLocalized}</time>`);
__privateMethod(this, _WindowFilter_instances, startAutoRefresh_fn).call(this);
}
/** Spawns a windowed Color Filter window.
* If another color filter window already exists, we DON'T spawn another!
@ -2511,17 +2451,11 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
button.ontouchend = () => {
button.click();
};
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically" }).addHeader(1, { "textContent": "Color Filter" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-flex-between bm-center-vertically", "style": "gap: 1.5ch;" }).addButton({ "textContent": "None" }, (instance, button) => {
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically bm-filter-header" }).addHeader(1, { "textContent": "Color Filter" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-flex-between bm-center-vertically bm-filter-toolbar", "style": "gap: 1.5ch;" }).addButton({ "class": "bm-button-secondary", "textContent": "None" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _WindowFilter_instances, selectColorList_fn).call(this, false);
}).buildElement().addButton({ "textContent": "Refresh" }, (instance, button) => {
button.onclick = () => {
button.disabled = true;
this.updateColorList();
button.disabled = false;
};
}).buildElement().addButton({ "textContent": "All" }, (instance, button) => {
}).buildElement().addButton({ "class": "bm-button-secondary", "textContent": "All" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _WindowFilter_instances, selectColorList_fn).call(this, true);
}).buildElement().buildElement().addDiv({ "class": "bm-container bm-scrollable" }).buildElement().buildElement().addDiv({
}).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-scrollable bm-filter-scrollable" }).buildElement().buildElement().addDiv({
"class": "bm-resize-corner",
"title": "Resize Color Filter window",
"aria-label": "Resize Color Filter window",
@ -2534,6 +2468,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
__privateMethod(this, _WindowFilter_instances, buildColorList_fn).call(this, scrollableContainer);
__privateMethod(this, _WindowFilter_instances, syncSortFormControls_fn).call(this);
__privateMethod(this, _WindowFilter_instances, sortColorList_fn).call(this, this.sortPrimary, this.sortSecondary, this.showUnused);
__privateMethod(this, _WindowFilter_instances, startAutoRefresh_fn).call(this);
}
/** The information about a specific color on the palette.
* @typedef {Object} ColorData
@ -2584,6 +2519,11 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
const allTotal = this.allPixelsTotal.toString().length > 7 ? this.allPixelsTotal.toString().slice(0, 2) + "\u2026" + this.allPixelsTotal.toString().slice(-3) : this.allPixelsTotal.toString();
this.updateInnerHTML("#bm-filter-windowed-color-totals", `${allCorrect}/${allTotal}`, true);
}
this.updateInnerHTML("#bm-filter-tile-load", `<b>Tiles Loaded:</b> ${localizeNumber(this.tilesLoadedTotal)} / ${localizeNumber(this.tilesTotal)}`);
this.updateInnerHTML("#bm-filter-tot-correct", `<b>Correct Pixels:</b> ${localizeNumber(this.allPixelsCorrectTotal)}`);
this.updateInnerHTML("#bm-filter-tot-total", `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
this.updateInnerHTML("#bm-filter-tot-remaining", `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
this.updateInnerHTML("#bm-filter-tot-completed", `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, "Z")}">${this.timeRemainingLocalized}</time>`);
if (!colorList) {
return colorStatistics;
}
@ -2682,9 +2622,33 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
if (windowElement?.classList.contains("bm-windowed")) {
__privateMethod(this, _WindowFilter_instances, saveWindowState_fn).call(this, windowElement);
}
__privateMethod(this, _WindowFilter_instances, stopAutoRefresh_fn).call(this);
__privateMethod(this, _WindowFilter_instances, cleanupWindowPersistence_fn).call(this);
windowElement?.remove();
};
/** Starts the automatic Color Filter statistics refresh loop.
* @since 0.92.1
*/
startAutoRefresh_fn = function() {
__privateMethod(this, _WindowFilter_instances, stopAutoRefresh_fn).call(this);
this.colorRefreshInterval = setInterval(() => {
if (!document.querySelector(`#${this.windowID}`)) {
__privateMethod(this, _WindowFilter_instances, stopAutoRefresh_fn).call(this);
return;
}
this.updateColorList();
}, this.colorRefreshIntervalMS);
};
/** Stops the automatic Color Filter statistics refresh loop.
* @since 0.92.1
*/
stopAutoRefresh_fn = function() {
if (!this.colorRefreshInterval) {
return;
}
clearInterval(this.colorRefreshInterval);
this.colorRefreshInterval = null;
};
/** Disconnects live observers used for window persistence.
* @since 0.92.0
*/
@ -3051,6 +3015,164 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.timeRemainingLocalized = localizeDate(this.timeRemaining);
};
// src/WindowMain.js
var _WindowMain_instances, coordinateInputPaste_fn;
var WindowMain = class extends Overlay {
/** Constructor for the main Blue Marble window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @since 0.88.326
* @see {@link Overlay#constructor}
*/
constructor(name2, version2) {
super(name2, version2);
__privateAdd(this, _WindowMain_instances);
this.window = null;
this.windowID = "bm-window-main";
this.windowParent = document.body;
}
/** Creates the main Blue Marble window.
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.58.3
*/
buildWindow() {
if (document.querySelector(`#${this.windowID}`)) {
this.handleDisplayError("Main window already exists!");
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window bm-windowed", "style": "top: 10px; left: unset; right: 75px;" }, (instance, div) => {
}).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Blue Marble"', "data-button-status": "expanded" }, (instance, button) => {
button.onclick = () => instance.handleMinimization(button);
button.ontouchend = () => {
button.click();
};
}).buildElement().addDiv().buildElement().addDiv({ "class": "bm-flex-center" }).addButton({ "class": "bm-button-circle", "innerHTML": "\u2699\uFE0F", "title": "Settings" }, (instance, button) => {
button.onclick = () => {
instance.settingsManager.buildWindow();
};
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-main-hero" }).addImg({ "class": "bm-favicon", "src": "https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png" }, (instance, img) => {
const date = /* @__PURE__ */ new Date();
const dayOfTheYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 1)) / (1e3 * 60 * 60 * 24)) + 1;
if (dayOfTheYear == 204) {
img.parentNode.style.position = "relative";
img.parentNode.innerHTML = img.parentNode.innerHTML + `<svg viewBox="0 0 9 7" width="2em" height="2em" style="position: absolute; top: -.75em; left: 3.25ch;"><path d="M0,3L9,0L2,7" fill="#0af"/><path d="M0,3A.4,.4 0 1 1 1,5" fill="#a00"/><path d="M1.5,6A1,1 0 0 1 3,6L2,7" fill="#a0f"/><path d="M4,5A.6,.6 0 1 1 5,4" fill="#0a0"/><path d="M6,3A.8,.8 0 1 1 7,2" fill="#fa0"/><path d="M4.5,1.5A1,1 0 0 1 3,2" fill="#aa0"/></svg>`;
img.onload = () => {
const confettiManager = new ConfettiManager();
confettiManager.createConfetti(document.querySelector(`#${this.windowID}`));
};
}
}).buildElement().addHeader(1, { "textContent": this.name }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-main-stats" }).addDiv({ "class": "bm-main-stat-card bm-main-stat-card-value" }).addSpan({ "class": "bm-main-stat-label", "textContent": "Droplets" }).buildElement().addSpan({ "id": "bm-user-droplets", "class": "bm-main-stat-value", "textContent": "0" }).buildElement().buildElement().addDiv({ "class": "bm-main-stat-card bm-main-stat-card-value" }).addSpan({ "class": "bm-main-stat-label", "textContent": "Next Level" }).buildElement().addSpan({ "id": "bm-user-nextlevel", "class": "bm-main-stat-value", "textContent": "0 px" }).buildElement().buildElement().addDiv({ "class": "bm-main-stat-card bm-main-stat-card-timer" }).addSpan({ "class": "bm-main-stat-label", "textContent": "Charges" }).buildElement().addTimer(Date.now(), 1e3, { "class": "bm-main-stat-value", "style": "font-weight: 700;" }, (instance, timer) => {
instance.apiManager.chargeRefillTimerID = timer.id;
}).buildElement().buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-main-shell" }).addDiv({ "class": "bm-container bm-main-coords" }).addButton(
{ "class": "bm-button-circle bm-button-pin", "style": "margin-top: 0;", "innerHTML": '<svg viewBox="0 0 4 6"><path d="M.5,3.4A2,2 0 1 1 3.5,3.4L2,6"/><circle cx="2" cy="2" r=".7" fill="#fff"/></svg>' },
(instance, button) => {
button.onclick = () => {
const coords2 = instance.apiManager?.coordsTilePixel;
if (!coords2?.[0]) {
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
instance.updateInnerHTML("bm-input-tx", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-ty", coords2?.[1] || "");
instance.updateInnerHTML("bm-input-px", coords2?.[2] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[3] || "");
};
}
).buildElement().addInput({ "type": "number", "id": "bm-input-tx", "class": "bm-input-coords", "placeholder": "Tl X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-ty", "class": "bm-input-coords", "placeholder": "Tl Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-px", "class": "bm-input-coords", "placeholder": "Px X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-py", "class": "bm-input-coords", "placeholder": "Px Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().buildElement().addDiv({ "class": "bm-container bm-main-upload" }).addInputFile({ "class": "bm-input-file", "textContent": "Upload Template", "accept": "image/png, image/jpeg, image/webp, image/bmp, image/gif" }).buildElement().buildElement().addDiv({ "class": "bm-container bm-flex-between bm-main-actions" }).addButton({ "class": "bm-button-secondary", "textContent": "Disable", "data-button-status": "shown" }, (instance, button) => {
button.onclick = () => {
button.disabled = true;
if (button.dataset["buttonStatus"] == "shown") {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(false);
button.dataset["buttonStatus"] = "hidden";
button.textContent = "Enable";
instance.handleDisplayStatus(`Disabled templates!`);
} else {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(true);
button.dataset["buttonStatus"] = "shown";
button.textContent = "Disable";
instance.handleDisplayStatus(`Enabled templates!`);
}
button.disabled = false;
};
}).buildElement().addButton({ "class": "bm-button-primary", "textContent": "Create" }, (instance, button) => {
button.onclick = () => {
const input = document.querySelector(`#${this.windowID} .bm-input-file`);
const coordTlX = document.querySelector("#bm-input-tx");
if (!coordTlX.checkValidity()) {
coordTlX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordTlY = document.querySelector("#bm-input-ty");
if (!coordTlY.checkValidity()) {
coordTlY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxX = document.querySelector("#bm-input-px");
if (!coordPxX.checkValidity()) {
coordPxX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxY = document.querySelector("#bm-input-py");
if (!coordPxY.checkValidity()) {
coordPxY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
if (!input?.files[0]) {
instance.handleDisplayError(`No file selected!`);
return;
}
instance?.apiManager?.templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ""), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
instance.handleDisplayStatus(`Drew to canvas!`);
};
}).buildElement().addButton({ "class": "bm-button-secondary", "textContent": "Filter" }, (instance, button) => {
button.onclick = () => this.buildWindowFilter();
}).buildElement().buildElement().addDiv({ "class": "bm-container bm-main-status" }).addTextarea({ "id": this.outputStatusId, "placeholder": `Status: Sleeping...
Version: ${this.version}`, "readOnly": true }).buildElement().buildElement().buildElement().buildElement().buildElement().buildOverlay(this.windowParent);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}
/** Displays a new color filter window.
* This is a helper function that creates a new class instance.
* This might cause a memory leak. I pray that this is not the case...
* @since 0.88.330
*/
buildWindowFilter() {
const windowFilter = new WindowFilter(this);
windowFilter.buildPreferredWindow();
}
};
_WindowMain_instances = new WeakSet();
coordinateInputPaste_fn = async function(instance, input, event) {
event.preventDefault();
const data = await getClipboardData(event);
const coords2 = data.split(/[^a-zA-Z0-9]+/).filter((index) => index).map(Number).filter(
(number) => !isNaN(number)
// Removes NaN `[4]`
);
if (coords2.length == 2 && input.id == "bm-input-px") {
instance.updateInnerHTML("bm-input-px", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[1] || "");
} else if (coords2.length == 1) {
instance.updateInnerHTML(input.id, coords2?.[0] || "");
} else {
instance.updateInnerHTML("bm-input-tx", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-ty", coords2?.[1] || "");
instance.updateInnerHTML("bm-input-px", coords2?.[2] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[3] || "");
}
};
// src/WindowWizard.js
var _WindowWizard_instances, displaySchemaHealth_fn, displayTemplateList_fn, convertSchema_1_x_x_To_2_x_x_fn;
var _WindowWizard = class _WindowWizard extends Overlay {
@ -3216,187 +3338,6 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
var WindowWizard = _WindowWizard;
// src/WindowMain.js
var _WindowMain_instances, coordinateInputPaste_fn;
var WindowMain = class extends Overlay {
/** Constructor for the main Blue Marble window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @since 0.88.326
* @see {@link Overlay#constructor}
*/
constructor(name2, version2) {
super(name2, version2);
__privateAdd(this, _WindowMain_instances);
this.window = null;
this.windowID = "bm-window-main";
this.windowParent = document.body;
}
/** Creates the main Blue Marble window.
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.58.3
*/
buildWindow() {
if (document.querySelector(`#${this.windowID}`)) {
this.handleDisplayError("Main window already exists!");
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window bm-windowed", "style": "top: 10px; left: unset; right: 75px;" }, (instance, div) => {
}).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Blue Marble"', "data-button-status": "expanded" }, (instance, button) => {
button.onclick = () => instance.handleMinimization(button);
button.ontouchend = () => {
button.click();
};
}).buildElement().addDiv().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container" }).addImg({ "class": "bm-favicon", "src": "https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png" }, (instance, img) => {
const date = /* @__PURE__ */ new Date();
const dayOfTheYear = Math.floor((date.getTime() - new Date(date.getFullYear(), 0, 1)) / (1e3 * 60 * 60 * 24)) + 1;
if (dayOfTheYear == 204) {
img.parentNode.style.position = "relative";
img.parentNode.innerHTML = img.parentNode.innerHTML + `<svg viewBox="0 0 9 7" width="2em" height="2em" style="position: absolute; top: -.75em; left: 3.25ch;"><path d="M0,3L9,0L2,7" fill="#0af"/><path d="M0,3A.4,.4 0 1 1 1,5" fill="#a00"/><path d="M1.5,6A1,1 0 0 1 3,6L2,7" fill="#a0f"/><path d="M4,5A.6,.6 0 1 1 5,4" fill="#0a0"/><path d="M6,3A.8,.8 0 1 1 7,2" fill="#fa0"/><path d="M4.5,1.5A1,1 0 0 1 3,2" fill="#aa0"/></svg>`;
img.onload = () => {
const confettiManager = new ConfettiManager();
confettiManager.createConfetti(document.querySelector(`#${this.windowID}`));
};
}
}).buildElement().addHeader(1, { "textContent": this.name }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container" }).addSpan({ "id": "bm-user-droplets", "textContent": "Droplets:" }).buildElement().addBr().buildElement().addSpan({ "id": "bm-user-nextlevel", "textContent": "Next level in..." }).buildElement().addBr().buildElement().addSpan({ "textContent": "Charges: " }).addTimer(Date.now(), 1e3, { "style": "font-weight: 700;" }, (instance, timer) => {
instance.apiManager.chargeRefillTimerID = timer.id;
}).buildElement().buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container" }).addDiv({ "class": "bm-container" }).addButton(
{ "class": "bm-button-circle bm-button-pin", "style": "margin-top: 0;", "innerHTML": '<svg viewBox="0 0 4 6"><path d="M.5,3.4A2,2 0 1 1 3.5,3.4L2,6"/><circle cx="2" cy="2" r=".7" fill="#fff"/></svg>' },
(instance, button) => {
button.onclick = () => {
const coords2 = instance.apiManager?.coordsTilePixel;
if (!coords2?.[0]) {
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
instance.updateInnerHTML("bm-input-tx", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-ty", coords2?.[1] || "");
instance.updateInnerHTML("bm-input-px", coords2?.[2] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[3] || "");
};
}
).buildElement().addInput({ "type": "number", "id": "bm-input-tx", "class": "bm-input-coords", "placeholder": "Tl X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-ty", "class": "bm-input-coords", "placeholder": "Tl Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-px", "class": "bm-input-coords", "placeholder": "Px X", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().addInput({ "type": "number", "id": "bm-input-py", "class": "bm-input-coords", "placeholder": "Px Y", "min": 0, "max": 2047, "step": 1, "required": true }, (instance, input) => {
input.addEventListener("paste", (event) => __privateMethod(this, _WindowMain_instances, coordinateInputPaste_fn).call(this, instance, input, event));
}).buildElement().buildElement().addDiv({ "class": "bm-container" }).addInputFile({ "class": "bm-input-file", "textContent": "Upload Template", "accept": "image/png, image/jpeg, image/webp, image/bmp, image/gif" }).buildElement().buildElement().addDiv({ "class": "bm-container bm-flex-between" }).addButton({ "textContent": "Disable", "data-button-status": "shown" }, (instance, button) => {
button.onclick = () => {
button.disabled = true;
if (button.dataset["buttonStatus"] == "shown") {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(false);
button.dataset["buttonStatus"] = "hidden";
button.textContent = "Enable";
instance.handleDisplayStatus(`Disabled templates!`);
} else {
instance.apiManager?.templateManager?.setTemplatesShouldBeDrawn(true);
button.dataset["buttonStatus"] = "shown";
button.textContent = "Disable";
instance.handleDisplayStatus(`Enabled templates!`);
}
button.disabled = false;
};
}).buildElement().addButton({ "textContent": "Create" }, (instance, button) => {
button.onclick = () => {
const input = document.querySelector(`#${this.windowID} .bm-input-file`);
const coordTlX = document.querySelector("#bm-input-tx");
if (!coordTlX.checkValidity()) {
coordTlX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordTlY = document.querySelector("#bm-input-ty");
if (!coordTlY.checkValidity()) {
coordTlY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxX = document.querySelector("#bm-input-px");
if (!coordPxX.checkValidity()) {
coordPxX.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
const coordPxY = document.querySelector("#bm-input-py");
if (!coordPxY.checkValidity()) {
coordPxY.reportValidity();
instance.handleDisplayError("Coordinates are malformed! Did you try clicking on the canvas first?");
return;
}
if (!input?.files[0]) {
instance.handleDisplayError(`No file selected!`);
return;
}
instance?.apiManager?.templateManager.createTemplate(input.files[0], input.files[0]?.name.replace(/\.[^/.]+$/, ""), [Number(coordTlX.value), Number(coordTlY.value), Number(coordPxX.value), Number(coordPxY.value)]);
instance.handleDisplayStatus(`Drew to canvas!`);
};
}).buildElement().addButton({ "textContent": "Filter" }, (instance, button) => {
button.onclick = () => this.buildWindowFilter();
}).buildElement().buildElement().addDiv({ "class": "bm-container" }).addTextarea({ "id": this.outputStatusId, "placeholder": `Status: Sleeping...
Version: ${this.version}`, "readOnly": true }).buildElement().buildElement().addDiv({ "class": "bm-container bm-flex-between", "style": "margin-bottom: 0; flex-direction: column;" }).addDiv({ "class": "bm-flex-between" }).addButton({ "class": "bm-button-circle", "innerHTML": "\u2699\uFE0F", "title": "Settings" }, (instance, button) => {
button.onclick = () => {
instance.settingsManager.buildWindow();
};
}).buildElement().addButton({ "class": "bm-button-circle", "innerHTML": "\u{1F9D9}", "title": "Template Wizard" }, (instance, button) => {
button.onclick = () => {
const templateManager2 = instance.apiManager?.templateManager;
const wizard = new WindowWizard(this.name, this.version, templateManager2?.schemaVersion, templateManager2);
wizard.buildWindow();
};
}).buildElement().addButton({ "class": "bm-button-circle", "innerHTML": "\u{1F3A8}", "title": "Template Color Converter" }, (instance, button) => {
button.onclick = () => {
window.open("https://pepoafonso.github.io/color_converter_wplace/", "_blank", "noopener noreferrer");
};
}).buildElement().addButton({ "class": "bm-button-circle", "innerHTML": "\u{1F310}", "title": "Official Blue Marble Website" }, (instance, button) => {
button.onclick = () => {
window.open("https://bluemarble.lol/", "_blank", "noopener noreferrer");
};
}).buildElement().addButton({ "class": "bm-button-circle", "title": "Donate to SwingTheVine", "innerHTML": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#fff" style="width:80%; margin:auto;"><path d="M249.8 75c89.8 0 113 1.1 146.3 4.4 78.1 7.8 123.6 56 123.6 125.2l0 8.9c0 64.3-47.1 116.9-110.8 122.4-5 16.6-12.8 33.2-23.3 49.9-24.4 37.7-73.1 85.3-162.9 85.3l-17.7 0c-73.1 0-129.7-31.6-163.5-89.2-29.9-50.4-33.8-106.4-33.8-181.2 0-73.7 44.4-113.6 96.4-120.2 39.3-5 88.1-5.5 145.7-5.5zm0 41.6c-60.4 0-103.6 .5-136.3 5.5-46 6.7-64.3 32.7-64.3 79.2l.2 25.7c1.2 57.3 7.1 97.1 27.5 134.5 26.6 49.3 74.8 68.2 129.7 68.2l17.2 0c72 0 107-34.9 126.3-65.4 9.4-15.5 17.7-32.7 22.2-54.3l3.3-13.8 19.9 0c44.3 0 82.6-36 82.6-82l0-8.3c0-51.5-32.2-78.7-88.1-85.3-31.6-2.8-50.4-3.9-140.2-3.9zM267 169.2c38.2 0 64.8 31.6 64.8 67 0 32.7-18.3 61-42.1 83.1-15 15-39.3 30.5-55.9 40.5-4.4 2.8-10 4.4-16.7 4.4-5.5 0-10.5-1.7-15.5-4.4-16.6-10-41-25.5-56.5-40.5-21.8-20.8-39.2-46.9-41.3-77l-.2-6.1c0-35.5 25.5-67 64.3-67 22.7 0 38.8 11.6 49.3 27.7 11.6-16.1 27.2-27.7 49.9-27.7zm122.5-3.9c28.3 0 43.8 16.6 43.8 43.2s-15.5 42.7-43.8 42.7c-8.9 0-13.8-5-13.8-11.7l0-62.6c0-6.7 5-11.6 13.8-11.6z"/></svg>' }, (instance, button) => {
button.onclick = () => {
window.open("https://ko-fi.com/swingthevine", "_blank", "noopener noreferrer");
};
}).buildElement().addButton({ "class": "bm-button-circle", "innerHTML": "\u{1F91D}", "title": "Credits" }, (instance, button) => {
button.onclick = () => {
const credits = new WindowCredts(this.name, this.version);
credits.buildWindow();
};
}).buildElement().buildElement().addSmall({ "textContent": "Made by SwingTheVine", "style": "margin-top: auto;" }).buildElement().buildElement().buildElement().buildElement().buildElement().buildOverlay(this.windowParent);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}
/** Displays a new color filter window.
* This is a helper function that creates a new class instance.
* This might cause a memory leak. I pray that this is not the case...
* @since 0.88.330
*/
buildWindowFilter() {
const windowFilter = new WindowFilter(this);
windowFilter.buildPreferredWindow();
}
};
_WindowMain_instances = new WeakSet();
coordinateInputPaste_fn = async function(instance, input, event) {
event.preventDefault();
const data = await getClipboardData(event);
const coords2 = data.split(/[^a-zA-Z0-9]+/).filter((index) => index).map(Number).filter(
(number) => !isNaN(number)
// Removes NaN `[4]`
);
if (coords2.length == 2 && input.id == "bm-input-px") {
instance.updateInnerHTML("bm-input-px", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[1] || "");
} else if (coords2.length == 1) {
instance.updateInnerHTML(input.id, coords2?.[0] || "");
} else {
instance.updateInnerHTML("bm-input-tx", coords2?.[0] || "");
instance.updateInnerHTML("bm-input-ty", coords2?.[1] || "");
instance.updateInnerHTML("bm-input-px", coords2?.[2] || "");
instance.updateInnerHTML("bm-input-py", coords2?.[3] || "");
}
};
// src/templateManager.js
var _TemplateManager_instances, restoreFilteredColorsFromSettings_fn, persistFilteredColors_fn, loadTemplate_fn, storeTemplates_fn, parseBlueMarble_fn, parseOSU_fn, calculateCorrectPixelsOnTile_And_FilterTile_fn;
var TemplateManager = class {
@ -4063,29 +4004,7 @@ Use Blue Marble version ${scriptVersion} or load a new template.`);
console.log(`%cBlue Marble%c: Recieved message about "%s"`, "color: cornflowerblue;", "", endpointText);
switch (endpointText) {
case "me":
if (dataJSON["status"] && dataJSON["status"]?.toString()[0] != "2") {
overlay.handleDisplayError(`You are not logged in or Wplace is offline!
Could not fetch userdata.`);
return;
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON["level"]) * Math.pow(30, 0.65), 1 / 0.65) - dataJSON["pixelsPainted"]);
console.log(dataJSON["id"]);
if (!!dataJSON["id"] || dataJSON["id"] === 0) {
console.log(numberToEncoded(
dataJSON["id"],
"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
));
}
this.templateManager.userID = dataJSON["id"];
if (this.chargeRefillTimerID.length != 0) {
const chargeRefillTimer = document.querySelector("#" + this.chargeRefillTimerID);
if (chargeRefillTimer) {
const chargeData = dataJSON["charges"];
chargeRefillTimer.dataset["endDate"] = Date.now() + (chargeData["max"] - chargeData["count"]) * chargeData["cooldownMs"];
}
}
overlay.updateInnerHTML("bm-user-droplets", `Droplets: <b>${localizeNumber(dataJSON["droplets"])}</b>`);
overlay.updateInnerHTML("bm-user-nextlevel", `Next level in <b>${localizeNumber(nextLevelPixels)}</b> pixel${nextLevelPixels == 1 ? "" : "s"}`);
this.applyUserDataToOverlay(overlay, dataJSON);
break;
case "pixel":
const coordsTile = data["endpoint"].split("?")[0].split("/").filter((s) => s && !isNaN(Number(s)));
@ -4149,6 +4068,76 @@ Did you try clicking the canvas first?`);
}
});
}
/** Applies user data from the /me endpoint to the current overlay.
* @param {Overlay} overlay
* @param {Object.<string, any>} dataJSON
* @since 0.92.1
*/
applyUserDataToOverlay(overlay, dataJSON) {
if (dataJSON["status"] && dataJSON["status"]?.toString()[0] != "2") {
overlay.handleDisplayError(`You are not logged in or Wplace is offline!
Could not fetch userdata.`);
return;
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON["level"]) * Math.pow(30, 0.65), 1 / 0.65) - dataJSON["pixelsPainted"]);
console.log(dataJSON["id"]);
if (!!dataJSON["id"] || dataJSON["id"] === 0) {
console.log(numberToEncoded(
dataJSON["id"],
"!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
));
}
this.templateManager.userID = dataJSON["id"];
if (this.chargeRefillTimerID.length != 0) {
const chargeRefillTimer = document.querySelector("#" + this.chargeRefillTimerID);
if (chargeRefillTimer) {
const chargeData = dataJSON["charges"];
chargeRefillTimer.dataset["endDate"] = Date.now() + (chargeData["max"] - chargeData["count"]) * chargeData["cooldownMs"];
}
}
overlay.updateInnerHTML("bm-user-droplets", `<b>${localizeNumber(dataJSON["droplets"])}</b>`);
overlay.updateInnerHTML("bm-user-nextlevel", `<b>${localizeNumber(nextLevelPixels)}</b> px`);
}
/** Requests the current /me payload directly so the overlay has initial user data
* even if the first network response was missed during startup.
* @param {Overlay} overlay
* @since 0.92.1
*/
async requestCurrentUserData(overlay) {
try {
const response = await fetch(`${window.location.origin}/api/me`, {
credentials: "include"
});
if (!response.ok) {
overlay.handleDisplayError(`Could not fetch userdata.
HTTP ${response.status}`);
return;
}
const dataJSON = await response.json();
this.applyUserDataToOverlay(overlay, dataJSON);
} catch (error) {
consoleError("Failed to fetch current user data:", error);
}
}
/** Applies cached /me data from sessionStorage if it was captured during early startup.
* @param {Overlay} overlay
* @returns {boolean}
* @since 0.92.1
*/
applyCachedUserData(overlay) {
try {
const cached = sessionStorage.getItem("bm-last-me");
if (!cached) {
return false;
}
const dataJSON = JSON.parse(cached);
this.applyUserDataToOverlay(overlay, dataJSON);
return true;
} catch (error) {
consoleError("Failed to apply cached user data:", error);
return false;
}
}
// Sends a heartbeat to the telemetry server
async sendHeartbeat(version2) {
console.log("Sending heartbeat to telemetry server...");
@ -4329,6 +4318,14 @@ Did you try clicking the canvas first?`);
if (contentType.includes("application/json")) {
console.log(`%c${name2}%c: Sending JSON message about endpoint "${endpointName}"`, consoleStyle2, "");
cloned.json().then((jsonData) => {
const endpointText = endpointName?.split("?")[0].split("/").filter((s) => s && isNaN(Number(s))).filter((s) => s && !s.includes(".")).pop();
if (endpointText == "me") {
try {
sessionStorage.setItem("bm-last-me", JSON.stringify(jsonData));
} catch (error) {
console.warn(`%c${name2}%c: Failed to cache "/me" payload`, consoleStyle2, "", error);
}
}
window.postMessage({
source: "blue-marble",
endpoint: endpointName,
@ -4375,13 +4372,9 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
});
var cssOverlay = GM_getResourceText("CSS-BM-File");
GM_addStyle(cssOverlay);
var robotoMonoInjectionPoint = "robotoMonoInjectionPoint";
if (!!(robotoMonoInjectionPoint.indexOf("@font-face") + 1)) {
console.log(`Loading Roboto Mono as a file...`);
GM_addStyle(robotoMonoInjectionPoint);
} else {
stylesheetLink = document.createElement("link");
stylesheetLink.href = "https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap";
function appendFontStylesheet(href) {
const stylesheetLink = document.createElement("link");
stylesheetLink.href = href;
stylesheetLink.rel = "preload";
stylesheetLink.as = "style";
stylesheetLink.onload = function() {
@ -4390,7 +4383,14 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
};
document.head?.appendChild(stylesheetLink);
}
var stylesheetLink;
var robotoMonoInjectionPoint = "robotoMonoInjectionPoint";
appendFontStylesheet("https://fonts.googleapis.com/css2?family=Michroma&family=Rajdhani:wght@400;500;600;700&display=swap");
if (!!(robotoMonoInjectionPoint.indexOf("@font-face") + 1)) {
console.log(`Loading Roboto Mono as a file...`);
GM_addStyle(robotoMonoInjectionPoint);
} else {
appendFontStylesheet("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap");
}
var userSettings = JSON.parse(GM_getValue("bmUserSettings", "{}"));
var observers = new Observers();
var windowMain = new WindowMain(name, version);
@ -4424,9 +4424,11 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
void initializeBlueMarble();
async function initializeBlueMarble() {
await templateManager.importJSON(storageTemplates);
apiManager.spontaneousResponseListener(windowMain);
windowMain.buildWindow();
windowMain.buildWindowFilter();
apiManager.spontaneousResponseListener(windowMain);
apiManager.applyCachedUserData(windowMain);
void apiManager.requestCurrentUserData(windowMain);
observeBlack();
consoleLog(`%c${name}%c (${version}) userscript has loaded!`, "color: cornflowerblue;", "");
}
@ -4460,4 +4462,4 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
}
})();
// Build Hash: 94db4c70d9af
// Build Hash: afe38147b547

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,39 +7,161 @@
fill: white;
}
#bm-window-filter:not(.bm-windowed) {
width: min(50rem, calc(100vw - 0.55rem));
max-width: min(50rem, calc(100vw - 0.55rem)) !important;
}
#bm-window-filter .bm-filter-header {
padding-top: 0.08rem;
}
#bm-window-filter .bm-filter-toolbar {
gap: 0.22rem;
flex-wrap: wrap;
width: 100%;
padding: 0.16rem;
}
#bm-window-filter .bm-filter-toolbar > button {
flex: 1 1 10rem;
}
#bm-window-filter .bm-filter-scrollable {
padding-right: 0.08rem;
}
#bm-window-filter .bm-filter-insights {
display: grid;
grid-template-columns: minmax(16rem, 20rem) minmax(0, 1fr);
gap: 0.24rem 0.3rem;
align-items: stretch;
}
#bm-window-filter .bm-filter-insights > hr,
#bm-window-filter .bm-filter-sort-panel {
grid-column: 1 / -1;
}
#bm-window-filter .bm-filter-stats-card,
#bm-window-filter .bm-filter-note,
#bm-window-filter .bm-filter-sort-panel {
padding: 0.38rem 0.48rem;
border-radius: 13px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: linear-gradient(155deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.16),
0 8px 20px rgba(0, 0, 0, 0.08);
}
#bm-window-filter .bm-filter-stats-card {
display: grid;
gap: 0.18rem;
}
#bm-window-filter .bm-filter-stats-card br {
display: none;
}
#bm-window-filter .bm-filter-stats-card span {
display: block;
padding: 0.28rem 0.38rem;
border-radius: 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.08));
}
#bm-window-filter .bm-filter-note p {
margin: 0;
}
#bm-window-filter .bm-filter-sort-panel fieldset {
border: none;
padding: 0;
margin: 0;
}
#bm-window-filter .bm-filter-sort-panel legend {
margin-bottom: 0.12rem;
}
#bm-window-filter .bm-filter-sort-panel .bm-container {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
}
#bm-window-filter .bm-filter-sort-actions {
display: flex;
justify-content: flex-start;
}
#bm-window-filter .bm-filter-sort-actions button {
min-width: 7.8rem;
}
/* Filter flex */
#bm-filter-flex {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 1em 3ch;
align-items: stretch;
gap: 0.3rem;
}
/* Filter color */
#bm-window-filter .bm-filter-color {
position: relative;
overflow: hidden;
width: fit-content;
max-width: 35ch;
background-color: rgba(21, 48, 99, 0.9);
border-radius: 1em;
padding: 0.5em;
gap: 1ch;
transition: background-color 0.3s ease;
padding: 0.28rem;
gap: 0.28rem;
border-radius: 13px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.16),
0 8px 20px rgba(0, 0, 0, 0.08);
transition:
background 0.25s ease,
border-color 0.25s ease,
box-shadow 0.25s ease,
transform 0.25s ease;
}
#bm-window-filter .bm-filter-color::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.18), transparent 24%),
radial-gradient(circle at 18% 0%, rgba(186, 246, 255, 0.12), transparent 22%);
}
/* Filter color on hover */
#bm-window-filter .bm-filter-color:hover,
#bm-window-filter.bm-filter-color:focus-within {
background-color: rgba(17, 40, 85, 0.9);
#bm-window-filter .bm-filter-color:focus-within {
transform: translateY(-2px);
border-color: rgba(146, 221, 255, 0.26);
background: linear-gradient(160deg, rgba(255, 255, 255, 0.22), rgba(186, 246, 255, 0.1));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 10px 24px rgba(0, 0, 0, 0.1);
}
/* Filter window container for RGB color display */
#bm-window-filter .bm-filter-container-rgb {
display: block;
border: thick double darkslategray;
width: fit-content;
height: fit-content;
padding: 1ch;
padding: 0.26rem;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 10px;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
0 6px 16px rgba(0, 0, 0, 0.08);
}
/* Filter window container for RGB color display for Other color */
@ -60,6 +182,7 @@
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 8 8" width="1em" height="1em"><path d="M0,0V8H16V16H8V0" fill="rgba(0,0,0,0.5)"/></svg>') repeat;
background-color: transparent !important;
}
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb svg {
fill: white !important;
}
@ -71,7 +194,7 @@
/* Filter window hide color button */
#bm-window-filter .bm-filter-container-rgb button {
padding: 0.75em 0.5ch;
padding: 0.26em 0.14ch;
}
/* Filter window hide color button SVG */
@ -83,7 +206,15 @@
#bm-window-filter .bm-filter-color > .bm-flex-between {
flex-direction: column;
align-items: flex-start;
gap: 0;
gap: 0.02rem;
}
#bm-window-filter .bm-filter-color h2 {
margin: 0.04rem 0 0;
}
#bm-window-filter .bm-filter-color .bm-filter-color-pxl-desc {
margin: 0.1rem 0 0;
}
/* Filter window color flavor text */
@ -103,33 +234,49 @@
--bm-scrollable-max-height: 100%;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
width: 300px;
height: min(70vh, 32rem);
min-width: 260px;
min-height: 220px;
width: 268px;
height: min(60vh, 22rem);
min-width: 228px;
min-height: 180px;
max-width: min(1000px, calc(100vw - 16px)) !important;
max-height: min(1400px, calc(100vh - 16px)) !important;
overflow: hidden;
box-sizing: border-box;
position: fixed;
transition: transform 0s;
transition:
background 320ms ease,
border-color 220ms ease,
box-shadow 220ms ease,
transform 0s;
}
/* Keep the content area flexible inside the resizable window */
#bm-window-filter.bm-windowed .bm-window-content {
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr);
grid-template-rows: auto auto auto auto minmax(0, 1fr);
grid-row: 2;
min-height: 0;
min-width: 0;
overflow: hidden;
}
#bm-window-filter.bm-windowed .bm-filter-toolbar {
gap: 0.16rem;
flex-wrap: nowrap;
width: 100%;
padding: 0.12rem;
}
#bm-window-filter.bm-windowed .bm-filter-toolbar > button {
flex: 1 1 0;
min-width: 0;
}
/* Filter flex in windowed mode */
#bm-window-filter.bm-windowed #bm-filter-flex {
flex-direction: column;
align-items: stretch;
gap: 0.25em;
gap: 0.16rem;
width: 100%;
align-self: stretch;
min-width: 0;
@ -144,7 +291,8 @@
flex: 1 1 auto;
min-width: 0;
margin: 0;
padding: 0;
padding: 0.12rem;
border-radius: 11px;
box-sizing: border-box;
}
@ -157,7 +305,7 @@
/* Let the scroll area grow and shrink with the resizable window */
#bm-window-filter.bm-windowed .bm-container.bm-scrollable {
display: block;
grid-row: 4;
grid-row: 5;
min-height: 0;
min-width: 0;
height: 100%;
@ -170,25 +318,36 @@
/* Visible resize handle in the bottom-right corner */
#bm-window-filter.bm-windowed .bm-resize-corner {
position: absolute;
right: 0;
bottom: 0;
display: block;
width: 28px;
height: 28px;
right: 4px;
bottom: 4px;
display: flex;
width: 20px;
height: 20px;
align-items: flex-end;
justify-content: flex-end;
padding-right: 4px;
padding-bottom: 4px;
box-sizing: border-box;
z-index: 5;
cursor: nwse-resize;
pointer-events: auto;
opacity: 1;
opacity: 0.78;
touch-action: none;
user-select: none;
background: transparent;
border: none;
box-shadow: none;
color: rgba(255, 255, 255, 0.86);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.14), rgba(83, 141, 255, 0.14));
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
#bm-window-filter.bm-windowed .bm-resize-corner:hover,
#bm-window-filter.bm-windowed .bm-resize-corner.bm-resizing {
opacity: 1;
border-color: rgba(146, 221, 255, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 8px 16px rgba(0, 0, 0, 0.16);
}
/* Filter window container for RGB color display in windowed mode */
@ -199,26 +358,26 @@
flex: 1 1 auto;
gap: 0.5ch;
align-items: center;
padding: 0.1em 0.5ch;
padding: 0.1rem 0.2rem;
border: none;
border-radius: 1em;
border-radius: 8px;
box-sizing: border-box;
}
/* Filter window hide color button */
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
padding: 0.5em 0.25ch;
padding: 0.2em 0.1ch;
flex: 0 0 auto;
}
/* Filter window hide color button SVG in windowed mode */
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
width: 3ch;
width: 2.7ch;
}
/* Filter window header 2 in windowed mode */
#bm-window-filter.bm-windowed .bm-filter-color h2 {
font-size: 0.75em;
font-size: 0.78rem;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
@ -227,5 +386,28 @@
/* Filter window dragbar text area in windowed mode */
#bm-window-filter #bm-filter-windowed-color-totals {
display: inline-flex;
align-items: center;
padding: 0.08rem 0.24rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
font-size: 1em;
}
@media (max-width: 980px) {
#bm-window-filter:not(.bm-windowed) {
width: min(100vw - 0.35rem, 50rem);
max-width: min(100vw - 0.35rem, 50rem) !important;
}
#bm-window-filter .bm-filter-insights {
grid-template-columns: 1fr;
}
#bm-window-filter .bm-filter-note,
#bm-window-filter .bm-filter-sort-panel,
#bm-window-filter .bm-filter-insights > hr {
grid-column: auto;
}
}

View file

@ -27,6 +27,8 @@ export default class WindowFilter extends Overlay {
this.windowResizeObserver = null; // Resize observer for the windowed mode
this.windowViewportResizeHandler = null; // Resize handler for viewport changes
this.windowSaveTimeout = null; // Debounce timer for resize persistence
this.colorRefreshInterval = null; // Auto-refresh timer for live color statistics
this.colorRefreshIntervalMS = 10000; // Refresh Color Filter statistics every 10 seconds
this.windowMinWidth = 260; // Minimum width for the windowed filter
this.windowMinHeight = 220; // Minimum height for the windowed filter
this.windowMaxWidth = 1000; // Maximum width for the windowed filter
@ -113,28 +115,22 @@ export default class WindowFilter extends Overlay {
.buildElement()
.buildElement()
.addDiv({'class': 'bm-window-content'})
.addDiv({'class': 'bm-container bm-center-vertically'})
.addDiv({'class': 'bm-container bm-center-vertically bm-filter-header'})
.addHeader(1, {'textContent': 'Color Filter'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
.addButton({'textContent': 'Hide All Colors'}, (instance, button) => {
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically bm-filter-toolbar', 'style': 'gap: 1.5ch;'})
.addButton({'class': 'bm-button-secondary', 'textContent': 'Hide All Colors'}, (instance, button) => {
button.onclick = () => this.#selectColorList(false);
}).buildElement()
.addButton({'textContent': 'Refresh Data'}, (instance, button) => {
button.onclick = () => {
button.disabled = true;
this.updateColorList();
button.disabled = false;
};
}).buildElement()
.addButton({'textContent': 'Show All Colors'}, (instance, button) => {
.addButton({'class': 'bm-button-secondary', 'textContent': 'Show All Colors'}, (instance, button) => {
button.onclick = () => this.#selectColorList(true);
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-scrollable'})
.addDiv({'class': 'bm-container', 'style': 'margin-left: 2.5ch; margin-right: 2.5ch;'})
.addDiv({'class': 'bm-container'})
.addHr().buildElement()
.addDiv({'class': 'bm-container bm-scrollable bm-filter-scrollable'})
.addDiv({'class': 'bm-container bm-filter-insights', 'style': 'margin-left: 2.5ch; margin-right: 2.5ch;'})
.addDiv({'class': 'bm-container bm-filter-stats-card'})
.addSpan({'id': 'bm-filter-tile-load', 'innerHTML': '<b>Tiles Loaded:</b> 0 / ???'}).buildElement()
.addBr().buildElement()
.addSpan({'id': 'bm-filter-tot-correct', 'innerHTML': '<b>Correct Pixels:</b> ???'}).buildElement()
@ -145,11 +141,11 @@ export default class WindowFilter extends Overlay {
.addBr().buildElement()
.addSpan({'id': 'bm-filter-tot-completed', 'innerHTML': '??? ???'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container bm-filter-note'})
.addP({'innerHTML': `Press the 🗗 button to make this window smaller. Colors with the icon ${this.eyeOpen.replace('<svg', '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace('<svg', '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.`}).buildElement()
.buildElement()
.addHr().buildElement()
.addForm({'class': 'bm-container'})
.addForm({'class': 'bm-container bm-filter-sort-panel'})
.addFieldset()
.addLegend({'textContent': 'Sort Options:', 'style': 'font-weight: 700;'}).buildElement()
.addDiv({'class': 'bm-container'})
@ -172,8 +168,8 @@ export default class WindowFilter extends Overlay {
.addCheckbox({'id': 'bm-filter-show-unused', 'name': 'showUnused', 'textContent': 'Show unused colors'}).buildElement()
.buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addButton({'textContent': 'Sort Colors', 'type': 'submit'}, (instance, button) => {
.addDiv({'class': 'bm-container bm-filter-sort-actions'})
.addButton({'class': 'bm-button-primary', 'textContent': 'Sort Colors', 'type': 'submit'}, (instance, button) => {
button.onclick = (event) => {
event.preventDefault(); // Stop default form submission
@ -214,6 +210,7 @@ export default class WindowFilter extends Overlay {
this.updateInnerHTML('#bm-filter-tot-total', `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
this.updateInnerHTML('#bm-filter-tot-remaining', `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
this.updateInnerHTML('#bm-filter-tot-completed', `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, 'Z')}">${this.timeRemainingLocalized}</time>`);
this.#startAutoRefresh();
}
/** Spawns a windowed Color Filter window.
@ -266,26 +263,20 @@ export default class WindowFilter extends Overlay {
.buildElement()
.buildElement()
.addDiv({'class': 'bm-window-content'})
.addDiv({'class': 'bm-container bm-center-vertically'})
.addDiv({'class': 'bm-container bm-center-vertically bm-filter-header'})
.addHeader(1, {'textContent': 'Color Filter'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
.addButton({'textContent': 'None'}, (instance, button) => {
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically bm-filter-toolbar', 'style': 'gap: 1.5ch;'})
.addButton({'class': 'bm-button-secondary', 'textContent': 'None'}, (instance, button) => {
button.onclick = () => this.#selectColorList(false);
}).buildElement()
.addButton({'textContent': 'Refresh'}, (instance, button) => {
button.onclick = () => {
button.disabled = true;
this.updateColorList();
button.disabled = false;
};
}).buildElement()
.addButton({'textContent': 'All'}, (instance, button) => {
.addButton({'class': 'bm-button-secondary', 'textContent': 'All'}, (instance, button) => {
button.onclick = () => this.#selectColorList(true);
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-scrollable'})
.addHr().buildElement()
.addDiv({'class': 'bm-container bm-scrollable bm-filter-scrollable'})
// Color list will appear here
.buildElement()
.buildElement()
@ -308,6 +299,7 @@ export default class WindowFilter extends Overlay {
this.#buildColorList(scrollableContainer);
this.#syncSortFormControls();
this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
this.#startAutoRefresh();
}
/** Retrieves the persisted window state object.
@ -373,10 +365,34 @@ export default class WindowFilter extends Overlay {
if (windowElement?.classList.contains('bm-windowed')) {
this.#saveWindowState(windowElement);
}
this.#stopAutoRefresh();
this.#cleanupWindowPersistence();
windowElement?.remove();
}
/** Starts the automatic Color Filter statistics refresh loop.
* @since 0.92.1
*/
#startAutoRefresh() {
this.#stopAutoRefresh();
this.colorRefreshInterval = setInterval(() => {
if (!document.querySelector(`#${this.windowID}`)) {
this.#stopAutoRefresh();
return;
}
this.updateColorList();
}, this.colorRefreshIntervalMS);
}
/** Stops the automatic Color Filter statistics refresh loop.
* @since 0.92.1
*/
#stopAutoRefresh() {
if (!this.colorRefreshInterval) {return;}
clearInterval(this.colorRefreshInterval);
this.colorRefreshInterval = null;
}
/** Disconnects live observers used for window persistence.
* @since 0.92.0
*/
@ -876,6 +892,12 @@ export default class WindowFilter extends Overlay {
this.updateInnerHTML('#bm-filter-windowed-color-totals', `${allCorrect}/${allTotal}`, true);
}
this.updateInnerHTML('#bm-filter-tile-load', `<b>Tiles Loaded:</b> ${localizeNumber(this.tilesLoadedTotal)} / ${localizeNumber(this.tilesTotal)}`);
this.updateInnerHTML('#bm-filter-tot-correct', `<b>Correct Pixels:</b> ${localizeNumber(this.allPixelsCorrectTotal)}`);
this.updateInnerHTML('#bm-filter-tot-total', `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
this.updateInnerHTML('#bm-filter-tot-remaining', `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
this.updateInnerHTML('#bm-filter-tot-completed', `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, 'Z')}">${this.timeRemainingLocalized}</time>`);
// Return early if the color list does not exist.
// We can't update DOM elements that don't exist, so we exit now.
if (!colorList) {return colorStatistics;}

173
src/WindowMain.css Normal file
View file

@ -0,0 +1,173 @@
/* @since 0.92.2 */
#bm-window-main {
width: min(25.5rem, calc(100vw - 0.65rem));
max-width: min(25.5rem, calc(100vw - 0.65rem)) !important;
}
#bm-window-main .bm-window-content {
display: flex;
flex-direction: column;
}
#bm-window-main .bm-main-hero,
#bm-window-main .bm-main-shell {
position: relative;
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: linear-gradient(150deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 10px 24px rgba(0, 0, 0, 0.1);
}
#bm-window-main .bm-main-hero::before,
#bm-window-main .bm-main-shell::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.2), transparent 26%),
radial-gradient(circle at 20% 0%, rgba(186, 246, 255, 0.16), transparent 24%);
}
#bm-window-main .bm-main-hero {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.48rem 0.58rem;
}
#bm-window-main .bm-main-hero h1 {
margin: 0;
}
#bm-window-main .bm-main-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.24rem;
}
#bm-window-main .bm-main-stat-card {
min-width: 0;
min-height: 2.55rem;
display: flex;
align-items: flex-start;
padding: 0.34rem 0.45rem;
border-radius: 11px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
color: rgba(18, 35, 63, 0.96);
line-height: 1.22;
}
#bm-window-main .bm-main-stat-card > span,
#bm-window-main .bm-main-stat-card > time {
min-width: 0;
}
#bm-window-main .bm-main-stat-card-value,
#bm-window-main .bm-main-stat-card-timer {
flex-direction: column;
justify-content: center;
gap: 0.08rem;
}
#bm-window-main .bm-main-stat-value {
display: inline-block;
font-size: 1.3em;
color: rgba(15, 31, 56, 0.96);
white-space: normal;
overflow-wrap: anywhere;
}
#bm-window-main .bm-main-stat-value b {
font-size: 1em;
}
#bm-window-main .bm-main-stat-label {
color: rgba(49, 71, 105, 0.82);
font-size: 0.62rem;
letter-spacing: 0.08em;
font-family: var(--bm-font-display);
text-transform: uppercase;
}
#bm-window-main .bm-main-stat-card time {
white-space: nowrap;
font-family: var(--bm-font-mono);
letter-spacing: 0.06em;
}
#bm-window-main .bm-main-shell {
padding: 0.48rem;
}
#bm-window-main .bm-main-coords {
display: grid;
grid-template-columns: auto repeat(4, minmax(0, 1fr));
gap: 0.22rem;
align-items: center;
}
#bm-window-main .bm-main-coords .bm-button-pin {
width: 1.8rem;
height: 1.8rem;
}
#bm-window-main .bm-main-coords .bm-input-coords {
width: 100%;
margin-left: 0;
text-align: center;
}
#bm-window-main .bm-main-upload,
#bm-window-main .bm-main-status {
margin-top: 0.24rem;
}
#bm-window-main .bm-main-upload > div {
width: 100%;
}
#bm-window-main .bm-main-actions {
gap: 0.24rem;
margin-top: 0.3rem;
}
#bm-window-main .bm-main-actions > button {
flex: 1 1 0;
}
#bm-window-main .bm-main-status textarea {
min-height: 4.1rem;
}
@media (max-width: 720px) {
#bm-window-main {
width: min(100vw - 0.45rem, 25.5rem);
max-width: min(100vw - 0.45rem, 25.5rem) !important;
}
#bm-window-main .bm-main-stats {
grid-template-columns: 1fr;
}
#bm-window-main .bm-main-coords {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#bm-window-main .bm-main-coords .bm-button-pin {
grid-column: 1 / -1;
width: 100%;
aspect-ratio: auto;
height: 1.8rem;
}
#bm-window-main .bm-main-actions {
flex-direction: column;
}
}

View file

@ -1,9 +1,7 @@
import ConfettiManager from "./confetttiManager";
import Overlay from "./Overlay";
import { getClipboardData } from "./utils";
import WindowCredts from "./WindowCredits";
import WindowFilter from "./WindowFilter";
import WindowWizard from "./WindowWizard";
/** The overlay builder for the main Blue Marble window.
* @description This class handles the overlay UI for the main window of the Blue Marble userscript.
@ -50,9 +48,16 @@ export default class WindowMain extends Overlay {
button.ontouchend = () => {button.click();}; // Needed ONLY to negate weird interaction with dragbar
}).buildElement()
.addDiv().buildElement() // Contains the minimized h1 element
.addDiv({'class': 'bm-flex-center'})
.addButton({'class': 'bm-button-circle', 'innerHTML': '⚙️', 'title': 'Settings'}, (instance, button) => {
button.onclick = () => {
instance.settingsManager.buildWindow();
}
}).buildElement()
.buildElement()
.buildElement()
.addDiv({'class': 'bm-window-content'})
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container bm-main-hero'})
.addImg({'class': 'bm-favicon', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png'}, (instance, img) => {
// Adds a birthday hat & confetti to the window if it is Blue Marble's birthday
const date = new Date();
@ -69,20 +74,25 @@ export default class WindowMain extends Overlay {
.addHeader(1, {'textContent': this.name}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container'})
.addSpan({'id': 'bm-user-droplets', 'textContent': 'Droplets:'}).buildElement()
.addBr().buildElement()
.addSpan({'id': 'bm-user-nextlevel', 'textContent': 'Next level in...'}).buildElement()
.addBr().buildElement()
.addSpan({'textContent': 'Charges: '})
.addTimer(Date.now(), 1000, {'style': 'font-weight: 700;'}, (instance, timer) => {
.addDiv({'class': 'bm-container bm-main-stats'})
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-value'})
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Droplets'}).buildElement()
.addSpan({'id': 'bm-user-droplets', 'class': 'bm-main-stat-value', 'textContent': '0'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-value'})
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Next Level'}).buildElement()
.addSpan({'id': 'bm-user-nextlevel', 'class': 'bm-main-stat-value', 'textContent': '0 px'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-timer'})
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Charges'}).buildElement()
.addTimer(Date.now(), 1000, {'class': 'bm-main-stat-value', 'style': 'font-weight: 700;'}, (instance, timer) => {
instance.apiManager.chargeRefillTimerID = timer.id; // Store the timer ID in apiManager so we can update the timer automatically
}).buildElement()
.buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container bm-main-shell'})
.addDiv({'class': 'bm-container bm-main-coords'})
.addButton({'class': 'bm-button-circle bm-button-pin', 'style': 'margin-top: 0;', 'innerHTML': '<svg viewBox="0 0 4 6"><path d="M.5,3.4A2,2 0 1 1 3.5,3.4L2,6"/><circle cx="2" cy="2" r=".7" fill="#fff"/></svg>'},
(instance, button) => {
button.onclick = () => {
@ -111,11 +121,11 @@ export default class WindowMain extends Overlay {
input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container bm-main-upload'})
.addInputFile({'class': 'bm-input-file', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-flex-between'})
.addButton({'textContent': 'Disable', 'data-button-status': 'shown'}, (instance, button) => {
.addDiv({'class': 'bm-container bm-flex-between bm-main-actions'})
.addButton({'class': 'bm-button-secondary', 'textContent': 'Disable', 'data-button-status': 'shown'}, (instance, button) => {
button.onclick = () => {
button.disabled = true; // Disables the button until the transition ends
if (button.dataset['buttonStatus'] == 'shown') { // If templates are currently being 'shown' then hide them
@ -132,7 +142,7 @@ export default class WindowMain extends Overlay {
button.disabled = false; // Enables the button
}
}).buildElement()
.addButton({'textContent': 'Create'}, (instance, button) => {
.addButton({'class': 'bm-button-primary', 'textContent': 'Create'}, (instance, button) => {
button.onclick = () => {
const input = document.querySelector(`#${this.windowID} .bm-input-file`);
@ -153,52 +163,13 @@ export default class WindowMain extends Overlay {
instance.handleDisplayStatus(`Drew to canvas!`);
}
}).buildElement()
.addButton({'textContent': 'Filter'}, (instance, button) => {
.addButton({'class': 'bm-button-secondary', 'textContent': 'Filter'}, (instance, button) => {
button.onclick = () => this.buildWindowFilter();
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addDiv({'class': 'bm-container bm-main-status'})
.addTextarea({'id': this.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${this.version}`, 'readOnly': true}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-flex-between', 'style': 'margin-bottom: 0; flex-direction: column;'})
.addDiv({'class': 'bm-flex-between'})
// .addButton({'class': 'bm-button-circle', 'innerHTML': '🖌'}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '⚙️', 'title': 'Settings'}, (instance, button) => {
button.onclick = () => {
instance.settingsManager.buildWindow();
}
}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🧙', 'title': 'Template Wizard'}, (instance, button) => {
button.onclick = () => {
const templateManager = instance.apiManager?.templateManager;
const wizard = new WindowWizard(this.name, this.version, templateManager?.schemaVersion, templateManager);
wizard.buildWindow();
}
}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🎨', 'title': 'Template Color Converter'}, (instance, button) => {
button.onclick = () => {
window.open('https://pepoafonso.github.io/color_converter_wplace/', '_blank', 'noopener noreferrer');
}
}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🌐', 'title': 'Official Blue Marble Website'}, (instance, button) => {
button.onclick = () => {
window.open('https://bluemarble.lol/', '_blank', 'noopener noreferrer');
}
}).buildElement()
.addButton({'class': 'bm-button-circle', 'title': 'Donate to SwingTheVine', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#fff" style="width:80%; margin:auto;"><path d="M249.8 75c89.8 0 113 1.1 146.3 4.4 78.1 7.8 123.6 56 123.6 125.2l0 8.9c0 64.3-47.1 116.9-110.8 122.4-5 16.6-12.8 33.2-23.3 49.9-24.4 37.7-73.1 85.3-162.9 85.3l-17.7 0c-73.1 0-129.7-31.6-163.5-89.2-29.9-50.4-33.8-106.4-33.8-181.2 0-73.7 44.4-113.6 96.4-120.2 39.3-5 88.1-5.5 145.7-5.5zm0 41.6c-60.4 0-103.6 .5-136.3 5.5-46 6.7-64.3 32.7-64.3 79.2l.2 25.7c1.2 57.3 7.1 97.1 27.5 134.5 26.6 49.3 74.8 68.2 129.7 68.2l17.2 0c72 0 107-34.9 126.3-65.4 9.4-15.5 17.7-32.7 22.2-54.3l3.3-13.8 19.9 0c44.3 0 82.6-36 82.6-82l0-8.3c0-51.5-32.2-78.7-88.1-85.3-31.6-2.8-50.4-3.9-140.2-3.9zM267 169.2c38.2 0 64.8 31.6 64.8 67 0 32.7-18.3 61-42.1 83.1-15 15-39.3 30.5-55.9 40.5-4.4 2.8-10 4.4-16.7 4.4-5.5 0-10.5-1.7-15.5-4.4-16.6-10-41-25.5-56.5-40.5-21.8-20.8-39.2-46.9-41.3-77l-.2-6.1c0-35.5 25.5-67 64.3-67 22.7 0 38.8 11.6 49.3 27.7 11.6-16.1 27.2-27.7 49.9-27.7zm122.5-3.9c28.3 0 43.8 16.6 43.8 43.2s-15.5 42.7-43.8 42.7c-8.9 0-13.8-5-13.8-11.7l0-62.6c0-6.7 5-11.6 13.8-11.6z"/></svg>'}, (instance, button) => {
button.onclick = () => {
window.open('https://ko-fi.com/swingthevine', '_blank', 'noopener noreferrer');
}
}).buildElement()
.addButton({'class': 'bm-button-circle', 'innerHTML': '🤝', 'title': 'Credits'}, (instance, button) => {
button.onclick = () => {
const credits = new WindowCredts(this.name, this.version);
credits.buildWindow();
}
}).buildElement()
.buildElement()
.addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
.buildElement()
.buildElement()
.buildElement()
.buildElement().buildOverlay(this.windowParent);

View file

@ -54,44 +54,7 @@ export default class ApiManager {
switch (endpointText) {
case 'me': // Request to retrieve user data
// If the game can not retrieve the userdata...
if (dataJSON['status'] && dataJSON['status']?.toString()[0] != '2') {
// The server is probably down (NOT a 2xx status)
overlay.handleDisplayError(`You are not logged in or Wplace is offline!\nCould not fetch userdata.`);
return; // Kills itself before attempting to display null userdata
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1/0.65)) - dataJSON['pixelsPainted']); // Calculates pixels to the next level
console.log(dataJSON['id']);
if (!!dataJSON['id'] || dataJSON['id'] === 0) {
console.log(numberToEncoded(
dataJSON['id'],
'!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
));
}
this.templateManager.userID = dataJSON['id'];
// Obtains the refill timer for charges
if (this.chargeRefillTimerID.length != 0) {
const chargeRefillTimer = document.querySelector('#' + this.chargeRefillTimerID);
// If the refill timer exists...
if (chargeRefillTimer) {
/** Obtains the information about the user's charges @type {{cooldownMs: number, count: number, max: number}} */
const chargeData = dataJSON['charges'];
// Date that the user's charges will be refilled
chargeRefillTimer.dataset['endDate'] = Date.now() + ((chargeData['max'] - chargeData['count']) * chargeData['cooldownMs']);
}
}
// Updates displayed droplet information
overlay.updateInnerHTML('bm-user-droplets', `Droplets: <b>${localizeNumber(dataJSON['droplets'])}</b>`); // Updates the text content of the droplets field
overlay.updateInnerHTML('bm-user-nextlevel', `Next level in <b>${localizeNumber(nextLevelPixels)}</b> pixel${nextLevelPixels == 1 ? '' : 's'}`); // Updates the text content of the next level field
this.applyUserDataToOverlay(overlay, dataJSON);
break;
case 'pixel': // Request to retrieve pixel data
@ -195,6 +158,90 @@ export default class ApiManager {
});
}
/** Applies user data from the /me endpoint to the current overlay.
* @param {Overlay} overlay
* @param {Object.<string, any>} dataJSON
* @since 0.92.1
*/
applyUserDataToOverlay(overlay, dataJSON) {
// If the game can not retrieve the userdata...
if (dataJSON['status'] && dataJSON['status']?.toString()[0] != '2') {
overlay.handleDisplayError(`You are not logged in or Wplace is offline!\nCould not fetch userdata.`);
return;
}
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1 / 0.65)) - dataJSON['pixelsPainted']);
console.log(dataJSON['id']);
if (!!dataJSON['id'] || dataJSON['id'] === 0) {
console.log(numberToEncoded(
dataJSON['id'],
'!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
));
}
this.templateManager.userID = dataJSON['id'];
// Obtains the refill timer for charges
if (this.chargeRefillTimerID.length != 0) {
const chargeRefillTimer = document.querySelector('#' + this.chargeRefillTimerID);
// If the refill timer exists...
if (chargeRefillTimer) {
/** Obtains the information about the user's charges @type {{cooldownMs: number, count: number, max: number}} */
const chargeData = dataJSON['charges'];
// Date that the user's charges will be refilled
chargeRefillTimer.dataset['endDate'] = Date.now() + ((chargeData['max'] - chargeData['count']) * chargeData['cooldownMs']);
}
}
overlay.updateInnerHTML('bm-user-droplets', `<b>${localizeNumber(dataJSON['droplets'])}</b>`);
overlay.updateInnerHTML('bm-user-nextlevel', `<b>${localizeNumber(nextLevelPixels)}</b> px`);
}
/** Requests the current /me payload directly so the overlay has initial user data
* even if the first network response was missed during startup.
* @param {Overlay} overlay
* @since 0.92.1
*/
async requestCurrentUserData(overlay) {
try {
const response = await fetch(`${window.location.origin}/api/me`, {
credentials: 'include'
});
if (!response.ok) {
overlay.handleDisplayError(`Could not fetch userdata.\nHTTP ${response.status}`);
return;
}
const dataJSON = await response.json();
this.applyUserDataToOverlay(overlay, dataJSON);
} catch (error) {
consoleError('Failed to fetch current user data:', error);
}
}
/** Applies cached /me data from sessionStorage if it was captured during early startup.
* @param {Overlay} overlay
* @returns {boolean}
* @since 0.92.1
*/
applyCachedUserData(overlay) {
try {
const cached = sessionStorage.getItem('bm-last-me');
if (!cached) {return false;}
const dataJSON = JSON.parse(cached);
this.applyUserDataToOverlay(overlay, dataJSON);
return true;
} catch (error) {
consoleError('Failed to apply cached user data:', error);
return false;
}
}
// Sends a heartbeat to the telemetry server
async sendHeartbeat(version) {

View file

@ -6,5 +6,6 @@
@import './confettiManager.css';
@import './overlay.css';
@import './WindowFilter.css';
@import './WindowMain.css';
@import './WindowSettings.css';
@import './WindowWizard.css';

View file

@ -92,6 +92,18 @@ inject(() => {
// Sends a message about the endpoint it spied on
cloned.json()
.then(jsonData => {
const endpointText = endpointName?.split('?')[0].split('/').filter(s => s && isNaN(Number(s))).filter(s => s && !s.includes('.')).pop();
// Cache the latest /me payload so the userscript can hydrate its UI
// even if the first response arrives before listeners are attached.
if (endpointText == 'me') {
try {
sessionStorage.setItem('bm-last-me', JSON.stringify(jsonData));
} catch (error) {
console.warn(`%c${name}%c: Failed to cache "/me" payload`, consoleStyle, '', error);
}
}
window.postMessage({
source: 'blue-marble',
endpoint: endpointName,
@ -163,9 +175,23 @@ inject(() => {
const cssOverlay = GM_getResourceText("CSS-BM-File");
GM_addStyle(cssOverlay);
function appendFontStylesheet(href) {
const stylesheetLink = document.createElement('link');
stylesheetLink.href = href;
stylesheetLink.rel = 'preload';
stylesheetLink.as = 'style';
stylesheetLink.onload = function () {
this.onload = null;
this.rel = 'stylesheet';
};
document.head?.appendChild(stylesheetLink);
}
// Injection point for the Roboto Mono font file (only if this is the Standalone version)
const robotoMonoInjectionPoint = 'robotoMonoInjectionPoint';
appendFontStylesheet('https://fonts.googleapis.com/css2?family=Michroma&family=Rajdhani:wght@400;500;600;700&display=swap');
// If the Roboto Mono injection point contains '@font-face'...
if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
// A very hacky way of doing truthy/falsy logic
@ -176,15 +202,7 @@ if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
// Else, no Roboto Mono was found. We need to use a stylesheet.
// Imports the Roboto Mono font family as a stylesheet
var stylesheetLink = document.createElement('link');
stylesheetLink.href = 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap';
stylesheetLink.rel = 'preload';
stylesheetLink.as = 'style';
stylesheetLink.onload = function () {
this.onload = null;
this.rel = 'stylesheet';
};
document.head?.appendChild(stylesheetLink);
appendFontStylesheet('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
}
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
@ -240,10 +258,13 @@ void initializeBlueMarble();
async function initializeBlueMarble() {
await templateManager.importJSON(storageTemplates); // Loads the templates
apiManager.spontaneousResponseListener(windowMain); // Reads spontaneous fetch responces
windowMain.buildWindow(); // Builds the main Blue Marble window
windowMain.buildWindowFilter(); // Opens the Color Filter window automatically on page load
apiManager.spontaneousResponseListener(windowMain); // Reads spontaneous fetch responces
apiManager.applyCachedUserData(windowMain); // Hydrates the UI from the earliest cached /me response if it exists
void apiManager.requestCurrentUserData(windowMain); // Ensures the main window gets current /me data even if startup missed it
observeBlack(); // Observes the black palette color

View file

@ -15,27 +15,81 @@
/* The Blue Marble windows */
.bm-window {
--bm-surface-strong: rgba(9, 20, 42, 0.5);
--bm-surface-soft: rgba(24, 41, 74, 0.28);
--bm-surface-glass: rgba(255, 255, 255, 0.1);
--bm-surface-glass-strong: rgba(255, 255, 255, 0.18);
--bm-border-soft: rgba(255, 255, 255, 0.18);
--bm-border-strong: rgba(163, 228, 255, 0.34);
--bm-text-primary: rgba(17, 36, 66, 0.96);
--bm-text-secondary: rgba(36, 57, 90, 0.84);
--bm-accent-start: #baf6ff;
--bm-accent-end: #81b6ff;
--bm-accent-shadow: rgba(132, 182, 255, 0.22);
--bm-font-body: "Rajdhani", "Segoe UI Variable Text", "Segoe UI", sans-serif;
--bm-font-display: "Michroma", "Orbitron", "Segoe UI", sans-serif;
--bm-font-mono: "Roboto Mono", "Rajdhani", "Courier New", monospace;
position: fixed;
background-color: rgba(21, 48, 99, 0.9);
color: white;
padding: 10px;
border-radius: 8px;
isolation: isolate;
overflow: hidden;
background:
radial-gradient(circle at 14% 12%, rgba(255, 255, 255, 0.24), transparent 18%),
radial-gradient(circle at 86% 8%, rgba(186, 246, 255, 0.22), transparent 24%),
radial-gradient(circle at 82% 84%, rgba(129, 182, 255, 0.18), transparent 28%),
linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.06) 22%, rgba(105, 145, 212, 0.08) 54%, rgba(9, 20, 42, 0.18));
color: var(--bm-text-primary);
padding: 6px;
border-radius: 16px;
border: 1px solid var(--bm-border-soft);
box-shadow:
0 18px 40px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.22),
inset 0 -1px 0 rgba(255, 255, 255, 0.05);
z-index: 9000;
transition: all 0.3s ease, transform 0s;
transition:
background 320ms ease,
border-color 220ms ease,
box-shadow 220ms ease,
opacity 220ms ease,
transform 0s,
width 220ms ease,
max-width 220ms ease,
max-height 220ms ease;
top: 75px;
left: 60px;
width: auto;
max-height: fit-content;
max-width: calc(100% - 135px);
/* Font stack is as follows:
* Highest Priority (Roboto Mono)
* Windows fallback (Courier New)
* macOS fallback (Monaco)
* Linux fallback (DejaVu Sans Mono)
* Any possible monospace font (monospace)
* Last resort (Arial) */
font-family: 'Roboto Mono', 'Courier New', 'Monaco', 'DejaVu Sans Mono', monospace, 'Arial';
letter-spacing: 0.05em;
backdrop-filter: blur(26px) saturate(1.25);
font-family: var(--bm-font-body);
letter-spacing: 0.04em;
}
.bm-window::before,
.bm-window::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
.bm-window::before {
border-radius: inherit;
padding: 1px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.12) 24%, rgba(186, 246, 255, 0.22) 58%, rgba(129, 182, 255, 0.3));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0.85;
}
.bm-window::after {
border-radius: inherit;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent 24%),
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.22), transparent 22%),
radial-gradient(circle at 88% 16%, rgba(186, 246, 255, 0.18), transparent 18%);
opacity: 1;
}
/* The drag bar */
@ -43,12 +97,19 @@
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.5ch;
/* For background circles, width & height should be odd, cx & cy should be half of width & height, and r should be less than or equal to cx & cy */
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue"/></svg>') repeat;
gap: 0.28ch;
padding: 0.18rem 0.24rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.16);
background:
radial-gradient(circle at 0 0, rgba(255, 255, 255, 0.22) 0, transparent 42%),
linear-gradient(135deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
cursor: grab;
width: 100%;
height: fit-content;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.16),
0 6px 18px rgba(0, 0, 0, 0.1);
}
/* When a window is being dragged */
@ -73,37 +134,42 @@
/* The Blue Marble Favicon */
.bm-favicon {
display: inline-block;
height: 2.5em;
margin-right: 1ch;
height: 2.2em;
margin-right: 0.45ch;
padding: 0.2rem;
border-radius: 12px;
vertical-align: middle;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.08));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.16),
0 8px 18px rgba(0, 0, 0, 0.12);
}
/* Header 1 */
.bm-window h1 {
display: inline-block;
font-size: x-large;
font-weight: bold;
font-size: 1rem;
font-weight: 700;
vertical-align: middle;
font-family: var(--bm-font-display);
text-transform: uppercase;
letter-spacing: 0.14em;
color: rgba(16, 33, 60, 0.96);
}
/* Header 1 when inside dragbar */
/* Or, when the custom class is used */
.bm-dragbar h1,
.bm-dragbar-text {
font-size: 1.2em;
font-size: 0.78rem;
user-select: none;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-shadow:
3px 0px rgba(21, 48, 99, 0.5),
-3px 0px rgba(21, 48, 99, 0.5),
0px 3px rgba(21, 48, 99, 0.5),
0px -3px rgba(21, 48, 99, 0.5),
3px 3px rgba(21, 48, 99, 0.5),
-3px 3px rgba(21, 48, 99, 0.5),
3px -3px rgba(21, 48, 99, 0.5),
-3px -3px rgba(21, 48, 99, 0.5);
font-family: var(--bm-font-display);
letter-spacing: 0.14em;
color: rgba(18, 37, 66, 0.95);
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.28);
}
/* Container for Header 1 when inside dragbar */
@ -114,9 +180,12 @@
/* Header 2 */
.bm-window h2 {
display: inline-block;
font-size: larger;
font-size: 0.88rem;
font-weight: 700;
vertical-align: middle;
font-family: var(--bm-font-display);
letter-spacing: 0.1em;
color: rgba(18, 35, 64, 0.96);
}
/* Header 3 */
@ -124,6 +193,24 @@
display: inline-block;
font-size: large;
font-weight: 700;
font-family: var(--bm-font-display);
letter-spacing: 0.08em;
color: rgba(18, 35, 64, 0.96);
}
/* Paragraphs */
.bm-window p {
color: var(--bm-text-secondary);
line-height: 1.5;
letter-spacing: 0.035em;
}
/* Horizontal dividers */
.bm-window hr {
border: none;
height: 1px;
margin: 0.32rem 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
}
/* Container with a vertically centered header 1-6 */
@ -135,42 +222,139 @@
/* Containers for "sections" of elements */
.bm-container {
margin: 0.5em 0;
margin: 0.24em 0;
}
/* Shared form controls */
.bm-window input,
.bm-window select,
.bm-window textarea,
.bm-window button {
font: inherit;
}
/* All window buttons */
.bm-window button {
background-color: #144eb9;
border-radius: 1em;
padding: 0 0.75ch;
appearance: none;
color: var(--bm-text-primary);
font-family: var(--bm-font-display);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(164, 208, 255, 0.14));
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 999px;
padding: 0.28em 0.62em;
min-height: 1.78em;
font-weight: 600;
letter-spacing: 0.1em;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.22),
0 8px 20px rgba(0, 0, 0, 0.1);
transition:
background 180ms ease,
border-color 180ms ease,
box-shadow 180ms ease,
filter 180ms ease,
opacity 180ms ease,
transform 180ms ease;
}
/* All window buttons when hovered/focused */
.bm-window button:hover, .bm-window button:focus-visible {
background-color: #1061e5;
.bm-window button:hover,
.bm-window button:focus-visible {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(186, 246, 255, 0.18));
border-color: rgba(230, 245, 255, 0.36);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.26),
0 10px 22px rgba(0, 0, 0, 0.12);
transform: translateY(-1px);
}
/* All window buttons when pressed (plus disabled color) */
.bm-window button:active,
.bm-window button:disabled {
background-color: #2e97ff;
/* Focus ring */
.bm-window button:focus-visible,
.bm-window input:focus-visible,
.bm-window select:focus-visible,
.bm-window textarea:focus-visible {
outline: none;
border-color: rgba(116, 231, 255, 0.62);
box-shadow:
0 0 0 3px rgba(116, 231, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
/* All window buttons when pressed */
.bm-window button:active {
transform: translateY(0) scale(0.98);
filter: brightness(0.98);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
0 8px 16px rgba(18, 36, 78, 0.24);
}
/* All window buttons when disabled */
.bm-window button:disabled, .bm-window button:disabled {
text-decoration: line-through;
.bm-window button:disabled {
opacity: 0.56;
cursor: not-allowed;
text-decoration: none;
transform: none;
filter: saturate(0.72);
box-shadow: none;
}
/* Button variants */
.bm-window button.bm-button-primary {
color: #07203b;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.42), rgba(186, 246, 255, 0.34));
border-color: rgba(239, 248, 255, 0.42);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.44),
0 10px 22px rgba(129, 182, 255, 0.14);
}
.bm-window button.bm-button-primary:hover,
.bm-window button.bm-button-primary:focus-visible {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.52), rgba(204, 250, 255, 0.36));
}
.bm-window button.bm-button-secondary {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08));
border-color: rgba(255, 255, 255, 0.18);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.14),
0 8px 20px rgba(0, 0, 0, 0.08);
}
.bm-window button.bm-button-secondary:hover,
.bm-window button.bm-button-secondary:focus-visible {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(186, 246, 255, 0.12));
}
/* Icon buttons (single character text content buttons) */
.bm-button-circle {
border: white 1px solid;
height: 1.5em;
width: 1.5em;
margin-top: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.16);
inline-size: 1.62rem;
block-size: 1.62rem;
min-height: 1.62rem !important;
min-width: 1.62rem;
margin-top: 0;
text-align: center;
line-height: 1em;
padding: 0 !important; /* Overrides the padding in ".bm-window button" */
line-height: 1;
padding: 0 !important;
aspect-ratio: 1 / 1;
border-radius: 50% !important;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.1)) !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 8px 18px rgba(0, 0, 0, 0.1) !important;
overflow: hidden;
flex: 0 0 auto;
font-size: 0.74rem;
}
.bm-button-circle svg {
width: 70%;
height: 70%;
}
/* Pin button */
@ -187,39 +371,52 @@
/* Transparent buttons */
.bm-window button.bm-button-trans {
background-color: unset;
background: transparent !important;
box-shadow: none !important;
border-color: transparent !important;
}
/* Transparent buttons on dark backgrounds when hovered */
.bm-button-trans.bm-button-hover-white:hover,
.bm-button-trans.bm-button-hover-white:focus {
background-color: rgba(255, 255, 255, 0.17);
background-color: rgba(255, 255, 255, 0.18) !important;
}
/* Transparent buttons on dark backgrounds when pressed */
.bm-button-trans.bm-button-hover-white:active {
background-color: rgba(255, 255, 255, 0.22);
background-color: rgba(255, 255, 255, 0.24) !important;
}
/* Transparent buttons on light backgrounds when hovered */
.bm-button-trans.bm-button-hover-black:hover,
.bm-button-trans.bm-button-hover-black:focus {
background-color: rgba(0, 0, 0, 0.17);
background-color: rgba(0, 0, 0, 0.14) !important;
}
/* Transparent buttons on light backgrounds when pressed */
.bm-button-trans.bm-button-hover-black:active {
background-color: rgba(0, 0, 0, 0.22);
background-color: rgba(0, 0, 0, 0.2) !important;
}
/* Shared inputs */
.bm-window input[type="number"],
.bm-window select,
.bm-window textarea {
color: var(--bm-text-primary);
font-family: var(--bm-font-body);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.06));
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
/* Tile (x, y) & Pixel (x, y) input fields */
input[type="number"].bm-input-coords {
appearance: auto;
-moz-appearance: textfield;
width: 5.5ch;
width: 5.9ch;
margin-left: 1ch;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
padding: 0.2em 0.35ch;
font-size: small;
}
@ -255,10 +452,7 @@ input[type="file"] {
/* Dropdown selection & dropdown (::picker) menus */
.bm-window select {
color: white;
background-color: #144eb9;
border-radius: 1em;
padding: 0 0.5ch;
padding: 0.22em 0.45ch;
}
/* Checkbox container (the label element) */
@ -266,11 +460,14 @@ input[type="file"] {
display: flex;
width: fit-content;
gap: 1ch;
align-items: center;
color: var(--bm-text-secondary);
}
/* Checkbox */
.bm-window input[type="checkbox"] {
width: 1em;
accent-color: #74e7ff;
}
/* Window content container */
@ -283,21 +480,31 @@ input[type="file"] {
/* Text areas */
.bm-window textarea {
font-size: small;
background-color: rgba(0, 0, 0, 0.2);
padding: 0 0.5ch;
height: 5.25em;
padding: 0.38em 0.52em;
height: 4em;
width: 100%;
resize: vertical;
line-height: 1.45;
}
.bm-window textarea::placeholder,
.bm-window input::placeholder {
color: rgba(47, 68, 102, 0.6);
}
/* Anchor/Links with no children */
.bm-window a:not(:has(*)) {
color: rgba(21, 88, 164, 0.94);
text-decoration: underline;
text-decoration-color: rgba(21, 88, 164, 0.35);
}
/* Small elements */
.bm-window small {
font-size: x-small;
color: lightgray;
font-family: var(--bm-font-display);
letter-spacing: 0.12em;
color: var(--bm-text-secondary);
}
/* List items of unordered lists */
@ -310,6 +517,25 @@ input[type="file"] {
.bm-window .bm-container.bm-scrollable {
max-height: var(--bm-scrollable-max-height, calc(80vh - 150px));
overflow: auto;
scrollbar-width: thin;
scrollbar-color: rgba(146, 221, 255, 0.42) rgba(255, 255, 255, 0.05);
}
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(116, 231, 255, 0.48), rgba(83, 141, 255, 0.4));
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
/* Flex children space between */
@ -318,7 +544,7 @@ input[type="file"] {
align-content: center;
justify-content: space-between;
align-items: center;
gap: 0.5ch;
gap: 0.35ch;
}
/* Flex children space center */
@ -327,7 +553,7 @@ input[type="file"] {
align-content: center;
justify-content: center;
align-items: center;
gap: 0.5ch;
gap: 0.35ch;
}
/* ASCII Art */
@ -344,9 +570,8 @@ input[type="file"] {
/* Containers for "sections" of elements in windowed mode */
/* Does not apply to the main window */
.bm-windowed .bm-container:not(#bm-window-main .bm-container) {
margin-top: 0.25em;
margin-bottom: 0.25em;
/* Do not use 'margin' shorthand, as it will override left/right margin */
margin-top: 0.18em;
margin-bottom: 0.18em;
}
/* Header 1 in windowed mode */