Added Settings Window & Highlight UI

This commit is contained in:
SwingTheVine 2026-03-04 15:42:32 -05:00
parent dd95406b31
commit fbe86b8965
18 changed files with 498 additions and 26 deletions

View file

@ -389,6 +389,33 @@ input[type=file] {
font-size: 1em;
}
/* src/WindowSettings.css */
#bm-window-settings .bm-highlight-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 25%;
min-width: 3ch;
max-width: 30ch;
}
#bm-window-settings .bm-highlight-grid > button {
width: 100%;
aspect-ratio: 1 / 1;
background-color: white;
border: #333 1px solid;
border-radius: 0;
box-sizing: border-box;
}
#bm-window-settings .bm-highlight-grid > button[data-status=Incorrect] {
background-color: brown;
}
#bm-window-settings .bm-highlight-grid > button[data-status=Template] {
background-color: darkslategray;
}
#bm-window-settings .bm-highlight-grid > button:hover,
#bm-window-settings .bm-highlight-grid > button:focus {
opacity: 0.8;
}
/* src/WindowWizard.css */
#bm-wizard-tlist {
display: flex;

View file

@ -2,7 +2,7 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.91.10
// @version 0.91.38
// @description A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
// @description:en A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
// @author SwingTheVine
@ -521,6 +521,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.name = name2;
this.version = version2;
this.apiManager = null;
this.settingsManager = null;
this.outputStatusId = "bm-output-status";
this.overlay = null;
this.currentParent = null;
@ -533,6 +534,13 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
setApiManager(apiManager2) {
this.apiManager = apiManager2;
}
/** Populates the settingsManager variable with the settingsManager class.
* @param {SettingsManager} settingsManager - The settingsManager class instance
* @since 0.91.11
*/
setSettingsManager(settingsManager2) {
this.settingsManager = settingsManager2;
}
/** Finishes building an element.
* Call this after you are finished adding children.
* If the element will have no children, call it anyways.
@ -1713,10 +1721,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
(part, i) => i == 0 ? part : part[0].toUpperCase() + part.slice(1)
).join("")] = value;
} else if (property.startsWith("aria")) {
const camelCase = property.slice(5).split("-").map(
(part, i) => i == 0 ? part : part[0].toUpperCase() + part.slice(1)
).join("");
element["aria" + camelCase[0].toUpperCase() + camelCase.slice(1)] = value;
element.setAttribute(property, value);
} else {
element[property] = value;
}
@ -2707,7 +2712,7 @@ Did you try clicking the canvas first?`);
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": "Espresso" }).buildElement().addLi({ "textContent": "BEST FAN" }).buildElement().addLi({ "textContent": "Jack" }).buildElement().addLi({ "textContent": "raiken_au" }).buildElement().addLi({ "textContent": "Jacob" }).buildElement().addLi({ "textContent": "StupidOne" }).buildElement().addLi({ "textContent": "1 Anonymous Supporter" }).buildElement().buildElement().buildElement().buildElement().buildElement().buildOverlay(this.windowParent);
}).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": "Espresso" }).buildElement().addLi({ "textContent": "BEST FAN" }).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`);
}
};
@ -2785,7 +2790,7 @@ Did you try clicking the canvas first?`);
};
}).buildElement().addButton({ "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": `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().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) => {
button.onclick = (event) => {
event.preventDefault();
const formData = new FormData(document.querySelector(`#${this.windowID} form`));
@ -3267,7 +3272,11 @@ Did you try clicking the canvas first?`);
}).buildElement().addButton({ "textContent": "Filter" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _WindowMain_instances, buildWindowFilter_fn).call(this);
}).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": "\u{1F9D9}", "title": "Template Wizard" }, (instance, button) => {
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);
@ -3388,6 +3397,158 @@ Version: ${this.version}`, "readOnly": true }).buildElement().buildElement().add
GM.setValue("bmUserSettings", JSON.stringify(userSettings2));
};
// src/WindowSettings.js
var _WindowSettings_instances, errorOverrideFailure_fn;
var WindowSettings = class extends Overlay {
/** Constructor for the Settings window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @since 0.91.11
* @see {@link Overlay#constructor} for examples
*/
constructor(name2, version2) {
super(name2, version2);
__privateAdd(this, _WindowSettings_instances);
this.window = null;
this.windowID = "bm-window-settings";
this.windowParent = document.body;
}
/** Spawns a Settings window.
* If another settings window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.91.11
*/
buildWindow() {
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window" }).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Color Filter"', "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", "textContent": "\u2716", "aria-label": 'Close window "Color Filter"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
};
button.ontouchend = () => {
button.click();
};
}).buildElement().buildElement().buildElement().addDiv({ "class": "bm-window-content" }).addDiv({ "class": "bm-container bm-center-vertically" }).addHeader(1, { "textContent": "Settings" }).buildElement().buildElement().addHr().buildElement().addDiv({ "class": "bm-container bm-scrollable" }, (instance, div) => {
this.buildHighlight();
}).buildElement().buildElement().buildElement().buildOverlay(this.windowParent);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}
/** Builds the highlight section of the window.
* This should be overriden by {@link SettingsManager}
* @since 0.91.11
*/
buildHighlight() {
__privateMethod(this, _WindowSettings_instances, errorOverrideFailure_fn).call(this, "Pixel Highlight");
}
};
_WindowSettings_instances = new WeakSet();
/** Displays an error when a settings category fails to load.
* @param {string} name - The name of the category
*/
errorOverrideFailure_fn = function(name2) {
this.window = this.addDiv({ "class": "bm-container" }).addHeader(2, { "textContent": name2 }).buildElement().addHr().buildElement().addP({ "innerHTML": `An error occured loading the ${name2} category. <code>SettingsManager</code> failed to override the ${name2} function inside <code>WindowSettings</code>.` }).buildElement().buildElement();
};
// src/settingsManager.js
var _SettingsManager_instances, updateHighlightSettings_fn;
var SettingsManager = class extends WindowSettings {
/** Constructor for the SettingsManager class
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @param {Object} userSettings - The user settings as an object
* @since 0.91.11
*/
constructor(name2, version2, userSettings2) {
super(name2, version2);
__privateAdd(this, _SettingsManager_instances);
this.userSettings = userSettings2;
this.lastUpdateTime = 0;
}
// This is one of the most insane OOP setups I have ever laid my eyes on
/** Builds the "highlight" category of the settings window
* @since 0.91.18
* @see WindowSettings#buildHighlight
*/
buildHighlight() {
this.window = this.addDiv({ "class": "bm-container" }).addHeader(2, { "textContent": "Pixel Highlight" }).buildElement().addHr().buildElement().addDiv({ "style": "margin-left: 1.5ch;" }).addP({ "id": "bm-highlight-grid-label", "textContent": "Create a custom pattern:" }).buildElement().addDiv({ "class": "bm-highlight-grid", "role": "group", "aria-labelledby": "bm-highlight-grid-label" }).addButton({ "data-status": "Disabled", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [-1, -1]);
}).buildElement().addButton({ "data-status": "Incorrect", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [0, -1]);
}).buildElement().addButton({ "data-status": "Disabled", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [1, -1]);
}).buildElement().addButton({ "data-status": "Incorrect", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [-1, 0]);
}).buildElement().addButton({ "data-status": "Template", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [0, 0]);
}).buildElement().addButton({ "data-status": "Incorrect", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [1, 0]);
}).buildElement().addButton({ "data-status": "Disabled", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [-1, 1]);
}).buildElement().addButton({ "data-status": "Incorrect", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [0, 1]);
}).buildElement().addButton({ "data-status": "Disabled", "aria-label": "Sub-pixel disabled" }, (instance, button) => {
button.onclick = () => __privateMethod(this, _SettingsManager_instances, updateHighlightSettings_fn).call(this, button, [1, 1]);
}).buildElement().buildElement().buildElement().buildElement();
}
};
_SettingsManager_instances = new WeakSet();
/** Updates the display of the highlight buttons in the settings window.
* Additionally, it will update user settings with the new selection.
* @param {HTMLButtonElement} button - The button that was pressed
* @param {Array<number, number>} coords - The relative coordinates of the button
*/
updateHighlightSettings_fn = function(button, coords2) {
console.log(coords2);
button.disabled = true;
const status = button.dataset["status"];
const userStorageOld = this.userSettings?.highlight || [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
let userStorageChange = [2, 0, 0];
const userStorageNew = userStorageOld;
console.log(userStorageOld);
switch (status) {
// If the button was in the "Disabled" state
case "Disabled":
button.dataset["status"] = "Incorrect";
button.ariaLabel = "Sub-pixel incorrect";
userStorageChange = [1, ...coords2];
break;
// If the button was in the "Incorrect" state
case "Incorrect":
button.dataset["status"] = "Template";
button.ariaLabel = "Sub-pixel template";
userStorageChange = [2, ...coords2];
break;
// If the button was in the "Template" state
case "Template":
button.dataset["status"] = "Disabled";
button.ariaLabel = "Sub-pixel disabled";
userStorageChange = [0, ...coords2];
break;
}
console.log(userStorageChange);
const indexOfChange = userStorageOld.findIndex(([, x, y]) => x == userStorageChange[1] && y == userStorageChange[2]);
console.log(indexOfChange);
if (userStorageChange[0] != 0) {
if (indexOfChange != -1) {
userStorageNew[indexOfChange] = userStorageChange;
} else {
userStorageNew.push(userStorageChange);
}
} else if (indexOfChange != -1) {
userStorageNew.splice(indexOfChange, 1);
}
console.log(userStorageNew);
this.userSettings["highlight"] = userStorageNew;
button.disabled = false;
};
// src/main.js
var name = GM_info.script.name.toString();
var version = GM_info.script.version.toString();
@ -3493,15 +3654,17 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
document.head?.appendChild(stylesheetLink);
}
var stylesheetLink;
var userSettings = JSON.parse(GM_getValue("bmUserSettings", "{}"));
var observers = new Observers();
var windowMain = new WindowMain(name, version);
var templateManager = new TemplateManager(name, version, windowMain);
var apiManager = new ApiManager(templateManager);
var settingsManager = new SettingsManager(name, version, userSettings);
windowMain.setSettingsManager(settingsManager);
windowMain.setApiManager(apiManager);
var storageTemplates = JSON.parse(GM_getValue("bmTemplates", "{}"));
console.log(storageTemplates);
templateManager.importJSON(storageTemplates);
var userSettings = JSON.parse(GM_getValue("bmUserSettings", "{}"));
console.log(userSettings);
console.log(Object.keys(userSettings).length);
if (Object.keys(userSettings).length == 0) {

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

@ -51,7 +51,7 @@
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer"><img alt="Blue Marble Website" src="https://img.shields.io/badge/Blue_Marble_Website-crqch-blue?style=flat&logo=globe&logoColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-212hrs_17mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-1125-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-1153-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Lines of Code" src="https://img.shields.io/badge/Lines_Of_Code-6620-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-5414-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-71.85%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.91.10",
"version": "0.91.38",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.91.10",
"version": "0.91.38",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.91.10",
"version": "0.91.38",
"type": "module",
"homepage": "https://bluemarble.lol/",
"repository": {

View file

@ -2,7 +2,7 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.91.10
// @version 0.91.38
// @description A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
// @description:en A userscript to enhance the user experience on Wplace.live. This includes, but is not limited to: uploading images to display locally on a canvas, adding a button to move the Wplace color palette menu, and other QoL features.
// @author SwingTheVine

View file

@ -37,6 +37,9 @@ export default class Overlay {
/** The API manager instance. Later populated when setApiManager is called @type {ApiManager} */
this.apiManager = null;
/** The Settings Manager instance. Later populated when setSettingsManager is called @type {SettingsManager} */
this.settingsManager = null;
this.outputStatusId = 'bm-output-status'; // ID for status element
@ -51,6 +54,12 @@ export default class Overlay {
*/
setApiManager(apiManager) {this.apiManager = apiManager;}
/** Populates the settingsManager variable with the settingsManager class.
* @param {SettingsManager} settingsManager - The settingsManager class instance
* @since 0.91.11
*/
setSettingsManager(settingsManager) {this.settingsManager = settingsManager;}
/** Creates an element.
* For **internal use** of the {@link Overlay} class.
* @param {string} tag - The tag name as a string.
@ -109,10 +118,7 @@ export default class Overlay {
).join('')
] = value;
} else if (property.startsWith('aria')) {
const camelCase = property.slice(5).split('-').map(
(part, i) => (i == 0) ? part : part[0].toUpperCase() + part.slice(1)
).join('');
element['aria' + camelCase[0].toUpperCase() + camelCase.slice(1)] = value;
element.setAttribute(property, value); // We can't do the solution for 'data', as 'aria-labelledby' would fail to apply
} else {
element[property] = value;
}

View file

@ -105,7 +105,7 @@ export default class WindowCredts extends Overlay {
.addLi({'textContent': 'raiken_au'}).buildElement()
.addLi({'textContent': 'Jacob'}).buildElement()
.addLi({'textContent': 'StupidOne'}).buildElement()
.addLi({'textContent': '1 Anonymous Supporter'}).buildElement()
.addLi({'textContent': '2 Anonymous Supporters'}).buildElement()
.buildElement()
.buildElement()
.buildElement()

View file

@ -124,7 +124,7 @@ export default class WindowFilter extends Overlay {
.addSpan({'id': 'bm-filter-tot-completed', 'innerHTML': '??? ???'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container'})
.addP({'innerHTML': `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()
.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'})

View file

@ -163,6 +163,11 @@ export default class WindowMain extends Overlay {
.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;

36
src/WindowSettings.css Normal file
View file

@ -0,0 +1,36 @@
/* @since 0.91.22 */
/* Highlight pattern container */
#bm-window-settings .bm-highlight-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 25%;
min-width: 3ch;
max-width: 30ch;
}
/* Highlight pattern button */
#bm-window-settings .bm-highlight-grid > button {
width: 100%;
aspect-ratio: 1 / 1;
background-color: white;
border: #333 1px solid;
border-radius: 0;
box-sizing: border-box;
}
/* Highlight pattern button in 'Incorrect' mode */
#bm-window-settings .bm-highlight-grid > button[data-status="Incorrect"] {
background-color: brown;
}
/* Highlight pattern button in 'Template' mode */
#bm-window-settings .bm-highlight-grid > button[data-status="Template"] {
background-color: darkslategray;
}
/* Highlight pattern button when hovered/focused */
#bm-window-settings .bm-highlight-grid > button:hover,
#bm-window-settings .bm-highlight-grid > button:focus {
opacity: 0.8;
}

86
src/WindowSettings.js Normal file
View file

@ -0,0 +1,86 @@
import Overlay from "./Overlay";
/** The overlay builder for the settings window in Blue Marble.
* The logic for this window is managed in {@link SettingsManager}
* @description This class handles the overlay UI for the settings window of the Blue Marble userscript.
* @class WindowSettings
* @since 0.91.11
* @see {@link Overlay} for examples
*/
export default class WindowSettings extends Overlay {
/** Constructor for the Settings window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @since 0.91.11
* @see {@link Overlay#constructor} for examples
*/
constructor(name, version) {
super(name, version); // Executes the code in the Overlay constructor
this.window = null; // Contains the *window* DOM tree
this.windowID = 'bm-window-settings'; // The ID attribute for this window
this.windowParent = document.body; // The parent of the window DOM tree
}
/** Spawns a Settings window.
* If another settings window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.91.11
*/
buildWindow() {
// If a settings window already exists, close it
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
return;
}
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window'})
.addDragbar()
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Color Filter"', 'data-button-status': 'expanded'}, (instance, button) => {
button.onclick = () => instance.handleMinimization(button);
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', 'textContent': '✖', 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.buildElement()
.buildElement()
.addDiv({'class': 'bm-window-content'})
.addDiv({'class': 'bm-container bm-center-vertically'})
.addHeader(1, {'textContent': 'Settings'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container bm-scrollable'}, (instance, div) => {
// Each category in the settings window
this.buildHighlight();
}).buildElement()
.buildElement()
.buildElement().buildOverlay(this.windowParent);
// Creates dragging capability on the drag bar for dragging the window
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}
/** Displays an error when a settings category fails to load.
* @param {string} name - The name of the category
*/
#errorOverrideFailure(name) {
this.window = this.addDiv({'class': 'bm-container'})
.addHeader(2, {'textContent': name}).buildElement()
.addHr().buildElement()
.addP({'innerHTML': `An error occured loading the ${name} category. <code>SettingsManager</code> failed to override the ${name} function inside <code>WindowSettings</code>.`}).buildElement()
.buildElement();
}
/** Builds the highlight section of the window.
* This should be overriden by {@link SettingsManager}
* @since 0.91.11
*/
buildHighlight() {
this.#errorOverrideFailure('Pixel Highlight');
}
}

View file

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

View file

@ -8,6 +8,7 @@ import TemplateManager from './templateManager.js';
import { consoleLog, consoleWarn } from './utils.js';
import WindowMain from './WindowMain.js';
import WindowTelemetry from './WindowTelemetry.js';
import SettingsManager from './settingsManager.js';
const name = GM_info.script.name.toString(); // Name of userscript
const version = GM_info.script.version.toString(); // Version of userscript
@ -186,19 +187,22 @@ if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
document.head?.appendChild(stylesheetLink);
}
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
// CONSTRUCTORS
const observers = new Observers(); // Constructs a new Observers object
const windowMain = new WindowMain(name, version); // Constructs a new Overlay object for the main overlay
const templateManager = new TemplateManager(name, version, windowMain); // Constructs a new TemplateManager object
const apiManager = new ApiManager(templateManager); // Constructs a new ApiManager object
const settingsManager = new SettingsManager(name, version, userSettings); // Constructs a new SettingsManager
windowMain.setSettingsManager(settingsManager); // Sets the settings manager
windowMain.setApiManager(apiManager); // Sets the API manager
const storageTemplates = JSON.parse(GM_getValue('bmTemplates', '{}'));
console.log(storageTemplates);
templateManager.importJSON(storageTemplates); // Loads the templates
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
console.log(userSettings);
console.log(Object.keys(userSettings).length);

144
src/settingsManager.js Normal file
View file

@ -0,0 +1,144 @@
import WindowSettings from "./WindowSettings";
/** SettingsManager class for handling user settings and making them persist between sessions.
* Logic for {@link WindowSettings} is managed here.
* @class SettingsManager
* @since 0.91.11
* @examples
* {
* "uuid": "497dcba3-ecbf-4587-a2dd-5eb0665e6880",
* "telemetry": 1,
* "flags": ["willHighlight", "openWindowed"],
* "highlight": [[1,0,-1],[1,-1,0],[2,1,0],[1,0,1]],
* "filter": [-2,0,4,5,6,29,63]
* }
*/
export default class SettingsManager extends WindowSettings {
/** Constructor for the SettingsManager class
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @param {Object} userSettings - The user settings as an object
* @since 0.91.11
*/
constructor(name, version, userSettings) {
super(name, version); // Executes WindowSettings constructor
this.userSettings = userSettings; // User settings as an Object
this.lastUpdateTime = 0; // When this unix timestamp is within the last 5 seconds, we should save this.userSettings to storage
}
// This is one of the most insane OOP setups I have ever laid my eyes on
/** Builds the "highlight" category of the settings window
* @since 0.91.18
* @see WindowSettings#buildHighlight
*/
buildHighlight() {
// Constructs the category and adds it to the window
this.window = this.addDiv({'class': 'bm-container'})
.addHeader(2, {'textContent': 'Pixel Highlight'}).buildElement()
.addHr().buildElement()
.addDiv({'style': 'margin-left: 1.5ch;'})
.addP({'id': 'bm-highlight-grid-label', 'textContent': 'Create a custom pattern:'}).buildElement()
.addDiv({'class': 'bm-highlight-grid', 'role': 'group', 'aria-labelledby': 'bm-highlight-grid-label'})
.addButton({'data-status': 'Disabled', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [-1, -1]);}).buildElement()
.addButton({'data-status': 'Incorrect', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [0, -1]);}).buildElement()
.addButton({'data-status': 'Disabled', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [1, -1]);}).buildElement()
.addButton({'data-status': 'Incorrect', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [-1, 0]);}).buildElement()
.addButton({'data-status': 'Template', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [0, 0]);}).buildElement()
.addButton({'data-status': 'Incorrect', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [1, 0]);}).buildElement()
.addButton({'data-status': 'Disabled', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [-1, 1]);}).buildElement()
.addButton({'data-status': 'Incorrect', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [0, 1]);}).buildElement()
.addButton({'data-status': 'Disabled', 'aria-label': 'Sub-pixel disabled'}, (instance, button) => {button.onclick = () => this.#updateHighlightSettings(button, [1, 1]);}).buildElement()
.buildElement()
.buildElement()
.buildElement();
// TODO: First-time load of highlight settings from user storage
}
/** Updates the display of the highlight buttons in the settings window.
* Additionally, it will update user settings with the new selection.
* @param {HTMLButtonElement} button - The button that was pressed
* @param {Array<number, number>} coords - The relative coordinates of the button
*/
#updateHighlightSettings(button, coords) {
console.log(coords);
button.disabled = true; // Disabled the button until we are done
const status = button.dataset['status']; // Obtains the current status of the button
/** Obtains the old highlight storage, or sets it to default. @type {Array<number[]>} */
const userStorageOld = this.userSettings?.highlight || [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
let userStorageChange = [2, 0, 0]; // The new change to the user storage
const userStorageNew = userStorageOld; // The old storage with the new change
console.log(userStorageOld);
// For each different type of status...
switch (status) {
// If the button was in the "Disabled" state
case 'Disabled':
// Change to "Incorrect"
button.dataset['status'] = 'Incorrect';
button.ariaLabel = 'Sub-pixel incorrect';
userStorageChange = [1, ...coords];
break;
// If the button was in the "Incorrect" state
case 'Incorrect':
// Change to "Template"
button.dataset['status'] = 'Template';
button.ariaLabel = 'Sub-pixel template';
userStorageChange = [2, ...coords];
break;
// If the button was in the "Template" state
case 'Template':
// Change to "Disabled"
button.dataset['status'] = 'Disabled';
button.ariaLabel = 'Sub-pixel disabled';
userStorageChange = [0, ...coords];
break;
}
console.log(userStorageChange);
// Finds the index of the pixel to change
const indexOfChange = userStorageOld.findIndex(([, x, y]) => ((x == userStorageChange[1]) && (y == userStorageChange[2])));
console.log(indexOfChange);
// If the new sub-pixel state is NOT disabled
if (userStorageChange[0] != 0) {
// If a sub-pixel was found...
if (indexOfChange != -1) {
userStorageNew[indexOfChange] = userStorageChange;
} else {
userStorageNew.push(userStorageChange);
}
} else if (indexOfChange != -1) {
// Else, it is disabled. We want to remove it if it exists.
userStorageNew.splice(indexOfChange, 1); // Removes 1 index from the array at the index of the pixel change
}
console.log(userStorageNew);
this.userSettings['highlight'] = userStorageNew;
// TODO: Add timer update here
button.disabled = false; // Reenables the button since we are done
}
}