Release 0.92.1

This commit is contained in:
Alexey 2026-04-20 22:51:30 +05:00
parent ed7127d40b
commit 803fb97f97
15 changed files with 1196 additions and 249 deletions

View file

@ -10,6 +10,7 @@
// ES Module imports
import esbuild from 'esbuild';
import crypto from 'crypto';
import fs from 'fs';
import { execSync } from 'child_process';
import { consoleStyle } from './utils.js';
@ -22,6 +23,24 @@ const terser = require('terser');
const isGitHub = !!process.env?.GITHUB_ACTIONS; // Is this running in a GitHub Action Workflow?'
/** Appends a build hash comment to an output file.
* The hash is based on the file contents before the hash comment is added.
* @param {string} path - Path to the file
* @param {'js' | 'css'} type - Output type for comment syntax
* @returns {string} The short build hash
* @since 0.92.0
*/
function appendBuildHashComment(path, type = 'js') {
const content = fs.readFileSync(path, 'utf8').trimEnd();
const hash = crypto.createHash('sha256').update(content, 'utf8').digest('hex').slice(0, 12);
const comment = (type == 'css')
? `/* Build Hash: ${hash} */`
: `// Build Hash: ${hash}`;
fs.writeFileSync(path, `${content}\n\n${comment}\n`, 'utf8');
return hash;
}
console.log(`${consoleStyle.BLUE}Starting build...${consoleStyle.RESET}`);
// Tries to build the wiki if build.js is run in a GitHub Workflow
@ -214,4 +233,16 @@ esbuild.build({
fs.writeFileSync(`dist/${greasyForkName}.user.js`, greasyForkBMjs, 'utf-8');
const buildHashes = {
'BlueMarble.user.css': appendBuildHashComment('dist/BlueMarble.user.css', 'css'),
'BlueMarble.user.js': appendBuildHashComment('dist/BlueMarble.user.js', 'js'),
[`${standaloneName}.user.js`]: appendBuildHashComment(`dist/${standaloneName}.user.js`, 'js'),
[`${greasyForkName}.user.css`]: appendBuildHashComment(`dist/${greasyForkName}.user.css`, 'css'),
[`${greasyForkName}.user.js`]: appendBuildHashComment(`dist/${greasyForkName}.user.js`, 'js')
};
console.log(`${consoleStyle.GREEN + consoleStyle.BOLD + consoleStyle.UNDERLINE}Building complete!${consoleStyle.RESET}`);
console.log(`Build hashes:`);
for (const [file, hash] of Object.entries(buildHashes)) {
console.log(`- ${file}: ${hash}`);
}

View file

@ -1,192 +1,3 @@
/* src/WindowFilter.css */
#bm-window-filter p svg {
display: inline;
height: 1em;
fill: white;
}
#bm-filter-flex {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 1em 3ch;
}
#bm-window-filter .bm-filter-color {
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;
}
#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-container-rgb {
display: block;
border: thick double darkslategray;
width: fit-content;
height: fit-content;
padding: 1ch;
}
#bm-window-filter .bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
background:
conic-gradient(
#aa0000 0%,
#aaaa00 16.6%,
#00aa00 33.3%,
#00aaaa 50%,
#0000aa 66.6%,
#aa00aa 83.3%,
#aa0000 100%);
}
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
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;
}
#bm-window-filter .bm-filter-color[data-id="0"] .bm-filter-container-rgb {
background-color: transparent !important;
}
#bm-window-filter .bm-filter-container-rgb button {
padding: 0.75em 0.5ch;
}
#bm-window-filter .bm-filter-container-rgb svg {
width: 4ch;
}
#bm-window-filter .bm-filter-color > .bm-flex-between {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
#bm-window-filter .bm-filter-color small {
font-size: 0.75em;
}
#bm-window-filter .bm-filter-color.bm-color-hide {
display: none;
}
#bm-window-filter.bm-windowed #bm-filter-flex {
flex-direction: column;
gap: 0.25em;
}
#bm-window-filter.bm-windowed .bm-filter-color {
width: auto;
margin: 0;
padding: 0;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb {
display: flex;
width: 100%;
gap: 0.5ch;
align-items: center;
padding: 0.1em 0.5ch;
border: none;
border-radius: 1em;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
padding: 0.5em 0.25ch;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
width: 3ch;
}
#bm-window-filter.bm-windowed .bm-filter-color h2 {
font-size: 0.75em;
}
#bm-window-filter #bm-filter-windowed-color-totals {
font-size: 1em;
}
/* src/WindowSettings.css */
#bm-window-settings div:has(> .bm-highlight-preset-container) {
width: fit-content;
justify-content: flex-start;
}
#bm-window-settings .bm-highlight-preset-container {
display: flex;
flex-direction: column;
width: 13%;
}
#bm-window-settings .bm-highlight-preset-container span {
width: fit-content;
margin: auto;
font-size: 0.7em;
}
#bm-window-settings .bm-highlight-preset-container button {
width: fit-content;
padding: 0;
border-radius: 0;
}
#bm-window-settings .bm-highlight-preset-container svg {
stroke: #333;
stroke-width: 0.02px;
width: 100%;
min-width: 1.5ch;
max-width: 14.5ch;
}
#bm-window-settings .bm-highlight-preset-container button:hover svg,
#bm-window-settings .bm-highlight-preset-container button:focus svg {
opacity: 0.9;
}
#bm-window-settings .bm-highlight-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 25%;
min-width: 3ch;
max-width: 15ch;
}
#bm-window-settings .bm-highlight-grid > button {
width: 100%;
padding: 0;
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;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
#bm-wizard-tlist > .bm-container {
width: 100%;
justify-content: flex-start;
background-color: rgba(21, 48, 99, 0.9);
border-radius: 1em;
padding: 0.5em;
transition: background-color 0.3s ease;
}
#bm-wizard-tlist > .bm-container:hover,
#bm-wizard-tlist > .bm-container:focus-within {
background-color: rgba(17, 40, 85, 0.9);
}
#bm-wizard-tlist .bm-wizard-template-container-image {
height: 100%;
font-size: xxx-large;
}
#bm-wizard-tlist .bm-wizard-template-container-flavor {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
/* src/confettiManager.css */
div:has(> confetti-piece) {
position: absolute;
@ -245,9 +56,6 @@ confetti-piece {
"Arial";
letter-spacing: 0.05em;
}
.bm-window.bm-windowed {
max-width: 300px;
}
.bm-dragbar {
display: grid;
grid-template-columns: auto 1fr auto;
@ -423,6 +231,7 @@ input[type=file] {
}
.bm-window-content {
overflow: hidden;
max-height: calc(100% - 5px);
transition: height 300ms cubic-bezier(.4, 0, .2, 1);
}
.bm-window textarea {
@ -444,7 +253,7 @@ input[type=file] {
margin-left: 5ch;
}
.bm-window .bm-container.bm-scrollable {
max-height: calc(80vh - 150px);
max-height: var(--bm-scrollable-max-height, calc(80vh - 150px));
overflow: auto;
}
.bm-flex-between {
@ -476,4 +285,273 @@ input[type=file] {
font-size: 1em;
}
/* src/WindowFilter.css */
#bm-window-filter p svg {
display: inline;
height: 1em;
fill: white;
}
#bm-filter-flex {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 1em 3ch;
}
#bm-window-filter .bm-filter-color {
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;
}
#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-container-rgb {
display: block;
border: thick double darkslategray;
width: fit-content;
height: fit-content;
padding: 1ch;
}
#bm-window-filter .bm-filter-color[data-id="-2"] .bm-filter-container-rgb {
background:
conic-gradient(
#aa0000 0%,
#aaaa00 16.6%,
#00aa00 33.3%,
#00aaaa 50%,
#0000aa 66.6%,
#aa00aa 83.3%,
#aa0000 100%);
}
#bm-window-filter .bm-filter-color[data-id="-1"] .bm-filter-container-rgb {
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;
}
#bm-window-filter .bm-filter-color[data-id="0"] .bm-filter-container-rgb {
background-color: transparent !important;
}
#bm-window-filter .bm-filter-container-rgb button {
padding: 0.75em 0.5ch;
}
#bm-window-filter .bm-filter-container-rgb svg {
width: 4ch;
}
#bm-window-filter .bm-filter-color > .bm-flex-between {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
#bm-window-filter .bm-filter-color small {
font-size: 0.75em;
}
#bm-window-filter .bm-filter-color.bm-color-hide {
display: none;
}
#bm-window-filter.bm-windowed {
--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;
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;
}
#bm-window-filter.bm-windowed .bm-window-content {
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr);
grid-row: 2;
min-height: 0;
min-width: 0;
overflow: hidden;
}
#bm-window-filter.bm-windowed #bm-filter-flex {
flex-direction: column;
align-items: stretch;
gap: 0.25em;
width: 100%;
align-self: stretch;
min-width: 0;
box-sizing: border-box;
}
#bm-window-filter.bm-windowed .bm-filter-color {
width: 100%;
max-width: none;
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#bm-window-filter.bm-windowed .bm-filter-color > .bm-flex-between {
width: 100%;
min-width: 0;
flex: 1 1 auto;
}
#bm-window-filter.bm-windowed .bm-container.bm-scrollable {
display: block;
grid-row: 4;
min-height: 0;
min-width: 0;
height: 100%;
width: 100%;
max-height: 100% !important;
overflow: auto;
box-sizing: border-box;
}
#bm-window-filter.bm-windowed .bm-resize-corner {
position: absolute;
right: 0;
bottom: 0;
display: block;
width: 28px;
height: 28px;
z-index: 5;
cursor: nwse-resize;
pointer-events: auto;
opacity: 1;
touch-action: none;
user-select: none;
background: transparent;
border: none;
box-shadow: none;
}
#bm-window-filter.bm-windowed .bm-resize-corner:hover,
#bm-window-filter.bm-windowed .bm-resize-corner.bm-resizing {
opacity: 1;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb {
display: flex;
width: 100%;
min-width: 0;
flex: 1 1 auto;
gap: 0.5ch;
align-items: center;
padding: 0.1em 0.5ch;
border: none;
border-radius: 1em;
box-sizing: border-box;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
padding: 0.5em 0.25ch;
flex: 0 0 auto;
}
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
width: 3ch;
}
#bm-window-filter.bm-windowed .bm-filter-color h2 {
font-size: 0.75em;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#bm-window-filter #bm-filter-windowed-color-totals {
font-size: 1em;
}
/* src/WindowSettings.css */
#bm-window-settings div:has(> .bm-highlight-preset-container) {
width: fit-content;
justify-content: flex-start;
}
#bm-window-settings .bm-highlight-preset-container {
display: flex;
flex-direction: column;
width: 13%;
}
#bm-window-settings .bm-highlight-preset-container span {
width: fit-content;
margin: auto;
font-size: 0.7em;
}
#bm-window-settings .bm-highlight-preset-container button {
width: fit-content;
padding: 0;
border-radius: 0;
}
#bm-window-settings .bm-highlight-preset-container svg {
stroke: #333;
stroke-width: 0.02px;
width: 100%;
min-width: 1.5ch;
max-width: 14.5ch;
}
#bm-window-settings .bm-highlight-preset-container button:hover svg,
#bm-window-settings .bm-highlight-preset-container button:focus svg {
opacity: 0.9;
}
#bm-window-settings .bm-highlight-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
width: 25%;
min-width: 3ch;
max-width: 15ch;
}
#bm-window-settings .bm-highlight-grid > button {
width: 100%;
padding: 0;
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;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
#bm-wizard-tlist > .bm-container {
width: 100%;
justify-content: flex-start;
background-color: rgba(21, 48, 99, 0.9);
border-radius: 1em;
padding: 0.5em;
transition: background-color 0.3s ease;
}
#bm-wizard-tlist > .bm-container:hover,
#bm-wizard-tlist > .bm-container:focus-within {
background-color: rgba(17, 40, 85, 0.9);
}
#bm-wizard-tlist .bm-wizard-template-container-image {
height: 100%;
font-size: xxx-large;
}
#bm-wizard-tlist .bm-wizard-template-container-flavor {
flex-direction: column;
align-items: flex-start;
gap: 0;
}
/* src/main.css */
/* Build Hash: 4336138174a3 */

View file

@ -2,14 +2,14 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.92.0
// @version 0.92.1
// @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
// @license MPL-2.0
// @supportURL https://discord.gg/tpeBPy46hf
// @homepageURL https://bluemarble.lol/
// @icon https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/78477321232b29c09e3794c360068d7d23a0172c/dist/assets/Favicon.png
// @icon https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/2cd51bf91944ae2acb253ea5bbd76f79b7a2edd3/dist/assets/Favicon.png
// @updateURL https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/BlueMarble-For-GreasyFork.user.js
// @downloadURL https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/BlueMarble-For-GreasyFork.user.js
// @match https://wplace.live/*
@ -21,7 +21,7 @@
// @grant GM_xmlhttpRequest
// @grant GM.download
// @connect telemetry.thebluecorner.net
// @resource CSS-BM-File https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/78477321232b29c09e3794c360068d7d23a0172c/dist/BlueMarble-For-GreasyFork.user.css
// @resource CSS-BM-File https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/2cd51bf91944ae2acb253ea5bbd76f79b7a2edd3/dist/BlueMarble-For-GreasyFork.user.css
// @antifeature tracking Anonymous opt-in telemetry data
// @noframes
// ==/UserScript==
@ -1366,9 +1366,11 @@
* @param {string} iMoveThingsSelector - The drag handle element
* @since 0.8.2
*/
handleDrag(moveMeSelector, iMoveThingsSelector) {
handleDrag(moveMeSelector, iMoveThingsSelector, options = {}) {
const moveMe = document.querySelector(moveMeSelector);
const iMoveThings = document.querySelector(iMoveThingsSelector);
const onEnd = options?.onEnd ?? (() => {
});
if (!moveMe || !iMoveThings) {
this.handleDisplayError(`Can not drag! ${!moveMe ? "moveMe" : ""} ${!moveMe && !iMoveThings ? "and " : ""}${!iMoveThings ? "iMoveThings " : ""}was not found!`);
return;
@ -1438,6 +1440,12 @@
document.removeEventListener("mouseup", endDrag);
document.removeEventListener("touchend", endDrag);
document.removeEventListener("touchcancel", endDrag);
onEnd({
element: moveMe,
x: currentX,
y: currentY
});
initialRect = null;
};
const onMouseMove = (event) => {
if (isDragging && initialRect) {
@ -1467,6 +1475,124 @@
event.preventDefault();
}, { passive: false });
}
/** Handles resizing of an overlay window from a resize handle.
* @param {string} resizeMeSelector - The element to resize
* @param {string} iResizeThingsSelector - The resize handle element
* @param {{onEnd?: function({element: HTMLElement, width: number, height: number}): void, minWidth?: number, minHeight?: number, maxWidth?: number, maxHeight?: number}} [options={}]
* @since 0.92.0
*/
handleResize(resizeMeSelector, iResizeThingsSelector, options = {}) {
const resizeMe = document.querySelector(resizeMeSelector);
const iResizeThings = document.querySelector(iResizeThingsSelector);
const onEnd = options?.onEnd ?? (() => {
});
if (!resizeMe || !iResizeThings) {
this.handleDisplayError(`Can not resize! ${!resizeMe ? "resizeMe" : ""} ${!resizeMe && !iResizeThings ? "and " : ""}${!iResizeThings ? "iResizeThings " : ""}was not found!`);
return;
}
let isResizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
let currentWidth = 0;
let currentHeight = 0;
let targetWidth = 0;
let targetHeight = 0;
let animationFrame = null;
const getMaximumWidth = () => Number.isFinite(options?.maxWidth) ? options.maxWidth : window.innerWidth - 16;
const getMaximumHeight = () => Number.isFinite(options?.maxHeight) ? options.maxHeight : window.innerHeight - 16;
const minimumWidth = Number.isFinite(options?.minWidth) ? options.minWidth : 200;
const minimumHeight = Number.isFinite(options?.minHeight) ? options.minHeight : 160;
const clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
const updateSize = () => {
if (isResizing) {
const deltaWidth = Math.abs(currentWidth - targetWidth);
const deltaHeight = Math.abs(currentHeight - targetHeight);
if (deltaWidth > 0.5 || deltaHeight > 0.5) {
currentWidth = targetWidth;
currentHeight = targetHeight;
resizeMe.style.width = `${currentWidth}px`;
resizeMe.style.height = `${currentHeight}px`;
}
animationFrame = requestAnimationFrame(updateSize);
}
};
const startResize = (clientX, clientY) => {
isResizing = true;
startX = clientX;
startY = clientY;
startWidth = resizeMe.offsetWidth;
startHeight = resizeMe.offsetHeight;
currentWidth = startWidth;
currentHeight = startHeight;
targetWidth = startWidth;
targetHeight = startHeight;
document.body.style.userSelect = "none";
iResizeThings.classList.add("bm-resizing");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("touchmove", onTouchMove, { passive: false });
document.addEventListener("mouseup", endResize);
document.addEventListener("touchend", endResize);
document.addEventListener("touchcancel", endResize);
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
updateSize();
};
const endResize = () => {
isResizing = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
document.body.style.userSelect = "";
iResizeThings.classList.remove("bm-resizing");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("touchmove", onTouchMove);
document.removeEventListener("mouseup", endResize);
document.removeEventListener("touchend", endResize);
document.removeEventListener("touchcancel", endResize);
onEnd({
element: resizeMe,
width: currentWidth,
height: currentHeight
});
};
const onMouseMove = (event) => {
if (!isResizing) {
return;
}
targetWidth = clamp(startWidth + (event.clientX - startX), minimumWidth, getMaximumWidth());
targetHeight = clamp(startHeight + (event.clientY - startY), minimumHeight, getMaximumHeight());
};
const onTouchMove = (event) => {
if (!isResizing) {
return;
}
const touch = event?.touches?.[0];
if (!touch) {
return;
}
targetWidth = clamp(startWidth + (touch.clientX - startX), minimumWidth, getMaximumWidth());
targetHeight = clamp(startHeight + (touch.clientY - startY), minimumHeight, getMaximumHeight());
event.preventDefault();
};
iResizeThings.addEventListener("mousedown", (event) => {
event.preventDefault();
event.stopPropagation();
startResize(event.clientX, event.clientY);
});
iResizeThings.addEventListener("touchstart", (event) => {
const touch = event?.touches?.[0];
if (!touch) {
return;
}
event.preventDefault();
event.stopPropagation();
startResize(touch.clientX, touch.clientY);
}, { passive: false });
}
/** Handles status display.
* This will output plain text into the output Status box.
* Additionally, this will output an info message to the console.
@ -1637,15 +1763,28 @@
* @since 0.91.39
*/
async updateUserStorage() {
await this.saveUserStorage();
}
/** Saves the user settings in userscript storage.
* @param {boolean} [force=false] - Should the throttle be ignored?
* @since 0.92.0
*/
async saveUserStorage(force = false) {
const userSettingsCurrent = JSON.stringify(this.userSettings);
const userSettingsOld = JSON.stringify(this.userSettingsOld);
if (userSettingsCurrent != userSettingsOld && Date.now() - this.lastUpdateTime > this.updateFrequency) {
if (userSettingsCurrent != userSettingsOld && (force || Date.now() - this.lastUpdateTime > this.updateFrequency)) {
await GM.setValue(this.userSettingsSaveLocation, userSettingsCurrent);
this.userSettingsOld = structuredClone(this.userSettings);
this.lastUpdateTime = Date.now();
console.log(userSettingsCurrent);
}
}
/** Immediately saves the user settings in userscript storage.
* @since 0.92.0
*/
async saveUserStorageNow() {
await this.saveUserStorage(true);
}
/** Toggles a boolean flag to the state that was passed in.
* If no state was passed in, the flag will flip to the opposite state.
* The existence of the flag determines its state. If it exists, it is `true`.
@ -2219,7 +2358,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
// src/WindowFilter.js
var _WindowFilter_instances, buildColorList_fn, sortColorList_fn, selectColorList_fn, calculatePixelStatistics_fn;
var _WindowFilter_instances, getWindowState_fn, setWindowModePreference_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 = class extends Overlay {
/** Constructor for the color filter window
* @param {*} executor - The executing class
@ -2233,6 +2372,16 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.windowID = "bm-window-filter";
this.colorListID = "bm-filter-flex";
this.windowParent = document.body;
this.settingsManager = executor.settingsManager ?? null;
this.windowModeFlag = "ftr-oWin";
this.windowStateKey = "windowFilter";
this.windowResizeObserver = null;
this.windowViewportResizeHandler = null;
this.windowSaveTimeout = null;
this.windowMinWidth = 260;
this.windowMinHeight = 220;
this.windowMaxWidth = 1e3;
this.windowMaxHeight = 1400;
this.templateManager = executor.apiManager?.templateManager;
this.eyeOpen = '<svg viewBox="0 .5 6 3"><path d="M0,2Q3-1 6,2Q3,5 0,2H2A1,1 0 1 0 3,1Q3,2 2,2"/></svg>';
this.eyeClosed = '<svg viewBox="0 1 12 6"><mask id="a"><path d="M0,0H12V8L0,2" fill="#fff"/></mask><path d="M0,4Q6-2 12,4Q6,10 0,4H4A2,2 0 1 0 6,2Q6,4 4,4ZM1,2L10,6.5L9.5,7L.5,2.5" mask="url(#a)"/></svg>';
@ -2250,6 +2399,16 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
this.sortSecondary = "ascending";
this.showUnused = false;
}
/** Builds the preferred filter window mode for the user.
* @since 0.92.0
*/
buildPreferredWindow() {
if (this.settingsManager?.userSettings?.flags?.includes(this.windowModeFlag)) {
this.buildWindowed();
return;
}
this.buildWindow();
}
/** Spawns a Color Filter window.
* If another color filter window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
@ -2257,7 +2416,7 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
*/
buildWindow() {
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
__privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window" }, (instance, div) => {
@ -2268,16 +2427,15 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
}).buildElement().addDiv().buildElement().addDiv({ "class": "bm-flex-center" }).addButton({ "class": "bm-button-circle", "textContent": "\u{1F5D7}", "aria-label": 'Switch to windowed mode for "Color Filter"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
__privateMethod(this, _WindowFilter_instances, setWindowModePreference_fn).call(this, true);
__privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
this.buildWindowed();
};
button.ontouchend = () => {
button.click();
};
}).buildElement().addButton({ "class": "bm-button-circle", "textContent": "\u2716", "aria-label": 'Close window "Color Filter"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
};
button.onclick = () => __privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
button.ontouchend = () => {
button.click();
};
@ -2320,10 +2478,14 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
*/
buildWindowed() {
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
__privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
return;
}
this.window = this.addDiv({ "id": this.windowID, "class": "bm-window bm-windowed" }).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Color Filter"', "data-button-status": "expanded" }, (instance, button) => {
this.window = this.addDiv({
"id": this.windowID,
"class": "bm-window bm-windowed",
"style": `width: 300px; height: min(70vh, 32rem); min-width: ${this.windowMinWidth}px; min-height: ${this.windowMinHeight}px; max-width: min(${this.windowMaxWidth}px, calc(100vw - 16px)); max-height: min(${this.windowMaxHeight}px, calc(100vh - 16px));`
}).addDragbar().addButton({ "class": "bm-button-circle", "textContent": "\u25BC", "aria-label": 'Minimize window "Color Filter"', "data-button-status": "expanded" }, (instance, button) => {
button.onclick = () => {
const windowedColorTotals = document.querySelector("#bm-filter-windowed-color-totals");
if (windowedColorTotals) {
@ -2336,16 +2498,15 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
}).buildElement().addDiv().addSpan({ "id": "bm-filter-windowed-color-totals", "class": "bm-dragbar-text", "style": "font-weight: 700;" }).buildElement().buildElement().addDiv({ "class": "bm-flex-center" }).addButton({ "class": "bm-button-circle", "textContent": "\u{1F5D6}", "aria-label": 'Switch to fullscreen mode for "Color Filter"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
__privateMethod(this, _WindowFilter_instances, setWindowModePreference_fn).call(this, false);
__privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
this.buildWindow();
};
button.ontouchend = () => {
button.click();
};
}).buildElement().addButton({ "class": "bm-button-circle", "textContent": "\u2716", "aria-label": 'Close window "Color Filter"' }, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
};
button.onclick = () => __privateMethod(this, _WindowFilter_instances, closeWindow_fn).call(this);
button.ontouchend = () => {
button.click();
};
@ -2359,8 +2520,15 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
};
}).buildElement().addButton({ "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().buildElement().buildOverlay(this.windowParent);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
}).buildElement().buildElement().addDiv({ "class": "bm-container bm-scrollable" }).buildElement().buildElement().addDiv({
"class": "bm-resize-corner",
"title": "Resize Color Filter window",
"aria-label": "Resize Color Filter window",
"role": "presentation",
"textContent": "\u25E2",
"style": "position: absolute; right: 0; bottom: 0; width: 28px; height: 28px; display: flex; 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; touch-action: none; user-select: none; font-size: 8px; line-height: 1; color: rgba(255,255,255,0.95); background: transparent; border: none; box-shadow: none;"
}).buildElement().buildElement().buildOverlay(this.windowParent);
__privateMethod(this, _WindowFilter_instances, initializeWindowedPersistence_fn).call(this);
const scrollableContainer = document.querySelector(`#${this.windowID} .bm-container.bm-scrollable`);
__privateMethod(this, _WindowFilter_instances, buildColorList_fn).call(this, scrollableContainer);
__privateMethod(this, _WindowFilter_instances, sortColorList_fn).call(this, this.sortPrimary, this.sortSecondary, this.showUnused);
@ -2445,6 +2613,196 @@ Getting Y ${pixelY}-${pixelY + drawSizeY}`);
}
};
_WindowFilter_instances = new WeakSet();
/** Retrieves the persisted window state object.
* @returns {Object | null}
* @since 0.92.0
*/
getWindowState_fn = function() {
var _a, _b;
if (!this.settingsManager) {
return null;
}
(_a = this.settingsManager.userSettings)[_b = this.windowStateKey] ?? (_a[_b] = {});
return this.settingsManager.userSettings[this.windowStateKey];
};
/** Updates the preferred window mode setting.
* @param {boolean} shouldBeWindowed
* @since 0.92.0
*/
setWindowModePreference_fn = function(shouldBeWindowed) {
if (!this.settingsManager) {
return;
}
this.settingsManager.toggleFlag(this.windowModeFlag, shouldBeWindowed);
void this.settingsManager.saveUserStorageNow();
};
/** Immediately closes the filter window and cleans up persistence observers.
* @since 0.92.0
*/
closeWindow_fn = function() {
const windowElement = document.querySelector(`#${this.windowID}`);
if (windowElement?.classList.contains("bm-windowed")) {
__privateMethod(this, _WindowFilter_instances, saveWindowState_fn).call(this, windowElement);
}
__privateMethod(this, _WindowFilter_instances, cleanupWindowPersistence_fn).call(this);
windowElement?.remove();
};
/** Disconnects live observers used for window persistence.
* @since 0.92.0
*/
cleanupWindowPersistence_fn = function() {
if (this.windowResizeObserver) {
this.windowResizeObserver.disconnect();
this.windowResizeObserver = null;
}
if (this.windowViewportResizeHandler) {
window.removeEventListener("resize", this.windowViewportResizeHandler);
this.windowViewportResizeHandler = null;
}
if (this.windowSaveTimeout) {
clearTimeout(this.windowSaveTimeout);
this.windowSaveTimeout = null;
}
};
/** Returns a clamped dimension value for the window.
* @param {number} size - The size in pixels
* @param {number} minimum - Minimum allowed size
* @param {number} maximum - Maximum allowed size
* @returns {number}
* @since 0.92.0
*/
clampWindowDimension_fn = function(size, minimum, maximum) {
const resolvedMaximum = Math.max(minimum, maximum);
return Math.min(Math.max(Math.round(Number(size) || minimum), minimum), resolvedMaximum);
};
/** Returns a viewport-safe position for the window.
* @param {HTMLElement} windowElement
* @param {number} x
* @param {number} y
* @returns {{x: number, y: number}}
* @since 0.92.0
*/
clampWindowPosition_fn = function(windowElement, x, y) {
const margin = 8;
const maxX = Math.max(margin, window.innerWidth - windowElement.offsetWidth - margin);
const maxY = Math.max(margin, window.innerHeight - windowElement.offsetHeight - margin);
return {
x: Math.min(Math.max(Math.round(Number(x) || margin), margin), maxX),
y: Math.min(Math.max(Math.round(Number(y) || margin), margin), maxY)
};
};
/** Applies the persisted size and position to the windowed filter.
* @param {HTMLElement} windowElement
* @since 0.92.0
*/
restoreWindowState_fn = function(windowElement) {
const windowState = __privateMethod(this, _WindowFilter_instances, getWindowState_fn).call(this);
if (!windowState || !windowElement) {
return;
}
const width = Number(windowState.width);
const height = Number(windowState.height);
const hasWidth = Number.isFinite(width);
const hasHeight = Number.isFinite(height);
if (hasWidth) {
windowState.width = __privateMethod(this, _WindowFilter_instances, clampWindowDimension_fn).call(this, width, this.windowMinWidth, Math.min(this.windowMaxWidth, window.innerWidth - 16));
windowElement.style.width = `${windowState.width}px`;
}
if (hasHeight) {
windowState.height = __privateMethod(this, _WindowFilter_instances, clampWindowDimension_fn).call(this, height, this.windowMinHeight, Math.min(this.windowMaxHeight, window.innerHeight - 16));
windowElement.style.height = `${windowState.height}px`;
}
requestAnimationFrame(() => {
if (!windowElement.isConnected) {
return;
}
const x = Number(windowState.x);
const y = Number(windowState.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return;
}
const clampedPosition = __privateMethod(this, _WindowFilter_instances, clampWindowPosition_fn).call(this, windowElement, x, y);
windowElement.style.left = "0px";
windowElement.style.top = "0px";
windowElement.style.right = "";
windowElement.style.transform = `translate(${clampedPosition.x}px, ${clampedPosition.y}px)`;
if (clampedPosition.x != x || clampedPosition.y != y) {
windowState.x = clampedPosition.x;
windowState.y = clampedPosition.y;
void this.settingsManager?.saveUserStorageNow();
}
});
};
/** Saves the current size and position of the windowed filter.
* @param {HTMLElement} windowElement
* @since 0.92.0
*/
saveWindowState_fn = function(windowElement) {
const windowState = __privateMethod(this, _WindowFilter_instances, getWindowState_fn).call(this);
if (!windowState || !windowElement?.isConnected || !windowElement.classList.contains("bm-windowed")) {
return;
}
const rect = windowElement.getBoundingClientRect();
const width = __privateMethod(this, _WindowFilter_instances, clampWindowDimension_fn).call(this, rect.width, this.windowMinWidth, Math.min(this.windowMaxWidth, window.innerWidth - 16));
const height = __privateMethod(this, _WindowFilter_instances, clampWindowDimension_fn).call(this, rect.height, this.windowMinHeight, Math.min(this.windowMaxHeight, window.innerHeight - 16));
if (Math.round(rect.width) != width) {
windowElement.style.width = `${width}px`;
}
if (Math.round(rect.height) != height) {
windowElement.style.height = `${height}px`;
}
const clampedPosition = __privateMethod(this, _WindowFilter_instances, clampWindowPosition_fn).call(this, windowElement, rect.left, rect.top);
windowElement.style.left = "0px";
windowElement.style.top = "0px";
windowElement.style.right = "";
windowElement.style.transform = `translate(${clampedPosition.x}px, ${clampedPosition.y}px)`;
windowState.x = clampedPosition.x;
windowState.y = clampedPosition.y;
windowState.width = width;
windowState.height = height;
void this.settingsManager?.saveUserStorageNow();
};
/** Debounces persisting the current window size and position.
* @param {HTMLElement} windowElement
* @param {number} [delay=150]
* @since 0.92.0
*/
scheduleWindowStateSave_fn = function(windowElement, delay = 150) {
if (this.windowSaveTimeout) {
clearTimeout(this.windowSaveTimeout);
}
this.windowSaveTimeout = setTimeout(() => {
this.windowSaveTimeout = null;
__privateMethod(this, _WindowFilter_instances, saveWindowState_fn).call(this, windowElement);
}, delay);
};
/** Enables persistence and resize handling for the windowed filter.
* @since 0.92.0
*/
initializeWindowedPersistence_fn = function() {
const windowElement = document.querySelector(`#${this.windowID}.bm-window`);
if (!windowElement) {
return;
}
__privateMethod(this, _WindowFilter_instances, cleanupWindowPersistence_fn).call(this);
__privateMethod(this, _WindowFilter_instances, restoreWindowState_fn).call(this, windowElement);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`, {
onEnd: ({ element }) => __privateMethod(this, _WindowFilter_instances, saveWindowState_fn).call(this, element)
});
this.handleResize(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-resize-corner`, {
minWidth: this.windowMinWidth,
minHeight: this.windowMinHeight,
maxWidth: Math.min(this.windowMaxWidth, window.innerWidth - 16),
maxHeight: Math.min(this.windowMaxHeight, window.innerHeight - 16),
onEnd: ({ element }) => __privateMethod(this, _WindowFilter_instances, saveWindowState_fn).call(this, element)
});
if (typeof ResizeObserver == "function") {
this.windowResizeObserver = new ResizeObserver(() => __privateMethod(this, _WindowFilter_instances, scheduleWindowStateSave_fn).call(this, windowElement));
this.windowResizeObserver.observe(windowElement);
}
this.windowViewportResizeHandler = () => __privateMethod(this, _WindowFilter_instances, scheduleWindowStateSave_fn).call(this, windowElement, 0);
window.addEventListener("resize", this.windowViewportResizeHandler);
};
/** Creates the color list container.
* @param {HTMLElement} parentElement - Parent element to add the color list to as a child
* @since 0.88.222
@ -2977,7 +3335,7 @@ Version: ${this.version}`, "readOnly": true }).buildElement().buildElement().add
*/
buildWindowFilter_fn = function() {
const windowFilter = new WindowFilter(this);
windowFilter.buildWindow();
windowFilter.buildPreferredWindow();
};
coordinateInputPaste_fn = async function(instance, input, event) {
event.preventDefault();
@ -4014,3 +4372,5 @@ Time Since Blink: ${String(Math.floor(elapsed / 6e4)).padStart(2, "0")}:${String
observer.observe(document.body, { childList: true, subtree: true });
}
})();
// Build Hash: f5ff285a601e

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

8
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.91.116",
"version": "0.92.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.91.116",
"version": "0.92.1",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",
@ -37,6 +37,7 @@
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.28.0"
},
@ -52,6 +53,7 @@
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
@ -544,7 +546,6 @@
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"peer": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
@ -741,7 +742,6 @@
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"dev": true,
"peer": true,
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.4.0",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.92.0",
"version": "0.92.1",
"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.92.0
// @version 0.92.1
// @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

@ -1278,11 +1278,12 @@ export default class Overlay {
* @param {string} iMoveThingsSelector - The drag handle element
* @since 0.8.2
*/
handleDrag(moveMeSelector, iMoveThingsSelector) {
handleDrag(moveMeSelector, iMoveThingsSelector, options = {}) {
// Retrieves the elements
const moveMe = document.querySelector(moveMeSelector);
const iMoveThings = document.querySelector(iMoveThingsSelector);
const onEnd = options?.onEnd ?? (() => {});
// What to do when one of the two elements are not found
if (!moveMe || !iMoveThings) {
@ -1375,6 +1376,14 @@ export default class Overlay {
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('touchend', endDrag);
document.removeEventListener('touchcancel', endDrag);
onEnd({
element: moveMe,
x: currentX,
y: currentY
});
initialRect = null;
};
// Mouse move
@ -1411,6 +1420,135 @@ export default class Overlay {
}, { passive: false });
}
/** Handles resizing of an overlay window from a resize handle.
* @param {string} resizeMeSelector - The element to resize
* @param {string} iResizeThingsSelector - The resize handle element
* @param {{onEnd?: function({element: HTMLElement, width: number, height: number}): void, minWidth?: number, minHeight?: number, maxWidth?: number, maxHeight?: number}} [options={}]
* @since 0.92.0
*/
handleResize(resizeMeSelector, iResizeThingsSelector, options = {}) {
const resizeMe = document.querySelector(resizeMeSelector);
const iResizeThings = document.querySelector(iResizeThingsSelector);
const onEnd = options?.onEnd ?? (() => {});
if (!resizeMe || !iResizeThings) {
this.handleDisplayError(`Can not resize! ${!resizeMe ? 'resizeMe' : ''} ${!resizeMe && !iResizeThings ? 'and ' : ''}${!iResizeThings ? 'iResizeThings ' : ''}was not found!`);
return;
}
let isResizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
let currentWidth = 0;
let currentHeight = 0;
let targetWidth = 0;
let targetHeight = 0;
let animationFrame = null;
const getMaximumWidth = () => Number.isFinite(options?.maxWidth) ? options.maxWidth : window.innerWidth - 16;
const getMaximumHeight = () => Number.isFinite(options?.maxHeight) ? options.maxHeight : window.innerHeight - 16;
const minimumWidth = Number.isFinite(options?.minWidth) ? options.minWidth : 200;
const minimumHeight = Number.isFinite(options?.minHeight) ? options.minHeight : 160;
const clamp = (value, minimum, maximum) => Math.min(Math.max(value, minimum), Math.max(minimum, maximum));
const updateSize = () => {
if (isResizing) {
const deltaWidth = Math.abs(currentWidth - targetWidth);
const deltaHeight = Math.abs(currentHeight - targetHeight);
if (deltaWidth > 0.5 || deltaHeight > 0.5) {
currentWidth = targetWidth;
currentHeight = targetHeight;
resizeMe.style.width = `${currentWidth}px`;
resizeMe.style.height = `${currentHeight}px`;
}
animationFrame = requestAnimationFrame(updateSize);
}
};
const startResize = (clientX, clientY) => {
isResizing = true;
startX = clientX;
startY = clientY;
startWidth = resizeMe.offsetWidth;
startHeight = resizeMe.offsetHeight;
currentWidth = startWidth;
currentHeight = startHeight;
targetWidth = startWidth;
targetHeight = startHeight;
document.body.style.userSelect = 'none';
iResizeThings.classList.add('bm-resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('touchmove', onTouchMove, { passive: false });
document.addEventListener('mouseup', endResize);
document.addEventListener('touchend', endResize);
document.addEventListener('touchcancel', endResize);
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
updateSize();
};
const endResize = () => {
isResizing = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
document.body.style.userSelect = '';
iResizeThings.classList.remove('bm-resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('mouseup', endResize);
document.removeEventListener('touchend', endResize);
document.removeEventListener('touchcancel', endResize);
onEnd({
element: resizeMe,
width: currentWidth,
height: currentHeight
});
};
const onMouseMove = event => {
if (!isResizing) {return;}
targetWidth = clamp(startWidth + (event.clientX - startX), minimumWidth, getMaximumWidth());
targetHeight = clamp(startHeight + (event.clientY - startY), minimumHeight, getMaximumHeight());
};
const onTouchMove = event => {
if (!isResizing) {return;}
const touch = event?.touches?.[0];
if (!touch) {return;}
targetWidth = clamp(startWidth + (touch.clientX - startX), minimumWidth, getMaximumWidth());
targetHeight = clamp(startHeight + (touch.clientY - startY), minimumHeight, getMaximumHeight());
event.preventDefault();
};
iResizeThings.addEventListener('mousedown', event => {
event.preventDefault();
event.stopPropagation();
startResize(event.clientX, event.clientY);
});
iResizeThings.addEventListener('touchstart', event => {
const touch = event?.touches?.[0];
if (!touch) {return;}
event.preventDefault();
event.stopPropagation();
startResize(touch.clientX, touch.clientY);
}, { passive: false });
}
/** Handles status display.
* This will output plain text into the output Status box.
* Additionally, this will output an info message to the console.
@ -1434,4 +1572,4 @@ export default class Overlay {
consoleError(`${this.name}: ${text}`); // Outputs something like "ScriptName: text" as an error message to the console
this.updateInnerHTML(this.outputStatusId, 'Error: ' + text, true); // Update output Status box
}
}
}

View file

@ -98,33 +98,117 @@
/* WINDOWED MODE */
/* Resizable filter window in windowed mode */
#bm-window-filter.bm-windowed {
--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;
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;
}
/* 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-row: 2;
min-height: 0;
min-width: 0;
overflow: hidden;
}
/* Filter flex in windowed mode */
#bm-window-filter.bm-windowed #bm-filter-flex {
flex-direction: column;
align-items: stretch;
gap: 0.25em;
width: 100%;
align-self: stretch;
min-width: 0;
box-sizing: border-box;
}
/* Filter color in windowed mode */
#bm-window-filter.bm-windowed .bm-filter-color {
width: auto;
width: 100%;
max-width: none;
align-self: stretch;
flex: 1 1 auto;
min-width: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#bm-window-filter.bm-windowed .bm-filter-color > .bm-flex-between {
width: 100%;
min-width: 0;
flex: 1 1 auto;
}
/* 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;
min-height: 0;
min-width: 0;
height: 100%;
width: 100%;
max-height: 100% !important;
overflow: auto;
box-sizing: border-box;
}
/* 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;
z-index: 5;
cursor: nwse-resize;
pointer-events: auto;
opacity: 1;
touch-action: none;
user-select: none;
background: transparent;
border: none;
box-shadow: none;
}
#bm-window-filter.bm-windowed .bm-resize-corner:hover,
#bm-window-filter.bm-windowed .bm-resize-corner.bm-resizing {
opacity: 1;
}
/* Filter window container for RGB color display in windowed mode */
#bm-window-filter.bm-windowed .bm-filter-container-rgb {
display: flex;
width: 100%;
min-width: 0;
flex: 1 1 auto;
gap: 0.5ch;
align-items: center;
padding: 0.1em 0.5ch;
border: none;
border-radius: 1em;
box-sizing: border-box;
}
/* Filter window hide color button */
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
padding: 0.5em 0.25ch;
flex: 0 0 auto;
}
/* Filter window hide color button SVG in windowed mode */
@ -135,9 +219,13 @@
/* Filter window header 2 in windowed mode */
#bm-window-filter.bm-windowed .bm-filter-color h2 {
font-size: 0.75em;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Filter window dragbar text area in windowed mode */
#bm-window-filter #bm-filter-windowed-color-totals {
font-size: 1em;
}
}

View file

@ -21,6 +21,16 @@ export default class WindowFilter extends Overlay {
this.windowID = 'bm-window-filter'; // The ID attribute for this window
this.colorListID = 'bm-filter-flex'; // The ID attribute for the color list
this.windowParent = document.body; // The parent of the window DOM tree
this.settingsManager = executor.settingsManager ?? null; // Settings manager from the executor
this.windowModeFlag = 'ftr-oWin'; // User setting flag for opening the filter in windowed mode
this.windowStateKey = 'windowFilter'; // User setting key for the persisted window state
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.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
this.windowMaxHeight = 1400; // Maximum height for the windowed filter
/** The templateManager instance currently being used. @type {TemplateManager} */
this.templateManager = executor.apiManager?.templateManager;
@ -51,6 +61,17 @@ export default class WindowFilter extends Overlay {
this.showUnused = false; // Were unused colors shown the last time the user sorted the color list?
}
/** Builds the preferred filter window mode for the user.
* @since 0.92.0
*/
buildPreferredWindow() {
if (this.settingsManager?.userSettings?.flags?.includes(this.windowModeFlag)) {
this.buildWindowed();
return;
}
this.buildWindow();
}
/** Spawns a Color Filter window.
* If another color filter window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
@ -60,7 +81,7 @@ export default class WindowFilter extends Overlay {
// If a color filter wizard window already exists, close it
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
this.#closeWindow();
return;
}
@ -79,13 +100,14 @@ export default class WindowFilter extends Overlay {
.addDiv({'class': 'bm-flex-center'})
.addButton({'class': 'bm-button-circle', 'textContent': '🗗', 'aria-label': 'Switch to windowed mode for "Color Filter"'}, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
this.#setWindowModePreference(true);
this.#closeWindow();
this.buildWindowed();
};
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.addButton({'class': 'bm-button-circle', 'textContent': '✖', 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
button.onclick = () => this.#closeWindow();
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.buildElement()
@ -202,12 +224,16 @@ export default class WindowFilter extends Overlay {
// If a color filter wizard window already exists, close it
if (document.querySelector(`#${this.windowID}`)) {
document.querySelector(`#${this.windowID}`).remove();
this.#closeWindow();
return;
}
// Creates a new windowed color filter window
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window bm-windowed'})
this.window = this.addDiv({
'id': this.windowID,
'class': 'bm-window bm-windowed',
'style': `width: 300px; height: min(70vh, 32rem); min-width: ${this.windowMinWidth}px; min-height: ${this.windowMinHeight}px; max-width: min(${this.windowMaxWidth}px, calc(100vw - 16px)); max-height: min(${this.windowMaxHeight}px, calc(100vh - 16px));`
})
.addDragbar()
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Color Filter"', 'data-button-status': 'expanded'}, (instance, button) => {
button.onclick = () => {
@ -226,13 +252,14 @@ export default class WindowFilter extends Overlay {
.addDiv({'class': 'bm-flex-center'})
.addButton({'class': 'bm-button-circle', 'textContent': '🗖', 'aria-label': 'Switch to fullscreen mode for "Color Filter"'}, (instance, button) => {
button.onclick = () => {
document.querySelector(`#${this.windowID}`)?.remove();
this.#setWindowModePreference(false);
this.#closeWindow();
this.buildWindow();
};
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.addButton({'class': 'bm-button-circle', 'textContent': '✖', 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
button.onclick = () => this.#closeWindow();
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.buildElement()
@ -261,10 +288,17 @@ export default class WindowFilter extends Overlay {
// Color list will appear here
.buildElement()
.buildElement()
.addDiv({
'class': 'bm-resize-corner',
'title': 'Resize Color Filter window',
'aria-label': 'Resize Color Filter window',
'role': 'presentation',
'textContent': '◢',
'style': 'position: absolute; right: 0; bottom: 0; width: 28px; height: 28px; display: flex; 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; touch-action: none; user-select: none; font-size: 8px; line-height: 1; color: rgba(255,255,255,0.95); background: transparent; border: none; box-shadow: none;'
}).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`);
this.#initializeWindowedPersistence();
// Obtains the scrollable container to put the color filter in
const scrollableContainer = document.querySelector(`#${this.windowID} .bm-container.bm-scrollable`);
@ -274,6 +308,206 @@ export default class WindowFilter extends Overlay {
this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
}
/** Retrieves the persisted window state object.
* @returns {Object | null}
* @since 0.92.0
*/
#getWindowState() {
if (!this.settingsManager) {return null;}
this.settingsManager.userSettings[this.windowStateKey] ??= {};
return this.settingsManager.userSettings[this.windowStateKey];
}
/** Updates the preferred window mode setting.
* @param {boolean} shouldBeWindowed
* @since 0.92.0
*/
#setWindowModePreference(shouldBeWindowed) {
if (!this.settingsManager) {return;}
this.settingsManager.toggleFlag(this.windowModeFlag, shouldBeWindowed);
void this.settingsManager.saveUserStorageNow();
}
/** Immediately closes the filter window and cleans up persistence observers.
* @since 0.92.0
*/
#closeWindow() {
const windowElement = document.querySelector(`#${this.windowID}`);
if (windowElement?.classList.contains('bm-windowed')) {
this.#saveWindowState(windowElement);
}
this.#cleanupWindowPersistence();
windowElement?.remove();
}
/** Disconnects live observers used for window persistence.
* @since 0.92.0
*/
#cleanupWindowPersistence() {
if (this.windowResizeObserver) {
this.windowResizeObserver.disconnect();
this.windowResizeObserver = null;
}
if (this.windowViewportResizeHandler) {
window.removeEventListener('resize', this.windowViewportResizeHandler);
this.windowViewportResizeHandler = null;
}
if (this.windowSaveTimeout) {
clearTimeout(this.windowSaveTimeout);
this.windowSaveTimeout = null;
}
}
/** Returns a clamped dimension value for the window.
* @param {number} size - The size in pixels
* @param {number} minimum - Minimum allowed size
* @param {number} maximum - Maximum allowed size
* @returns {number}
* @since 0.92.0
*/
#clampWindowDimension(size, minimum, maximum) {
const resolvedMaximum = Math.max(minimum, maximum);
return Math.min(Math.max(Math.round(Number(size) || minimum), minimum), resolvedMaximum);
}
/** Returns a viewport-safe position for the window.
* @param {HTMLElement} windowElement
* @param {number} x
* @param {number} y
* @returns {{x: number, y: number}}
* @since 0.92.0
*/
#clampWindowPosition(windowElement, x, y) {
const margin = 8;
const maxX = Math.max(margin, window.innerWidth - windowElement.offsetWidth - margin);
const maxY = Math.max(margin, window.innerHeight - windowElement.offsetHeight - margin);
return {
x: Math.min(Math.max(Math.round(Number(x) || margin), margin), maxX),
y: Math.min(Math.max(Math.round(Number(y) || margin), margin), maxY)
};
}
/** Applies the persisted size and position to the windowed filter.
* @param {HTMLElement} windowElement
* @since 0.92.0
*/
#restoreWindowState(windowElement) {
const windowState = this.#getWindowState();
if (!windowState || !windowElement) {return;}
const width = Number(windowState.width);
const height = Number(windowState.height);
const hasWidth = Number.isFinite(width);
const hasHeight = Number.isFinite(height);
if (hasWidth) {
windowState.width = this.#clampWindowDimension(width, this.windowMinWidth, Math.min(this.windowMaxWidth, window.innerWidth - 16));
windowElement.style.width = `${windowState.width}px`;
}
if (hasHeight) {
windowState.height = this.#clampWindowDimension(height, this.windowMinHeight, Math.min(this.windowMaxHeight, window.innerHeight - 16));
windowElement.style.height = `${windowState.height}px`;
}
requestAnimationFrame(() => {
if (!windowElement.isConnected) {return;}
const x = Number(windowState.x);
const y = Number(windowState.y);
if (!Number.isFinite(x) || !Number.isFinite(y)) {return;}
const clampedPosition = this.#clampWindowPosition(windowElement, x, y);
windowElement.style.left = '0px';
windowElement.style.top = '0px';
windowElement.style.right = '';
windowElement.style.transform = `translate(${clampedPosition.x}px, ${clampedPosition.y}px)`;
if ((clampedPosition.x != x) || (clampedPosition.y != y)) {
windowState.x = clampedPosition.x;
windowState.y = clampedPosition.y;
void this.settingsManager?.saveUserStorageNow();
}
});
}
/** Saves the current size and position of the windowed filter.
* @param {HTMLElement} windowElement
* @since 0.92.0
*/
#saveWindowState(windowElement) {
const windowState = this.#getWindowState();
if (!windowState || !windowElement?.isConnected || !windowElement.classList.contains('bm-windowed')) {return;}
const rect = windowElement.getBoundingClientRect();
const width = this.#clampWindowDimension(rect.width, this.windowMinWidth, Math.min(this.windowMaxWidth, window.innerWidth - 16));
const height = this.#clampWindowDimension(rect.height, this.windowMinHeight, Math.min(this.windowMaxHeight, window.innerHeight - 16));
if (Math.round(rect.width) != width) {
windowElement.style.width = `${width}px`;
}
if (Math.round(rect.height) != height) {
windowElement.style.height = `${height}px`;
}
const clampedPosition = this.#clampWindowPosition(windowElement, rect.left, rect.top);
windowElement.style.left = '0px';
windowElement.style.top = '0px';
windowElement.style.right = '';
windowElement.style.transform = `translate(${clampedPosition.x}px, ${clampedPosition.y}px)`;
windowState.x = clampedPosition.x;
windowState.y = clampedPosition.y;
windowState.width = width;
windowState.height = height;
void this.settingsManager?.saveUserStorageNow();
}
/** Debounces persisting the current window size and position.
* @param {HTMLElement} windowElement
* @param {number} [delay=150]
* @since 0.92.0
*/
#scheduleWindowStateSave(windowElement, delay = 150) {
if (this.windowSaveTimeout) {
clearTimeout(this.windowSaveTimeout);
}
this.windowSaveTimeout = setTimeout(() => {
this.windowSaveTimeout = null;
this.#saveWindowState(windowElement);
}, delay);
}
/** Enables persistence and resize handling for the windowed filter.
* @since 0.92.0
*/
#initializeWindowedPersistence() {
const windowElement = document.querySelector(`#${this.windowID}.bm-window`);
if (!windowElement) {return;}
this.#cleanupWindowPersistence();
this.#restoreWindowState(windowElement);
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`, {
onEnd: ({element}) => this.#saveWindowState(element)
});
this.handleResize(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-resize-corner`, {
minWidth: this.windowMinWidth,
minHeight: this.windowMinHeight,
maxWidth: Math.min(this.windowMaxWidth, window.innerWidth - 16),
maxHeight: Math.min(this.windowMaxHeight, window.innerHeight - 16),
onEnd: ({element}) => this.#saveWindowState(element)
});
if (typeof ResizeObserver == 'function') {
this.windowResizeObserver = new ResizeObserver(() => this.#scheduleWindowStateSave(windowElement));
this.windowResizeObserver.observe(windowElement);
}
this.windowViewportResizeHandler = () => this.#scheduleWindowStateSave(windowElement, 0);
window.addEventListener('resize', this.windowViewportResizeHandler);
}
/** Creates the color list container.
* @param {HTMLElement} parentElement - Parent element to add the color list to as a child
* @since 0.88.222
@ -705,4 +939,4 @@ export default class WindowFilter extends Overlay {
this.timeRemaining = new Date(((this.allPixelsTotal - this.allPixelsCorrectTotal) * 30 * 1000) + Date.now());
this.timeRemainingLocalized = localizeDate(this.timeRemaining);
}
}
}

View file

@ -214,7 +214,7 @@ export default class WindowMain extends Overlay {
*/
#buildWindowFilter() {
const windowFilter = new WindowFilter(this); // Creates a new color filter window instance
windowFilter.buildWindow();
windowFilter.buildPreferredWindow();
}
/** Handles pasting into the coordinate input boxes in the main Blue Marble window.
@ -254,4 +254,4 @@ export default class WindowMain extends Overlay {
instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
}
}
}
}

View file

@ -38,11 +38,6 @@
letter-spacing: 0.05em;
}
/* The Blue Marble windowed windows */
.bm-window.bm-windowed {
max-width: 300px;
}
/* The drag bar */
.bm-dragbar {
display: grid;
@ -281,6 +276,7 @@ input[type="file"] {
/* Window content container */
.bm-window-content {
overflow: hidden;
max-height: calc(100% - 5px);
transition: height 300ms cubic-bezier(.4, 0, .2, 1);
}
@ -312,7 +308,7 @@ input[type="file"] {
/* Scrollable container */
.bm-window .bm-container.bm-scrollable {
max-height: calc(80vh - 150px);
max-height: var(--bm-scrollable-max-height, calc(80vh - 150px));
overflow: auto;
}

View file

@ -16,7 +16,8 @@ import WindowSettings from "./WindowSettings";
* "telemetry": 1,
* "flags": ["hl-noTrans", "ftr-oWin", "te-noSkip"],
* "highlight": [[1,0,-1],[1,-1,0],[2,1,0],[1,0,1]],
* "filter": [-2,0,4,5,6,29,63]
* "filter": [-2,0,4,5,6,29,63],
* "windowFilter": {"x": 60, "y": 75, "width": 300, "height": 420}
* }
*/
export default class SettingsManager extends WindowSettings {
@ -45,13 +46,21 @@ export default class SettingsManager extends WindowSettings {
* @since 0.91.39
*/
async updateUserStorage() {
await this.saveUserStorage();
}
/** Saves the user settings in userscript storage.
* @param {boolean} [force=false] - Should the throttle be ignored?
* @since 0.92.0
*/
async saveUserStorage(force = false) {
// Turns the objects into a string
const userSettingsCurrent = JSON.stringify(this.userSettings);
const userSettingsOld = JSON.stringify(this.userSettingsOld);
// If the user settings have changed, AND the last update to user storage was over 5 seconds ago (5sec throttle)...
if ((userSettingsCurrent != userSettingsOld) && ((Date.now() - this.lastUpdateTime) > this.updateFrequency)) {
if ((userSettingsCurrent != userSettingsOld) && (force || ((Date.now() - this.lastUpdateTime) > this.updateFrequency))) {
await GM.setValue(this.userSettingsSaveLocation, userSettingsCurrent); // Updates user storage
this.userSettingsOld = structuredClone(this.userSettings); // Updates the old user settings with a duplicate of the current user settings
this.lastUpdateTime = Date.now(); // Updates the variable that contains the last time updated
@ -59,6 +68,13 @@ export default class SettingsManager extends WindowSettings {
}
}
/** Immediately saves the user settings in userscript storage.
* @since 0.92.0
*/
async saveUserStorageNow() {
await this.saveUserStorage(true);
}
/** Toggles a boolean flag to the state that was passed in.
* If no state was passed in, the flag will flip to the opposite state.
* The existence of the flag determines its state. If it exists, it is `true`.
@ -328,4 +344,4 @@ export default class SettingsManager extends WindowSettings {
.buildElement()
.buildElement()
}
}
}