45
README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Blue Marble Enhanced
|
||||
|
||||

|
||||
|
||||
This fork is based on [SwingTheVine/Wplace-BlueMarble](https://github.com/SwingTheVine/Wplace-BlueMarble) and focuses on practical improvements for everyday template work on [wplace.live](https://wplace.live/).
|
||||
|
||||
The goal is not to replace the upstream project. This fork keeps the original Blue Marble workflow, then adds usability, workflow, and Color Filter improvements on top.
|
||||
|
||||
## What Is Different
|
||||
|
||||
Version `0.94.0` introduces the first enhanced release:
|
||||
|
||||
- Redesigned Blue Marble windows with a minimal liquid-glass visual style.
|
||||
- Redesigned window controls, buttons, typography, spacing, and transitions.
|
||||
- Added a resizable windowed mode for Color Filter.
|
||||
- Added Color Filter position and size persistence.
|
||||
- Added persistence for shown and hidden colors in Color Filter.
|
||||
- Added automatic Color Filter refresh every 10 seconds.
|
||||
- Updated Color Filter visibility icons to match the new interface style.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the latest userscript from the release page:
|
||||
|
||||
[Download the latest release](https://github.com/alexeygasenko/Wplace-BlueMarble/releases/latest)
|
||||
|
||||
Use `BlueMarble.user.js` with a userscript manager such as Tampermonkey, then refresh [wplace.live](https://wplace.live/).
|
||||
|
||||
## Color Filter
|
||||
|
||||
Color Filter is one of the main areas improved here. It can be opened as a compact window, resized, moved around the canvas, and restored with the same size and position the next time you use it.
|
||||
|
||||
Hidden and visible colors are remembered, so you can isolate the colors you are actively painting without rebuilding the filter state every session. The list also refreshes automatically every 10 seconds, keeping pixel counts current without a manual refresh button.
|
||||
|
||||
## Upstream
|
||||
|
||||
Original project:
|
||||
|
||||
[SwingTheVine/Wplace-BlueMarble](https://github.com/SwingTheVine/Wplace-BlueMarble)
|
||||
|
||||
This fork keeps the original license and credits. For upstream documentation, contribution rules, and project background, refer to the original repository.
|
||||
|
||||
## License
|
||||
|
||||
Blue Marble is licensed under the Mozilla Public License 2.0. See [LICENSE.txt](./LICENSE.txt).
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
1358
dist/BlueMarble-For-GreasyFork.user.css
vendored
1213
dist/BlueMarble-For-GreasyFork.user.js
vendored
6
dist/BlueMarble-Standalone.user.js
vendored
4
dist/BlueMarble.user.css
vendored
6
dist/BlueMarble.user.js
vendored
303
docs/README.md
|
|
@ -1,292 +1,45 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td><a href="#blue-marble">Blue Marble</a></td>
|
||||
<td valign="top" rowspan="99"><a href="https://discord.gg/tpeBPy46hf"><img alt="Discord Banner" src="https://discord.com/api/guilds/796124137042608188/widget.png?style=banner4"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <a href="#quick-guide">Quick Guide</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <a href="#overview">Overview</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#installation-instructions">Installation Instructions</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#script-settings">Script Settings</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#template-settings">Template Settings</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <a href="#how-versioning-works">How Versioning Works</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <a href="#licenses">Licenses</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> <a href="#faq">FAQ</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#is-blue-marble-malware">Is Blue Marble malware?</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#why-are-some-pixels-not-showing-on-the-overlay">Why are some pixels not showing on the overlay?</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#how-can-blue-marble-place-pixels-for-me">How can Blue Marble place pixels for me?</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#how-do-i-hide-the-overlay">How do I hide the overlay?</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>  <a href="#why-do-game-notifications-appear-on-top-of-the-overlay">Why do game notifications appear on top of the overlay?</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
# Blue Marble Enhanced
|
||||
|
||||
<h1>Blue Marble</h1>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Latest Version" src="https://img.shields.io/badge/Latest_Version-0.92.0-lightblue?style=flat"></a>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/releases" target="_blank" rel="noopener noreferrer"><img alt="Latest Release" src="https://img.shields.io/github/v/release/SwingTheVine/Wplace-BlueMarble?sort=semver&style=flat&label=Latest%20Release&color=blue"></a>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/LICENSE.txt" target="_blank" rel="noopener noreferrer"><img alt="Software License: MPL-2.0" src="https://img.shields.io/badge/Software_License-MPL--2.0-slateblue?style=flat"></a>
|
||||
<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-1231-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-7540-blue?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-5918-blue?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-72.57%25-blue"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Repo Size" src="https://img.shields.io/github/repo-size/SwingTheVine/Wplace-BlueMarble"></a>
|
||||
<a href="https://hits.sh/github.com/SwingTheVine/Wplace-BlueMarble/" target="_blank" rel="noopener"><img alt="Views" src="https://hits.sh/github.com/SwingTheVine/Wplace-BlueMarble.svg?label=Views&extraCount=664359&color=ffffff"/></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Downloads" src="https://img.shields.io/github/downloads/SwingTheVine/Wplace-BlueMarble/total.svg"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Build" src="https://github.com/SwingTheVine/Wplace-BlueMarble/actions/workflows/build.yml/badge.svg"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Pages" src="https://github.com/SwingTheVine/Wplace-BlueMarble/actions/workflows/pages/pages-build-deployment/badge.svg?branch=wiki"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="CodeQL" src="https://github.com/SwingTheVine/Wplace-BlueMarble/actions/workflows/github-code-scanning/codeql/badge.svg"></a>
|
||||
<a href="https://www.bestpractices.dev/projects/11067" target="_blank" rel="noopener noreferrer"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/11067/badge"></a>
|
||||

|
||||
|
||||
<h2>Quick Guide</h2>
|
||||
<p>
|
||||
Press the arrows to reveal the option you want.
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to download Blue Marble.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="#installation-instructions">Click here</a> to view the installation instructions.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to ask questions about Blue Marble.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer">Click here</a> for the Discord server invite to the Blue Marble support server.
|
||||
<br>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/discussions/categories/q-a">Click here</a> for the GitHub help & question page for Blue Marble.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to report a bug.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/issues/new/choose">Click here</a> to report a bug, then choose the "Bug Report" option.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to suggest a feature.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/issues/new/choose">Click here</a> to suggest a feature, then choose the Feature Request" option.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to contribute.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/CONTRIBUTING.md">Click here</a> to read the contributing guidelines.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to report a vulnerability.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://github.com/SwingTheVine/Wplace-BlueMarble/security">Click here</a> to submit a vulnerability report.
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>I want to visit the website.</b> <sup>(Click to Expand)</sup>
|
||||
</summary>
|
||||
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer">Click here</a> to visit the official Blue Marble website.
|
||||
</details>
|
||||
</p>
|
||||
This fork is based on [SwingTheVine/Wplace-BlueMarble](https://github.com/SwingTheVine/Wplace-BlueMarble) and focuses on practical improvements for everyday template work on [wplace.live](https://wplace.live/).
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>
|
||||
Welcome to Blue Marble! Blue Marble is a userscript for the website <a href="https://wplace.live/" target="_blank" rel="noopener noreferrer">wplace.live</a>. The purpose of Blue Marble is to allow you to take an image, and layer it onto the canvas! That way, you can easily trace the image of your art, without having to look back and forth between multiple tabs/monitors. In addition, Blue Marble supports some neat extra features such as:
|
||||
<ul>
|
||||
<li>Displaying the number of pixels you need to level up</li>
|
||||
<li>Displaying a simple coordinate system (tile coordinats & pixel coordinates)</li>
|
||||
<li>Allowing you to move the color palette to the top of the screen when placing pixels</li>
|
||||
<li>Allowing you to use the eyedropper on the template image, provided the colors are correct</li>
|
||||
<li>Minimizing or maximizing the menu to switch between compact and full views</li>
|
||||
<li>Filtering overlay colors by toggling individual template colors or using global enable/disable buttons</li>
|
||||
<li>...and more!</li>
|
||||
</ul>
|
||||
If you like this userscript, please ⭐ the repository! For more information and updates, visit the <a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer">Blue Marble website</a>. If you wish to contribute to Blue Marble, check out the <a href="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/CONTRIBUTING.md" target="_blank" rel="noopener noreferrer">CONTRIBUTING.md</a> file in <code>docs/</code>.
|
||||
The goal is not to replace the upstream project. This fork keeps the original Blue Marble workflow, then adds usability, workflow, and Color Filter improvements on top.
|
||||
|
||||
<img alt="Showcase image of Blue Marble template" src="./assets/Showcase1.png">
|
||||
## What Is Different
|
||||
|
||||
<h3>Installation Instructions</h3>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Supported Browsers" src="https://img.shields.io/badge/Supported%20Browsers-Chrome%20%7C%20Firefox%2A%20%7C%20Safari%20%7C%20Edge%20%7C%20Brave-orange?style=flat"></a>
|
||||
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Unupported Browsers" src="https://img.shields.io/badge/Unsupported%20Browsers-Firefox%2A%20%7C%20Kiwi%20%7C%20Vivaldi-red?style=flat"></a>
|
||||
<p>
|
||||
Blue Marble has been verified to work on mobile devices. Blue Marble was designed on Chrome, but Blue Marble might work on "unsupported" browsers not listed above. Some versions/forks of Firefox work. Some versions/forks of Firefox do not work.
|
||||
<br>
|
||||
Installation instructions for Blue Marble are below. Click the arrows to expand the instructions you want to see. Blue text is a link.
|
||||
<details>
|
||||
<summary>
|
||||
<b>Install Chrome</b> <sup>(Click to expand)</sup>
|
||||
</summary>
|
||||
<a href="https://www.youtube.com/watch?v=gg5oiJcftEc" target="_blank" rel="noopener noreferrer"><img alt="Install Tutorial" src="https://img.shields.io/badge/Install_Tutorial-gray?style=flat&logo=YouTube&logoColor=white&logoSize=auto&labelColor=darkred"></a>
|
||||
<ol>
|
||||
<li>Install the <a href="https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo" target="_blank" rel="noopener noreferrer">TamperMonkey</a> extension for Chrome.
|
||||
<br>
|
||||
<img alt="Click the 'Add extension' button" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall1.png"></li>
|
||||
<li>Right-click the extension.
|
||||
<br>
|
||||
<img alt="Enter the 'Manage Extension' menu" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall2.png"></li>
|
||||
<li>Left-click "Manage Extension."</li>
|
||||
<li>Enable "Developer Mode."
|
||||
<br>
|
||||
<img alt="Enable 'Developer Mode' and 'Allow user scripts'" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall3.png"></li>
|
||||
<li>Enable "Allow user scripts."</li>
|
||||
<li><strong>One-click install:</strong> Click this link to Install Blue Marble directly: <a href="https://github.com/SwingTheVine/Wplace-BlueMarble/releases/download/pre/BlueMarble.user.js" target="_blank" rel="noopener noreferrer"><strong>Install Blue Marble</strong></a>
|
||||
<br>
|
||||
TamperMonkey will automatically detect the userscript and prompt you to Install it.</li>
|
||||
<li>Refresh the <a href="https://wplace.live/" target="_blank" rel="noopener noreferrer">wplace.live</a> webpage.</li>
|
||||
</ol>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>Install on Microsoft Edge</b> <sup>(Click to expand)</sup>
|
||||
</summary>
|
||||
<ol>
|
||||
<li>Install the <a href="https://microsoftedge.microsoft.com/addons/detail/iikmkjmpaadaobahmlepeloendndfphd" target="_blank" rel="noopener noreferrer">TamperMonkey</a> plugin for Microsoft Edge.
|
||||
<br>
|
||||
<img alt="Click the 'Get' button" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerEdgeInstall1.png"></li>
|
||||
<li>Right-click the extension.
|
||||
<br>
|
||||
<img alt="Enter the 'Manage Extension' menu" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerEdgeInstall2.png"></li>
|
||||
<li>Left-click "Manage Extension."</li>
|
||||
<li>Enable "Developer Mode."
|
||||
<br>
|
||||
<img alt="Enable 'Developer Mode'" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerEdgeInstall3.png"></li>
|
||||
<li>Download the <a href="https://github.com/SwingTheVine/Wplace-BlueMarble/releases" target="_blank" rel="noopener noreferrer">BlueMarble.user.js</a> file in the "Assets" of the latest release.</li>
|
||||
<li>Open the TamperMonkey Dashboard.
|
||||
<br>
|
||||
<img alt="Enter the TamperMonkey 'Dashboard'" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerEdgeInstall4.png"></li>
|
||||
<li>Drag the <code>BlueMarble.user.js</code> file inside the dashboard of TamperMonkey.
|
||||
<br>
|
||||
<img alt="Drag the userscript into the dashboard" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall5.png"></li>
|
||||
<li>Click the "Install" button to Install Blue Marble.
|
||||
<br>
|
||||
<img alt="Click the 'Install' button" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall6.png"></li>
|
||||
<li>Enable Blue Marble inside the TamperMonkey dashboard.
|
||||
<br>
|
||||
<img alt="Enable Blue Marble" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerChromeInstall7.png"></li>
|
||||
<li>Refresh the <a href="https://wplace.live/" target="_blank" rel="noopener noreferrer">wplace.live</a> webpage.</li>
|
||||
</ol>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<b>Install on Firefox</b> <sup>(Click to expand)</sup>
|
||||
</summary>
|
||||
<ol>
|
||||
<li>Install the <a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/" target="_blank" rel="noopener noreferrer">TamperMonkey</a> plugin for Firefox.
|
||||
<br>
|
||||
<img alt="Click the 'Add to Firefox' button" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/ComputerFirefoxInstall1.png"></li>
|
||||
<li><strong>One-click install:</strong> Click this link to Install Blue Marble directly: <a href="https://github.com/SwingTheVine/Wplace-BlueMarble/releases/download/pre/BlueMarble.user.js" target="_blank" rel="noopener noreferrer"><strong>Install Blue Marble</strong></a>
|
||||
<br>
|
||||
TamperMonkey will automatically detect the userscript and prompt you to install it.</li>
|
||||
<li>Refresh the <a href="https://wplace.live/" target="_blank" rel="noopener noreferrer">wplace.live</a> webpage.</li>
|
||||
</ol>
|
||||
</details>
|
||||
</p>
|
||||
Version `0.94.0` introduces the first enhanced release:
|
||||
|
||||
<h3>Template Instructions</h3>
|
||||
<p>
|
||||
Blue Marble will display your template as the same size. If your image is 500 pixels tall and 300 pixels wide, the template will be 500 pixels tall and 300 pixels wide. Here is the instructions to display a template image on the canvas:
|
||||
<ol>
|
||||
<li>Find the pixel of the top left corner. Fill in <code>Tl X</code>, <code>Tl Y</code>, <code>Px X</code>, and <code>Px Y</code> with the coordinates. You can use the "Pin" icon to auto-fill the coordinates after clicking the pixel.
|
||||
<br>
|
||||
<img alt="Find template coordinates" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/TemplateCoordinates1.png"></li>
|
||||
<li>Upload a PNG or WEBP image.</li>
|
||||
<li>Click the "Create" button.</li>
|
||||
<li>If your template still does not show, try clicking the "Enable" button.</li>
|
||||
</ol>
|
||||
</p>
|
||||
- Redesigned Blue Marble windows with a minimal liquid-glass visual style.
|
||||
- Redesigned window controls, buttons, typography, spacing, and transitions.
|
||||
- Added a resizable windowed mode for Color Filter.
|
||||
- Added Color Filter position and size persistence.
|
||||
- Added persistence for shown and hidden colors in Color Filter.
|
||||
- Added automatic Color Filter refresh every 10 seconds.
|
||||
- Updated Color Filter visibility icons to match the new interface style.
|
||||
|
||||
<h3>Script Settings</h3>
|
||||
<p>
|
||||
There are many settings available for the Blue Marble userscript! Through these settings, you can control how the script behaves.
|
||||
</p>
|
||||
## Installation
|
||||
|
||||
<h3>Template Settings</h3>
|
||||
<p>
|
||||
<h4>Transparent Pixels</h4>
|
||||
<p>
|
||||
Templates for Blue Marble work slightly different from normal. Since there is a "Transparent" color, and transparent pixels in templates are typically ignored, your template should have a custom color to signify "Transparent" colored pixels.
|
||||
<ul>
|
||||
<li>If you want a specific pixel to be any color, it should be transparent in your template.</li>
|
||||
<li>If you want a specific pixel to be the "Transparent" color on the Wplace palette, it should have the <code>#deface</code> hex color.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<h4>Coordinates</h4>
|
||||
<p>
|
||||
<h5>Tile Coordinates</h5>
|
||||
<p>
|
||||
The coordinate system for wplace.live is unique. Instead of all pixels having a global coordinate number (x, y), the coordinate number is relative to the tile. This means you need to know the tile number and the coordinate number to do anything. In Blue Marble, the tile coordinates and the pixel coordinates are displayed when you click on a pixel. These are the coordinates you should use for aligning a template.
|
||||
<br>
|
||||
<img alt="Where to find tile coordinates" src="https://github.com/SwingTheVine/Wplace-BlueMarble/blob/main/docs/assets/TemplateCoordinatesDisplay.png">
|
||||
</p>
|
||||
<h5>Template Coordinates</h5>
|
||||
<p>
|
||||
The template is aligned from the top left corner of the template. You can auto-fill this position using the "pin" (also called "waypoint") icon next to the coordinate input boxes.
|
||||
</p>
|
||||
</p>
|
||||
</p>
|
||||
</p>
|
||||
Install the latest userscript from the release page:
|
||||
|
||||
<h2>How Versioning Works</h2>
|
||||
<p>
|
||||
The versioning system for this userscript follows the <a href="https://semver.org/" target="_blank" rel="noopener noreferrer">Semantic Versioning rules</a>. As such, it is formatted in an <code>X.Y.Z</code> format where:
|
||||
<ul>
|
||||
<li>X is the major version. This is incremented when a non-backward compatible update is pushed. This is for new features that break previous versions of the userscript. Additionally, if wplace.live breaks the userscript, this will be incremented.</li>
|
||||
<li>Y is the minor version. This is incremented whenever I push to GitHub. This is for stable bug-fixes and new (non-breaking) features.</li>
|
||||
<li>Z is the patch version. This is incremented whenever I launch a development version of the userscript to test a patch. This is for unstable bug-fixes/features.</li>
|
||||
</ul>
|
||||
</p>
|
||||
[Download the latest release](https://github.com/alexeygasenko/Wplace-BlueMarble/releases/latest)
|
||||
|
||||
<h2>Licenses</h2>
|
||||
<p>
|
||||
(Below, all mentions of the "userscript" refer to the "Blue Marble" userscript made by SwingTheVine) <br>
|
||||
Most of this userscript is licensed under the <code>Mozilla Public License Version 2.0</code> (MPL-2.0). All software, code, and libraries in this repository are licensed under the MPL-2.0 license. However, the "Blue Marble" image in this userscript is owned by NASA and is licensed under the <code>Creative Commons 0 1.0 Universal</code> (CC0 1.0) license.
|
||||
</p>
|
||||
Use `BlueMarble.user.js` with a userscript manager such as Tampermonkey, then refresh [wplace.live](https://wplace.live/).
|
||||
|
||||
<h2>FAQ</h2>
|
||||
<p>
|
||||
<h3>Is Blue Marble malware?</h3>
|
||||
<p><b>A:</b> Blue Marble does not contain malicious code. The Blue Marble code can be found in the <code>src/</code> folder. If you worry about Blue Marble being malware, you can read the code, then bundle it yourself using the tools in <code>build/</code>.
|
||||
## Color Filter
|
||||
|
||||
<h3>Why are some pixels not showing on the overlay?</h3>
|
||||
<p><b>A:</b> This usually happens if the template image is not converted to the Wplace color palette. You should convert your template using a color converter for Wplace, or manually adjust the template image to match the Wplace color palette. Also check that no pixels are disabled in the filter settings of the Blue Marble menu.</p>
|
||||
Color Filter is one of the main areas improved here. It can be opened as a compact window, resized, moved around the canvas, and restored with the same size and position the next time you use it.
|
||||
|
||||
<h3>How can Blue Marble place pixels for me?</h3>
|
||||
<p><b>A:</b> Unfortunately, Blue Marble will not support the automatic placement of pixels without user interaction because it is not allowed by Wplace.
|
||||
Hidden and visible colors are remembered, so you can isolate the colors you are actively painting without rebuilding the filter state every session. The list also refreshes automatically every 10 seconds, keeping pixel counts current without a manual refresh button.
|
||||
|
||||
<h3>How do I hide the overlay?</h3>
|
||||
<p><b>A:</b> You can temporarily hide the overlay by clicking the "Disable" button in the Blue Marble menu.
|
||||
<br>
|
||||
If you want to completely remove both the overlay and the Blue Marble menu, turn off the userscript in Tampermonkey and refresh the page.</p>
|
||||
## Upstream
|
||||
|
||||
<h3>How do I tell colors apart?</h3>
|
||||
<p><b>A:</b> Find the color in the color filter list. Click the checkbox to turn the color on or off. If you want to work on only one color at a time (recommended), then click "Disable All" in the color filter. Finally, enable the checkbox next to the color you want to place. This way, only one color on your template will appear at a time.</p>
|
||||
Original project:
|
||||
|
||||
<h3>How do get the color of a pixel?</h3>
|
||||
<p><b>A:</b> Use the eyedropper in the palette menu of wplace. If your template colors match the wplace palette, you can select the template pixel dot to get the template's color for that pixel.</p>
|
||||
[SwingTheVine/Wplace-BlueMarble](https://github.com/SwingTheVine/Wplace-BlueMarble)
|
||||
|
||||
<h3>Why do game notifications appear on top of the overlay?</h3>
|
||||
<p><b>A:</b> Game notifications only appear when they need immediate attention. Therefore, they have priority over the overlay (which typically needs no attention).</p>
|
||||
</p>
|
||||
This fork keeps the original license and credits. For upstream documentation, contribution rules, and project background, refer to the original repository.
|
||||
|
||||
## License
|
||||
|
||||
Blue Marble is licensed under the Mozilla Public License 2.0. See [LICENSE.txt](../LICENSE.txt).
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 63 KiB |
BIN
docs/assets/blue-marble.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
8
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.91.116",
|
||||
"version": "0.94.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.91.116",
|
||||
"version": "0.94.0",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wplace-bluemarble",
|
||||
"version": "0.92.0",
|
||||
"version": "0.94.0",
|
||||
"type": "module",
|
||||
"homepage": "https://bluemarble.lol/",
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// @name Blue Marble
|
||||
// @name:en Blue Marble
|
||||
// @namespace https://github.com/SwingTheVine/
|
||||
// @version 0.92.0
|
||||
// @version 0.94.0
|
||||
// @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
|
||||
|
|
|
|||
231
src/Overlay.js
|
|
@ -1,3 +1,6 @@
|
|||
export const minimizeIconExpanded = '<svg class="bm-button-icon bm-button-icon-minimize" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 9.5l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
export const minimizeIconCollapsed = '<svg class="bm-button-icon bm-button-icon-minimize" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9.5 7l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
/** The overlay builder for the Blue Marble script.
|
||||
* @description This class handles the overlay UI for the Blue Marble script.
|
||||
* @class Overlay
|
||||
|
|
@ -1217,8 +1220,50 @@ export default class Overlay {
|
|||
|
||||
const window = button.closest('.bm-window'); // Get the window
|
||||
const dragbar = button.closest('.bm-dragbar'); // Get the dragbar
|
||||
const header = window.querySelector('h1'); // Get the header
|
||||
const windowContent = window.querySelector('.bm-window-content'); // Get the window content container
|
||||
const header = window?.querySelector('h1'); // Get the header
|
||||
const windowContent = window?.querySelector('.bm-window-content'); // Get the window content container
|
||||
|
||||
if (!window || !dragbar || !windowContent) {
|
||||
button.disabled = false;
|
||||
button.style.textDecoration = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const finishMinimizeTransition = (callback) => {
|
||||
let isFinished = false;
|
||||
let fallbackTimer;
|
||||
|
||||
const finish = () => {
|
||||
if (isFinished) {return;}
|
||||
isFinished = true;
|
||||
clearTimeout(fallbackTimer);
|
||||
windowContent.removeEventListener('transitionend', handler);
|
||||
callback();
|
||||
button.disabled = false;
|
||||
button.style.textDecoration = '';
|
||||
};
|
||||
|
||||
const handler = event => {
|
||||
if (event.target != windowContent || event.propertyName != 'height') {return;}
|
||||
finish();
|
||||
};
|
||||
|
||||
windowContent.addEventListener('transitionend', handler);
|
||||
fallbackTimer = setTimeout(finish, 360);
|
||||
};
|
||||
|
||||
const getCollapsedHeight = () => {
|
||||
const windowStyle = getComputedStyle(window);
|
||||
const toPixels = value => parseFloat(value) || 0;
|
||||
const extraHeight = windowStyle.boxSizing == 'border-box'
|
||||
? toPixels(windowStyle.paddingTop) +
|
||||
toPixels(windowStyle.paddingBottom) +
|
||||
toPixels(windowStyle.borderTopWidth) +
|
||||
toPixels(windowStyle.borderBottomWidth)
|
||||
: 0;
|
||||
|
||||
return Math.ceil(dragbar.getBoundingClientRect().height + extraHeight + 2);
|
||||
};
|
||||
|
||||
window.parentElement.append(window); // Moves the window to the top
|
||||
|
||||
|
|
@ -1227,22 +1272,29 @@ export default class Overlay {
|
|||
// ...we want to close it
|
||||
|
||||
// Logic for the transition animation to collapse the window
|
||||
window.dataset['widthBeforeMinimize'] = window.style.width;
|
||||
window.dataset['heightBeforeMinimize'] = window.style.height;
|
||||
window.dataset['minHeightBeforeMinimize'] = window.style.minHeight;
|
||||
windowContent.style.height = windowContent.scrollHeight + 'px';
|
||||
window.style.width = window.scrollWidth + 'px'; // So the width of the window does not change due to the lack of content
|
||||
windowContent.style.height = '0'; // Set the height to 0px
|
||||
windowContent.addEventListener('transitionend', function handler() { // Add an event listener to cleanup once the minimize transition is complete
|
||||
void windowContent.offsetHeight; // Force layout so the height transition always has a real start value
|
||||
if (!window.style.width) {
|
||||
window.style.width = window.scrollWidth + 'px'; // So the width of the window does not change due to the lack of content
|
||||
}
|
||||
finishMinimizeTransition(() => {
|
||||
windowContent.style.display = 'none'; // Changes "display" to "none" for screen readers
|
||||
button.disabled = false; // Enables the button
|
||||
button.style.textDecoration = ''; // Resets the text decoration to default
|
||||
windowContent.removeEventListener('transitionend', handler); // Removes the event listener
|
||||
});
|
||||
windowContent.style.height = '0'; // Set the height to 0px
|
||||
if (window.style.height || window.classList.contains('bm-windowed')) {
|
||||
window.style.minHeight = '0px';
|
||||
window.style.height = getCollapsedHeight() + 'px';
|
||||
}
|
||||
|
||||
// Makes a clone of the h1 element inside the window, and adds it to the dragbar
|
||||
const dragbarHeader1 = header.cloneNode(true);
|
||||
const dragbarHeader1 = header?.cloneNode(true) ?? document.createElement('h1');
|
||||
const dragbarHeader1Text = dragbarHeader1.textContent;
|
||||
button.nextElementSibling.appendChild(dragbarHeader1);
|
||||
|
||||
button.textContent = '▶'; // Swap button icon
|
||||
button.innerHTML = minimizeIconCollapsed; // Swap button icon
|
||||
button.dataset['buttonStatus'] = 'collapsed'; // Swap button status tracker
|
||||
button.ariaLabel = `Unminimize window "${dragbarHeader1Text}"`; // Screen reader label
|
||||
} else {
|
||||
|
|
@ -1256,16 +1308,19 @@ export default class Overlay {
|
|||
// Logic for the transition animation to expand the window
|
||||
windowContent.style.display = ''; // Resets display to default
|
||||
windowContent.style.height = '0'; // Sets the height to 0
|
||||
window.style.width = ''; // Resets the window width to default
|
||||
windowContent.style.height = windowContent.scrollHeight + 'px'; // Change the height back to normal
|
||||
windowContent.addEventListener('transitionend', function handler() { // Add an event listener to cleanup once the minimize transition is complete
|
||||
window.style.width = window.dataset['widthBeforeMinimize'] ?? ''; // Restores width to the pre-minimized value
|
||||
window.style.minHeight = window.dataset['minHeightBeforeMinimize'] ?? ''; // Restores resizable windows
|
||||
window.style.height = window.dataset['heightBeforeMinimize'] ?? ''; // Restores height to the pre-minimized value
|
||||
void windowContent.offsetHeight; // Force layout before expanding from 0px
|
||||
finishMinimizeTransition(() => {
|
||||
windowContent.style.height = ''; // Changes the height back to default
|
||||
button.disabled = false; // Enables the button
|
||||
button.style.textDecoration = ''; // Resets the text decoration to default
|
||||
windowContent.removeEventListener('transitionend', handler); // Removes the event listener
|
||||
delete window.dataset['widthBeforeMinimize'];
|
||||
delete window.dataset['heightBeforeMinimize'];
|
||||
delete window.dataset['minHeightBeforeMinimize'];
|
||||
});
|
||||
windowContent.style.height = windowContent.scrollHeight + 'px'; // Change the height back to normal
|
||||
|
||||
button.textContent = '▼'; // Swap button icon
|
||||
button.innerHTML = minimizeIconExpanded; // Swap button icon
|
||||
button.dataset['buttonStatus'] = 'expanded'; // Swap button status tracker
|
||||
button.ariaLabel = `Minimize window "${dragbarHeader1Text}"`; // Screen reader label
|
||||
}
|
||||
|
|
@ -1278,11 +1333,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 +1431,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 +1475,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 +1627,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Overlay from "./Overlay";
|
||||
import Overlay, { minimizeIconExpanded } from "./Overlay";
|
||||
import { localizeDate } from "./utils";
|
||||
|
||||
/** Manages the credits window for Blue Marble.
|
||||
|
|
@ -54,7 +54,7 @@ export default class WindowCredts extends Overlay {
|
|||
// Creates a new credits window
|
||||
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window'}, (instance, div) => {})
|
||||
.addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Credits"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, 'aria-label': 'Minimize window "Credits"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
button.onclick = () => instance.handleMinimization(button);
|
||||
button.ontouchend = () => {button.click()}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
|
|
@ -116,4 +116,4 @@ export default class WindowCredts extends Overlay {
|
|||
// Creates dragging capability on the drag bar for dragging the window
|
||||
this.handleDrag(`#${this.windowID}.bm-window`, `#${this.windowID} .bm-dragbar`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,117 @@
|
|||
/* @since 0.88.459 */
|
||||
|
||||
/* Filter window eye icon inside paragraphs */
|
||||
/* Filter window visibility icon */
|
||||
#bm-window-filter .bm-filter-eye-icon {
|
||||
display: block;
|
||||
width: 1.28em;
|
||||
height: 1.28em;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.9;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Filter window inline icons inside paragraphs */
|
||||
#bm-window-filter p svg {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
fill: white;
|
||||
width: 1em;
|
||||
color: currentColor;
|
||||
vertical-align: -0.16em;
|
||||
}
|
||||
|
||||
#bm-window-filter:not(.bm-windowed) {
|
||||
width: min(50rem, calc(100vw - 0.55rem));
|
||||
max-width: min(50rem, calc(100vw - 0.55rem)) !important;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-header {
|
||||
padding-top: 0.08rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-toolbar {
|
||||
gap: 0.22rem;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
padding: 0.16rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-toolbar > button {
|
||||
flex: 1 1 10rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-scrollable {
|
||||
padding-right: 0.08rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-insights {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(16rem, 20rem) minmax(0, 1fr);
|
||||
gap: 0.24rem 0.3rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-insights > hr,
|
||||
#bm-window-filter .bm-filter-sort-panel {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-stats-card,
|
||||
#bm-window-filter .bm-filter-note,
|
||||
#bm-window-filter .bm-filter-sort-panel {
|
||||
padding: 0.38rem 0.48rem;
|
||||
border-radius: 13px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.16),
|
||||
0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-stats-card {
|
||||
display: grid;
|
||||
gap: 0.18rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-stats-card br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-stats-card span {
|
||||
display: block;
|
||||
padding: 0.28rem 0.38rem;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-note p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-sort-panel fieldset {
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-sort-panel legend {
|
||||
margin-bottom: 0.12rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-sort-panel .bm-container {
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-sort-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-sort-actions button {
|
||||
min-width: 7.8rem;
|
||||
}
|
||||
|
||||
/* Filter flex */
|
||||
|
|
@ -13,33 +120,63 @@
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1em 3ch;
|
||||
align-items: stretch;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* Filter color */
|
||||
#bm-window-filter .bm-filter-color {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: fit-content;
|
||||
max-width: 35ch;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
border-radius: 1em;
|
||||
padding: 0.5em;
|
||||
gap: 1ch;
|
||||
transition: background-color 0.3s ease;
|
||||
padding: 0.28rem;
|
||||
gap: 0.28rem;
|
||||
border-radius: 13px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.16),
|
||||
0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
transition:
|
||||
background 0.25s ease,
|
||||
border-color 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
transform 0.25s ease;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-color::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.18), transparent 24%),
|
||||
radial-gradient(circle at 18% 0%, rgba(186, 246, 255, 0.12), transparent 22%);
|
||||
}
|
||||
|
||||
/* Filter color on hover */
|
||||
#bm-window-filter .bm-filter-color:hover,
|
||||
#bm-window-filter.bm-filter-color:focus-within {
|
||||
background-color: rgba(17, 40, 85, 0.9);
|
||||
#bm-window-filter .bm-filter-color:focus-within {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(146, 221, 255, 0.26);
|
||||
background: linear-gradient(160deg, rgba(255, 255, 255, 0.22), rgba(186, 246, 255, 0.1));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 10px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display */
|
||||
#bm-window-filter .bm-filter-container-rgb {
|
||||
display: block;
|
||||
border: thick double darkslategray;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 1ch;
|
||||
padding: 0.26rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.14),
|
||||
0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display for Other color */
|
||||
|
|
@ -60,8 +197,9 @@
|
|||
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;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display for Transparent color */
|
||||
|
|
@ -71,19 +209,36 @@
|
|||
|
||||
/* Filter window hide color button */
|
||||
#bm-window-filter .bm-filter-container-rgb button {
|
||||
padding: 0.75em 0.5ch;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.24em;
|
||||
min-width: 2.05rem;
|
||||
min-height: 2.05rem;
|
||||
border-radius: 999px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Filter window hide color button SVG */
|
||||
#bm-window-filter .bm-filter-container-rgb svg {
|
||||
width: 4ch;
|
||||
#bm-window-filter .bm-filter-container-rgb .bm-filter-eye-icon {
|
||||
width: 1.55rem;
|
||||
height: 1.55rem;
|
||||
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.18));
|
||||
}
|
||||
|
||||
/* Filter window container for color information */
|
||||
#bm-window-filter .bm-filter-color > .bm-flex-between {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
gap: 0.02rem;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-color h2 {
|
||||
margin: 0.04rem 0 0;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-color .bm-filter-color-pxl-desc {
|
||||
margin: 0.1rem 0 0;
|
||||
}
|
||||
|
||||
/* Filter window color flavor text */
|
||||
|
|
@ -98,46 +253,186 @@
|
|||
|
||||
/* 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: 360px;
|
||||
height: min(60vh, 22rem);
|
||||
min-width: 360px;
|
||||
min-height: 180px;
|
||||
max-width: min(1000px, calc(100vw - 16px)) !important;
|
||||
max-height: min(1400px, calc(100vh - 16px)) !important;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
transition:
|
||||
background 320ms ease,
|
||||
border-color 220ms ease,
|
||||
box-shadow 220ms ease,
|
||||
transform 0s;
|
||||
}
|
||||
|
||||
/* Keep the content area flexible inside the resizable window */
|
||||
#bm-window-filter.bm-windowed .bm-window-content {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto auto minmax(0, 1fr);
|
||||
grid-row: 2;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#bm-window-filter.bm-windowed .bm-filter-toolbar {
|
||||
gap: 0.16rem;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
padding: 0.12rem;
|
||||
}
|
||||
|
||||
#bm-window-filter.bm-windowed .bm-filter-toolbar > button {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Filter flex in windowed mode */
|
||||
#bm-window-filter.bm-windowed #bm-filter-flex {
|
||||
flex-direction: column;
|
||||
gap: 0.25em;
|
||||
align-items: stretch;
|
||||
gap: 0.16rem;
|
||||
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;
|
||||
padding: 0.12rem;
|
||||
border-radius: 11px;
|
||||
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: 5;
|
||||
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: 4px;
|
||||
bottom: 4px;
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
z-index: 5;
|
||||
cursor: nwse-resize;
|
||||
pointer-events: auto;
|
||||
opacity: 0.78;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
color: rgba(255, 255, 255, 0.86);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.14), rgba(83, 141, 255, 0.14));
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
#bm-window-filter.bm-windowed .bm-resize-corner:hover,
|
||||
#bm-window-filter.bm-windowed .bm-resize-corner.bm-resizing {
|
||||
opacity: 1;
|
||||
border-color: rgba(146, 221, 255, 0.36);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 8px 16px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
/* Filter window container for RGB color display in windowed mode */
|
||||
#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;
|
||||
padding: 0.1rem 0.2rem;
|
||||
border: none;
|
||||
border-radius: 1em;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Filter window hide color button */
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb button {
|
||||
padding: 0.5em 0.25ch;
|
||||
padding: 0.2em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* Filter window hide color button SVG in windowed mode */
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb svg {
|
||||
width: 3ch;
|
||||
#bm-window-filter.bm-windowed .bm-filter-container-rgb .bm-filter-eye-icon {
|
||||
width: 1.36rem;
|
||||
height: 1.36rem;
|
||||
}
|
||||
|
||||
/* Filter window header 2 in windowed mode */
|
||||
#bm-window-filter.bm-windowed .bm-filter-color h2 {
|
||||
font-size: 0.75em;
|
||||
font-size: 0.78rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Filter window dragbar text area in windowed mode */
|
||||
#bm-window-filter #bm-filter-windowed-color-totals {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.08rem 0.24rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
#bm-window-filter:not(.bm-windowed) {
|
||||
width: min(100vw - 0.35rem, 50rem);
|
||||
max-width: min(100vw - 0.35rem, 50rem) !important;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-insights {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#bm-window-filter .bm-filter-note,
|
||||
#bm-window-filter .bm-filter-sort-panel,
|
||||
#bm-window-filter .bm-filter-insights > hr {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import ConfettiManager from "./confetttiManager";
|
||||
import Overlay from "./Overlay";
|
||||
import Overlay, { minimizeIconExpanded } from "./Overlay";
|
||||
import { calculateRelativeLuminance, localizeDate, localizeNumber, localizePercent, rgbToHex } from "./utils";
|
||||
|
||||
const closeIcon = '<svg class="bm-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7L7 17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>';
|
||||
const fullscreenIcon = '<svg class="bm-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8.5 4.5H4.5v4M15.5 4.5h4v4M19.5 15.5v4h-4M8.5 19.5h-4v-4" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/><path d="M4.8 4.8l5.2 5.2M19.2 4.8L14 10M19.2 19.2L14 14M4.8 19.2L10 14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
|
||||
const windowedIcon = '<svg class="bm-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4.8 4.8l5.2 5.2M19.2 4.8L14 10M19.2 19.2L14 14M4.8 19.2L10 14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M10 7.5V10H7.5M16.5 10H14V7.5M14 16.5V14h2.5M7.5 14H10v2.5" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
/** The overlay builder for the color filter Blue Marble window.
|
||||
* @description This class handles the overlay UI for the color filter window of the Blue Marble userscript.
|
||||
* @class WindowFilter
|
||||
|
|
@ -21,13 +25,25 @@ 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.colorRefreshInterval = null; // Auto-refresh timer for live color statistics
|
||||
this.colorRefreshIntervalMS = 10000; // Refresh Color Filter statistics every 10 seconds
|
||||
this.windowMinWidth = 360; // 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;
|
||||
|
||||
// Eye icons
|
||||
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>';
|
||||
this.eyeOpen = '<svg class="bm-filter-eye-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.8 12s3.1-5 8.2-5 8.2 5 8.2 5-3.1 5-8.2 5-8.2-5-8.2-5Z"/><circle cx="12" cy="12" r="2.5"/></svg>';
|
||||
this.eyeClosed = '<svg class="bm-filter-eye-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4.6 9.8C6.1 8.3 8.6 7 12 7c5.1 0 8.2 5 8.2 5a15.2 15.2 0 0 1-2.2 2.7"/><path d="M14.1 16.7a8.3 8.3 0 0 1-2.1.3c-5.1 0-8.2-5-8.2-5a14.9 14.9 0 0 1 1.8-2.3"/><path d="M5 5l14 14"/><path d="M10.4 10.7a2.5 2.5 0 0 0 2.9 2.9"/></svg>';
|
||||
|
||||
// Obtains the color palette Blue Marble currently uses
|
||||
const { palette: palette, LUT: _ } = this.templateManager.paletteBM;
|
||||
|
|
@ -46,11 +62,22 @@ export default class WindowFilter extends Overlay {
|
|||
this.timeRemainingLocalized = ''; // The date & time the user will complete the templates in the date-time format of the user's device, as a string
|
||||
|
||||
// Color list display settings
|
||||
this.sortPrimary = 'id'; // The last used primary sort option
|
||||
this.sortSecondary = 'ascending'; // The last used secondary sort option
|
||||
this.sortPrimary = 'total'; // The last used primary sort option
|
||||
this.sortSecondary = 'descending'; // The last used secondary sort option
|
||||
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.#prefersWindowedMode()) {
|
||||
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 +87,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;
|
||||
}
|
||||
|
||||
|
|
@ -71,48 +98,43 @@ export default class WindowFilter extends Overlay {
|
|||
// div.parentElement.appendChild(div); // When the window is clicked on, bring to top
|
||||
// }
|
||||
}).addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Color Filter"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, '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': 'Switch to windowed mode for "Color Filter"'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': windowedIcon, '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();};
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': closeIcon, 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
|
||||
button.onclick = () => this.#closeWindow();
|
||||
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'})
|
||||
.addDiv({'class': 'bm-container bm-center-vertically bm-filter-header'})
|
||||
.addHeader(1, {'textContent': 'Color Filter'}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
|
||||
.addButton({'textContent': 'Hide All Colors'}, (instance, button) => {
|
||||
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically bm-filter-toolbar', 'style': 'gap: 1.5ch;'})
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'Hide All Colors'}, (instance, button) => {
|
||||
button.onclick = () => this.#selectColorList(false);
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'Refresh Data'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
button.disabled = true;
|
||||
this.updateColorList();
|
||||
button.disabled = false;
|
||||
};
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'Show All Colors'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'Show All Colors'}, (instance, button) => {
|
||||
button.onclick = () => this.#selectColorList(true);
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container bm-scrollable'})
|
||||
.addDiv({'class': 'bm-container', 'style': 'margin-left: 2.5ch; margin-right: 2.5ch;'})
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container bm-scrollable bm-filter-scrollable'})
|
||||
.addDiv({'class': 'bm-container bm-filter-insights', 'style': 'margin-left: 2.5ch; margin-right: 2.5ch;'})
|
||||
.addDiv({'class': 'bm-container bm-filter-stats-card'})
|
||||
.addSpan({'id': 'bm-filter-tile-load', 'innerHTML': '<b>Tiles Loaded:</b> 0 / ???'}).buildElement()
|
||||
.addBr().buildElement()
|
||||
.addSpan({'id': 'bm-filter-tot-correct', 'innerHTML': '<b>Correct Pixels:</b> ???'}).buildElement()
|
||||
|
|
@ -123,11 +145,11 @@ export default class WindowFilter extends Overlay {
|
|||
.addBr().buildElement()
|
||||
.addSpan({'id': 'bm-filter-tot-completed', 'innerHTML': '??? ???'}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.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()
|
||||
.addDiv({'class': 'bm-container bm-filter-note'})
|
||||
.addP({'innerHTML': `Press the ${windowedIcon.replace('<svg', '<svg aria-label="Switch to windowed mode"')} button to make this window smaller. Colors with the icon ${this.eyeOpen.replace('<svg', '<svg aria-label="Eye Open"')} will be shown on the canvas. Colors with the icon ${this.eyeClosed.replace('<svg', '<svg aria-label="Eye Closed"')} will not be shown on the canvas. The "Hide All Colors" and "Show All Colors" buttons only apply to colors that display in the list below. The amount of correct pixels is dependent on how many tiles of the template you have loaded since you last opened Wplace.live. If all tiles have been loaded, then the "correct pixel" count is accurate.`}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addForm({'class': 'bm-container'})
|
||||
.addForm({'class': 'bm-container bm-filter-sort-panel'})
|
||||
.addFieldset()
|
||||
.addLegend({'textContent': 'Sort Options:', 'style': 'font-weight: 700;'}).buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
|
|
@ -150,8 +172,8 @@ export default class WindowFilter extends Overlay {
|
|||
.addCheckbox({'id': 'bm-filter-show-unused', 'name': 'showUnused', 'textContent': 'Show unused colors'}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addButton({'textContent': 'Sort Colors', 'type': 'submit'}, (instance, button) => {
|
||||
.addDiv({'class': 'bm-container bm-filter-sort-actions'})
|
||||
.addButton({'class': 'bm-button-primary', 'textContent': 'Sort Colors', 'type': 'submit'}, (instance, button) => {
|
||||
button.onclick = (event) => {
|
||||
event.preventDefault(); // Stop default form submission
|
||||
|
||||
|
|
@ -183,6 +205,7 @@ export default class WindowFilter extends Overlay {
|
|||
|
||||
// These run when the user opens the Color Filter window
|
||||
this.#buildColorList(scrollableContainer);
|
||||
this.#syncSortFormControls();
|
||||
this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
|
||||
|
||||
// Displays some template statistics to the user
|
||||
|
|
@ -191,6 +214,7 @@ export default class WindowFilter extends Overlay {
|
|||
this.updateInnerHTML('#bm-filter-tot-total', `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
|
||||
this.updateInnerHTML('#bm-filter-tot-remaining', `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
|
||||
this.updateInnerHTML('#bm-filter-tot-completed', `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, 'Z')}">${this.timeRemainingLocalized}</time>`);
|
||||
this.#startAutoRefresh();
|
||||
}
|
||||
|
||||
/** Spawns a windowed Color Filter window.
|
||||
|
|
@ -202,14 +226,18 @@ 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: 360px; 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) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, '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) {
|
||||
|
|
@ -224,54 +252,318 @@ export default class WindowFilter extends Overlay {
|
|||
// Minimized h1 element will appear here
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-flex-center'})
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '🗖', 'aria-label': 'Switch to fullscreen mode for "Color Filter"'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': fullscreenIcon, '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();};
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': closeIcon, 'aria-label': 'Close window "Color Filter"'}, (instance, button) => {
|
||||
button.onclick = () => this.#closeWindow();
|
||||
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'})
|
||||
.addDiv({'class': 'bm-container bm-center-vertically bm-filter-header'})
|
||||
.addHeader(1, {'textContent': 'Color Filter'}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically', 'style': 'gap: 1.5ch;'})
|
||||
.addButton({'textContent': 'None'}, (instance, button) => {
|
||||
.addDiv({'class': 'bm-container bm-flex-between bm-center-vertically bm-filter-toolbar', 'style': 'gap: 1.5ch;'})
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'None'}, (instance, button) => {
|
||||
button.onclick = () => this.#selectColorList(false);
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'Refresh'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
button.disabled = true;
|
||||
this.updateColorList();
|
||||
button.disabled = false;
|
||||
};
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'All'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'All'}, (instance, button) => {
|
||||
button.onclick = () => this.#selectColorList(true);
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container bm-scrollable'})
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container bm-scrollable bm-filter-scrollable'})
|
||||
// Color list will appear here
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.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`);
|
||||
|
||||
// These run when the user opens the Color Filter window
|
||||
this.#buildColorList(scrollableContainer);
|
||||
this.#syncSortFormControls();
|
||||
this.#sortColorList(this.sortPrimary, this.sortSecondary, this.showUnused);
|
||||
this.#startAutoRefresh();
|
||||
}
|
||||
|
||||
/** 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];
|
||||
}
|
||||
|
||||
/** Returns whether the filter should open in windowed mode.
|
||||
* Defaults to windowed mode when no explicit preference was stored.
|
||||
* @returns {boolean}
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#prefersWindowedMode() {
|
||||
const windowState = this.#getWindowState();
|
||||
if (windowState?.mode == 'windowed') {return true;}
|
||||
if (windowState?.mode == 'fullscreen') {return false;}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Updates the preferred window mode setting.
|
||||
* @param {boolean} shouldBeWindowed
|
||||
* @since 0.92.0
|
||||
*/
|
||||
#setWindowModePreference(shouldBeWindowed) {
|
||||
const windowState = this.#getWindowState();
|
||||
if (windowState) {
|
||||
windowState.mode = shouldBeWindowed ? 'windowed' : 'fullscreen';
|
||||
}
|
||||
if (!this.settingsManager) {return;}
|
||||
this.settingsManager.toggleFlag(this.windowModeFlag, shouldBeWindowed);
|
||||
void this.settingsManager.saveUserStorageNow();
|
||||
}
|
||||
|
||||
/** Updates the visible sort controls to reflect the active sort state.
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#syncSortFormControls() {
|
||||
const sortPrimaryInput = document.querySelector(`#${this.windowID} #bm-filter-sort-primary`);
|
||||
const sortSecondaryInput = document.querySelector(`#${this.windowID} #bm-filter-sort-secondary`);
|
||||
const showUnusedInput = document.querySelector(`#${this.windowID} #bm-filter-show-unused`);
|
||||
|
||||
if (sortPrimaryInput instanceof HTMLSelectElement) {
|
||||
sortPrimaryInput.value = this.sortPrimary;
|
||||
}
|
||||
if (sortSecondaryInput instanceof HTMLSelectElement) {
|
||||
sortSecondaryInput.value = this.sortSecondary;
|
||||
}
|
||||
if (showUnusedInput instanceof HTMLInputElement) {
|
||||
showUnusedInput.checked = this.showUnused;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.#stopAutoRefresh();
|
||||
this.#cleanupWindowPersistence();
|
||||
windowElement?.remove();
|
||||
}
|
||||
|
||||
/** Starts the automatic Color Filter statistics refresh loop.
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#startAutoRefresh() {
|
||||
this.#stopAutoRefresh();
|
||||
this.colorRefreshInterval = setInterval(() => {
|
||||
if (!document.querySelector(`#${this.windowID}`)) {
|
||||
this.#stopAutoRefresh();
|
||||
return;
|
||||
}
|
||||
this.updateColorList();
|
||||
}, this.colorRefreshIntervalMS);
|
||||
}
|
||||
|
||||
/** Stops the automatic Color Filter statistics refresh loop.
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#stopAutoRefresh() {
|
||||
if (!this.colorRefreshInterval) {return;}
|
||||
clearInterval(this.colorRefreshInterval);
|
||||
this.colorRefreshInterval = null;
|
||||
}
|
||||
|
||||
/** Disconnects live observers used for window persistence.
|
||||
* @since 0.92.0
|
||||
*/
|
||||
#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;}
|
||||
if (windowElement.querySelector('.bm-dragbar button[data-button-status="collapsed"]')) {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.
|
||||
|
|
@ -348,7 +640,8 @@ export default class WindowFilter extends Overlay {
|
|||
'class': 'bm-button-trans ' + bgEffectForButtons,
|
||||
'data-state': isColorHidden ? 'hidden' : 'shown',
|
||||
'aria-label': isColorHidden ? `Show the color ${color.name || ''} on templates.` : `Hide the color ${color.name || ''} on templates.`,
|
||||
'innerHTML': isColorHidden ? this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`) : this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`)},
|
||||
'innerHTML': isColorHidden ? this.eyeClosed : this.eyeOpen,
|
||||
'style': `color: ${textColorForPaletteColorBackground};`},
|
||||
(instance, button) => {
|
||||
|
||||
// When the button is clicked
|
||||
|
|
@ -356,15 +649,15 @@ export default class WindowFilter extends Overlay {
|
|||
button.style.textDecoration = 'none';
|
||||
button.disabled = true;
|
||||
if (button.dataset['state'] == 'shown') {
|
||||
button.innerHTML = this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
|
||||
button.innerHTML = this.eyeClosed;
|
||||
button.dataset['state'] = 'hidden';
|
||||
button.ariaLabel = `Show the color ${color.name || ''} on templates.`;
|
||||
this.templateManager.shouldFilterColor.set(color.id, true);
|
||||
this.templateManager.setColorFiltered(color.id, true);
|
||||
} else {
|
||||
button.innerHTML = this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
|
||||
button.innerHTML = this.eyeOpen;
|
||||
button.dataset['state'] = 'shown';
|
||||
button.ariaLabel = `Hide the color ${color.name || ''} on templates.`;
|
||||
this.templateManager.shouldFilterColor.delete(color.id);
|
||||
this.templateManager.setColorFiltered(color.id, false);
|
||||
}
|
||||
button.disabled = false;
|
||||
button.style.textDecoration = '';
|
||||
|
|
@ -397,7 +690,8 @@ export default class WindowFilter extends Overlay {
|
|||
'class': 'bm-button-trans ' + bgEffectForButtons,
|
||||
'data-state': isColorHidden ? 'hidden' : 'shown',
|
||||
'aria-label': isColorHidden ? `Show the color ${color.name || ''} on templates.` : `Hide the color ${color.name || ''} on templates.`,
|
||||
'innerHTML': isColorHidden ? this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`) : this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`)},
|
||||
'innerHTML': isColorHidden ? this.eyeClosed : this.eyeOpen,
|
||||
'style': `color: ${textColorForPaletteColorBackground};`},
|
||||
(instance, button) => {
|
||||
|
||||
// When the button is clicked
|
||||
|
|
@ -405,15 +699,15 @@ export default class WindowFilter extends Overlay {
|
|||
button.style.textDecoration = 'none';
|
||||
button.disabled = true;
|
||||
if (button.dataset['state'] == 'shown') {
|
||||
button.innerHTML = this.eyeClosed.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
|
||||
button.innerHTML = this.eyeClosed;
|
||||
button.dataset['state'] = 'hidden';
|
||||
button.ariaLabel = `Show the color ${color.name || ''} on templates.`;
|
||||
this.templateManager.shouldFilterColor.set(color.id, true);
|
||||
this.templateManager.setColorFiltered(color.id, true);
|
||||
} else {
|
||||
button.innerHTML = this.eyeOpen.replace('<svg', `<svg fill="${textColorForPaletteColorBackground}"`);
|
||||
button.innerHTML = this.eyeOpen;
|
||||
button.dataset['state'] = 'shown';
|
||||
button.ariaLabel = `Hide the color ${color.name || ''} on templates.`;
|
||||
this.templateManager.shouldFilterColor.delete(color.id);
|
||||
this.templateManager.setColorFiltered(color.id, false);
|
||||
}
|
||||
button.disabled = false;
|
||||
button.style.textDecoration = '';
|
||||
|
|
@ -605,6 +899,12 @@ export default class WindowFilter extends Overlay {
|
|||
this.updateInnerHTML('#bm-filter-windowed-color-totals', `${allCorrect}/${allTotal}`, true);
|
||||
}
|
||||
|
||||
this.updateInnerHTML('#bm-filter-tile-load', `<b>Tiles Loaded:</b> ${localizeNumber(this.tilesLoadedTotal)} / ${localizeNumber(this.tilesTotal)}`);
|
||||
this.updateInnerHTML('#bm-filter-tot-correct', `<b>Correct Pixels:</b> ${localizeNumber(this.allPixelsCorrectTotal)}`);
|
||||
this.updateInnerHTML('#bm-filter-tot-total', `<b>Total Pixels:</b> ${localizeNumber(this.allPixelsTotal)}`);
|
||||
this.updateInnerHTML('#bm-filter-tot-remaining', `<b>Remaining:</b> ${localizeNumber((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0))} (${localizePercent(((this.allPixelsTotal || 0) - (this.allPixelsCorrectTotal || 0)) / (this.allPixelsTotal || 1))})`);
|
||||
this.updateInnerHTML('#bm-filter-tot-completed', `<b>Completed at:</b> <time datetime="${this.timeRemaining.toISOString().replace(/\.\d{3}Z$/, 'Z')}">${this.timeRemainingLocalized}</time>`);
|
||||
|
||||
// Return early if the color list does not exist.
|
||||
// We can't update DOM elements that don't exist, so we exit now.
|
||||
if (!colorList) {return colorStatistics;}
|
||||
|
|
@ -652,6 +952,8 @@ export default class WindowFilter extends Overlay {
|
|||
#calculatePixelStatistics() {
|
||||
|
||||
// Resets pixel totals to 0
|
||||
this.tilesLoadedTotal = 0;
|
||||
this.tilesTotal = 0;
|
||||
this.allPixelsTotal = 0;
|
||||
this.allPixelsCorrectTotal = 0;
|
||||
this.allPixelsCorrect = new Map();
|
||||
|
|
@ -705,4 +1007,4 @@ export default class WindowFilter extends Overlay {
|
|||
this.timeRemaining = new Date(((this.allPixelsTotal - this.allPixelsCorrectTotal) * 30 * 1000) + Date.now());
|
||||
this.timeRemainingLocalized = localizeDate(this.timeRemaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
173
src/WindowMain.css
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/* @since 0.92.2 */
|
||||
|
||||
#bm-window-main {
|
||||
width: min(25.5rem, calc(100vw - 0.65rem));
|
||||
max-width: min(25.5rem, calc(100vw - 0.65rem)) !important;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-window-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-hero,
|
||||
#bm-window-main .bm-main-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: linear-gradient(150deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
0 10px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-hero::before,
|
||||
#bm-window-main .bm-main-shell::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.2), transparent 26%),
|
||||
radial-gradient(circle at 20% 0%, rgba(186, 246, 255, 0.16), transparent 24%);
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.48rem 0.58rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-hero h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.24rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-card {
|
||||
min-width: 0;
|
||||
min-height: 2.55rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0.34rem 0.45rem;
|
||||
border-radius: 11px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: linear-gradient(160deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.07));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||
color: rgba(18, 35, 63, 0.96);
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-card > span,
|
||||
#bm-window-main .bm-main-stat-card > time {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-card-value,
|
||||
#bm-window-main .bm-main-stat-card-timer {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.08rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-value {
|
||||
display: inline-block;
|
||||
font-size: 1.3em;
|
||||
color: rgba(15, 31, 56, 0.96);
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-value b {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-label {
|
||||
color: rgba(49, 71, 105, 0.82);
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-family: var(--bm-font-display);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stat-card time {
|
||||
white-space: nowrap;
|
||||
font-family: var(--bm-font-mono);
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-shell {
|
||||
padding: 0.48rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-coords {
|
||||
display: grid;
|
||||
grid-template-columns: auto repeat(4, minmax(0, 1fr));
|
||||
gap: 0.22rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-coords .bm-button-pin {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-coords .bm-input-coords {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-upload,
|
||||
#bm-window-main .bm-main-status {
|
||||
margin-top: 0.24rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-upload > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-actions {
|
||||
gap: 0.24rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-actions > button {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-status textarea {
|
||||
min-height: 4.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#bm-window-main {
|
||||
width: min(100vw - 0.45rem, 25.5rem);
|
||||
max-width: min(100vw - 0.45rem, 25.5rem) !important;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-coords {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-coords .bm-button-pin {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
aspect-ratio: auto;
|
||||
height: 1.8rem;
|
||||
}
|
||||
|
||||
#bm-window-main .bm-main-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import ConfettiManager from "./confetttiManager";
|
||||
import Overlay from "./Overlay";
|
||||
import Overlay, { minimizeIconExpanded } from "./Overlay";
|
||||
import { getClipboardData } from "./utils";
|
||||
import WindowCredts from "./WindowCredits";
|
||||
import WindowFilter from "./WindowFilter";
|
||||
import WindowWizard from "./WindowWizard";
|
||||
|
||||
const settingsIcon = '<svg class="bm-button-icon bm-button-icon-settings" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 7h14M5 12h14M5 17h14"/><circle cx="9" cy="7" r="1.7" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.7" fill="currentColor" stroke="none"/><circle cx="11" cy="17" r="1.7" fill="currentColor" stroke="none"/></g></svg>';
|
||||
|
||||
/** The overlay builder for the main Blue Marble window.
|
||||
* @description This class handles the overlay UI for the main window of the Blue Marble userscript.
|
||||
|
|
@ -45,14 +45,22 @@ export default class WindowMain extends Overlay {
|
|||
// div.parentElement.appendChild(div); // When the window is clicked on, bring to top
|
||||
// }
|
||||
}).addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Blue Marble"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, 'aria-label': 'Minimize window "Blue Marble"', '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', 'innerHTML': settingsIcon, 'title': 'Settings', 'aria-label': 'Open settings'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
instance.settingsManager.buildWindow();
|
||||
}
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-window-content'})
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container bm-main-hero'})
|
||||
.addImg({'class': 'bm-favicon', 'src': 'https://raw.githubusercontent.com/SwingTheVine/Wplace-BlueMarble/main/dist/assets/Favicon.png'}, (instance, img) => {
|
||||
// Adds a birthday hat & confetti to the window if it is Blue Marble's birthday
|
||||
const date = new Date();
|
||||
|
|
@ -69,20 +77,25 @@ export default class WindowMain extends Overlay {
|
|||
.addHeader(1, {'textContent': this.name}).buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addSpan({'id': 'bm-user-droplets', 'textContent': 'Droplets:'}).buildElement()
|
||||
.addBr().buildElement()
|
||||
.addSpan({'id': 'bm-user-nextlevel', 'textContent': 'Next level in...'}).buildElement()
|
||||
.addBr().buildElement()
|
||||
.addSpan({'textContent': 'Charges: '})
|
||||
.addTimer(Date.now(), 1000, {'style': 'font-weight: 700;'}, (instance, timer) => {
|
||||
.addDiv({'class': 'bm-container bm-main-stats'})
|
||||
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-value'})
|
||||
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Droplets'}).buildElement()
|
||||
.addSpan({'id': 'bm-user-droplets', 'class': 'bm-main-stat-value', 'textContent': '0'}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-value'})
|
||||
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Next Level'}).buildElement()
|
||||
.addSpan({'id': 'bm-user-nextlevel', 'class': 'bm-main-stat-value', 'textContent': '0 px'}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-main-stat-card bm-main-stat-card-timer'})
|
||||
.addSpan({'class': 'bm-main-stat-label', 'textContent': 'Charges'}).buildElement()
|
||||
.addTimer(Date.now(), 1000, {'class': 'bm-main-stat-value', 'style': 'font-weight: 700;'}, (instance, timer) => {
|
||||
instance.apiManager.chargeRefillTimerID = timer.id; // Store the timer ID in apiManager so we can update the timer automatically
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.addHr().buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addDiv({'class': 'bm-container bm-main-shell'})
|
||||
.addDiv({'class': 'bm-container bm-main-coords'})
|
||||
.addButton({'class': 'bm-button-circle bm-button-pin', 'style': 'margin-top: 0;', 'innerHTML': '<svg viewBox="0 0 4 6"><path d="M.5,3.4A2,2 0 1 1 3.5,3.4L2,6"/><circle cx="2" cy="2" r=".7" fill="#fff"/></svg>'},
|
||||
(instance, button) => {
|
||||
button.onclick = () => {
|
||||
|
|
@ -111,11 +124,11 @@ export default class WindowMain extends Overlay {
|
|||
input.addEventListener("paste", event => this.#coordinateInputPaste(instance, input, event));
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addDiv({'class': 'bm-container bm-main-upload'})
|
||||
.addInputFile({'class': 'bm-input-file', 'textContent': 'Upload Template', 'accept': 'image/png, image/jpeg, image/webp, image/bmp, image/gif'}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container bm-flex-between'})
|
||||
.addButton({'textContent': 'Disable', 'data-button-status': 'shown'}, (instance, button) => {
|
||||
.addDiv({'class': 'bm-container bm-flex-between bm-main-actions'})
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'Disable', 'data-button-status': 'shown'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
button.disabled = true; // Disables the button until the transition ends
|
||||
if (button.dataset['buttonStatus'] == 'shown') { // If templates are currently being 'shown' then hide them
|
||||
|
|
@ -132,7 +145,7 @@ export default class WindowMain extends Overlay {
|
|||
button.disabled = false; // Enables the button
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'Create'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-primary', 'textContent': 'Create'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const input = document.querySelector(`#${this.windowID} .bm-input-file`);
|
||||
|
||||
|
|
@ -153,52 +166,13 @@ export default class WindowMain extends Overlay {
|
|||
instance.handleDisplayStatus(`Drew to canvas!`);
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'textContent': 'Filter'}, (instance, button) => {
|
||||
button.onclick = () => this.#buildWindowFilter();
|
||||
.addButton({'class': 'bm-button-secondary', 'textContent': 'Filter'}, (instance, button) => {
|
||||
button.onclick = () => this.buildWindowFilter();
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container'})
|
||||
.addDiv({'class': 'bm-container bm-main-status'})
|
||||
.addTextarea({'id': this.outputStatusId, 'placeholder': `Status: Sleeping...\nVersion: ${this.version}`, 'readOnly': true}).buildElement()
|
||||
.buildElement()
|
||||
.addDiv({'class': 'bm-container bm-flex-between', 'style': 'margin-bottom: 0; flex-direction: column;'})
|
||||
.addDiv({'class': 'bm-flex-between'})
|
||||
// .addButton({'class': 'bm-button-circle', 'innerHTML': '🖌'}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '⚙️', 'title': 'Settings'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
instance.settingsManager.buildWindow();
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '🧙', 'title': 'Template Wizard'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const templateManager = instance.apiManager?.templateManager;
|
||||
const wizard = new WindowWizard(this.name, this.version, templateManager?.schemaVersion, templateManager);
|
||||
wizard.buildWindow();
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '🎨', 'title': 'Template Color Converter'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
window.open('https://pepoafonso.github.io/color_converter_wplace/', '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '🌐', 'title': 'Official Blue Marble Website'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
window.open('https://bluemarble.lol/', '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'title': 'Donate to SwingTheVine', 'innerHTML': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#fff" style="width:80%; margin:auto;"><path d="M249.8 75c89.8 0 113 1.1 146.3 4.4 78.1 7.8 123.6 56 123.6 125.2l0 8.9c0 64.3-47.1 116.9-110.8 122.4-5 16.6-12.8 33.2-23.3 49.9-24.4 37.7-73.1 85.3-162.9 85.3l-17.7 0c-73.1 0-129.7-31.6-163.5-89.2-29.9-50.4-33.8-106.4-33.8-181.2 0-73.7 44.4-113.6 96.4-120.2 39.3-5 88.1-5.5 145.7-5.5zm0 41.6c-60.4 0-103.6 .5-136.3 5.5-46 6.7-64.3 32.7-64.3 79.2l.2 25.7c1.2 57.3 7.1 97.1 27.5 134.5 26.6 49.3 74.8 68.2 129.7 68.2l17.2 0c72 0 107-34.9 126.3-65.4 9.4-15.5 17.7-32.7 22.2-54.3l3.3-13.8 19.9 0c44.3 0 82.6-36 82.6-82l0-8.3c0-51.5-32.2-78.7-88.1-85.3-31.6-2.8-50.4-3.9-140.2-3.9zM267 169.2c38.2 0 64.8 31.6 64.8 67 0 32.7-18.3 61-42.1 83.1-15 15-39.3 30.5-55.9 40.5-4.4 2.8-10 4.4-16.7 4.4-5.5 0-10.5-1.7-15.5-4.4-16.6-10-41-25.5-56.5-40.5-21.8-20.8-39.2-46.9-41.3-77l-.2-6.1c0-35.5 25.5-67 64.3-67 22.7 0 38.8 11.6 49.3 27.7 11.6-16.1 27.2-27.7 49.9-27.7zm122.5-3.9c28.3 0 43.8 16.6 43.8 43.2s-15.5 42.7-43.8 42.7c-8.9 0-13.8-5-13.8-11.7l0-62.6c0-6.7 5-11.6 13.8-11.6z"/></svg>'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
window.open('https://ko-fi.com/swingthevine', '_blank', 'noopener noreferrer');
|
||||
}
|
||||
}).buildElement()
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': '🤝', 'title': 'Credits'}, (instance, button) => {
|
||||
button.onclick = () => {
|
||||
const credits = new WindowCredts(this.name, this.version);
|
||||
credits.buildWindow();
|
||||
}
|
||||
}).buildElement()
|
||||
.buildElement()
|
||||
.addSmall({'textContent': 'Made by SwingTheVine', 'style': 'margin-top: auto;'}).buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.buildElement()
|
||||
.buildElement().buildOverlay(this.windowParent);
|
||||
|
|
@ -212,9 +186,9 @@ export default class WindowMain extends Overlay {
|
|||
* This might cause a memory leak. I pray that this is not the case...
|
||||
* @since 0.88.330
|
||||
*/
|
||||
#buildWindowFilter() {
|
||||
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 +228,4 @@ export default class WindowMain extends Overlay {
|
|||
instance.updateInnerHTML('bm-input-py', coords?.[3] || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import Overlay from "./Overlay";
|
||||
import Overlay, { minimizeIconExpanded } from "./Overlay";
|
||||
|
||||
const closeIcon = '<svg class="bm-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7L7 17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>';
|
||||
|
||||
/** The overlay builder for the settings window in Blue Marble.
|
||||
* The logic for this window is managed in {@link SettingsManager}
|
||||
|
|
@ -37,13 +39,13 @@ export default class WindowSettings extends Overlay {
|
|||
|
||||
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) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, 'aria-label': 'Minimize window "Settings"', '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) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': closeIcon, 'aria-label': 'Close window "Settings"'}, (instance, button) => {
|
||||
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
|
||||
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
|
|
@ -94,4 +96,4 @@ export default class WindowSettings extends Overlay {
|
|||
buildTemplate() {
|
||||
this.#errorOverrideFailure('Template');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Overlay from "./Overlay";
|
||||
import Overlay, { minimizeIconExpanded } from "./Overlay";
|
||||
import Template from "./Template";
|
||||
import TemplateManager from "./templateManager";
|
||||
import { encodedToNumber, escapeHTML, getWplaceVersion, localizeDate, localizeNumber, sleep } from "./utils";
|
||||
|
|
@ -63,7 +63,7 @@ export default class WindowWizard extends Overlay {
|
|||
// div.parentElement.appendChild(div); // When the window is clicked on, bring to top
|
||||
// }
|
||||
}).addDragbar()
|
||||
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Template Wizard"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
.addButton({'class': 'bm-button-circle', 'innerHTML': minimizeIconExpanded, 'aria-label': 'Minimize window "Template Wizard"', 'data-button-status': 'expanded'}, (instance, button) => {
|
||||
button.onclick = () => instance.handleMinimization(button);
|
||||
button.ontouchend = () => {button.click()}; // Needed only to negate weird interaction with dragbar
|
||||
}).buildElement()
|
||||
|
|
|
|||
|
|
@ -54,44 +54,7 @@ export default class ApiManager {
|
|||
switch (endpointText) {
|
||||
|
||||
case 'me': // Request to retrieve user data
|
||||
|
||||
// If the game can not retrieve the userdata...
|
||||
if (dataJSON['status'] && dataJSON['status']?.toString()[0] != '2') {
|
||||
// The server is probably down (NOT a 2xx status)
|
||||
|
||||
overlay.handleDisplayError(`You are not logged in or Wplace is offline!\nCould not fetch userdata.`);
|
||||
return; // Kills itself before attempting to display null userdata
|
||||
}
|
||||
|
||||
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1/0.65)) - dataJSON['pixelsPainted']); // Calculates pixels to the next level
|
||||
|
||||
console.log(dataJSON['id']);
|
||||
if (!!dataJSON['id'] || dataJSON['id'] === 0) {
|
||||
console.log(numberToEncoded(
|
||||
dataJSON['id'],
|
||||
'!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
|
||||
));
|
||||
}
|
||||
this.templateManager.userID = dataJSON['id'];
|
||||
|
||||
// Obtains the refill timer for charges
|
||||
if (this.chargeRefillTimerID.length != 0) {
|
||||
const chargeRefillTimer = document.querySelector('#' + this.chargeRefillTimerID);
|
||||
|
||||
// If the refill timer exists...
|
||||
if (chargeRefillTimer) {
|
||||
|
||||
/** Obtains the information about the user's charges @type {{cooldownMs: number, count: number, max: number}} */
|
||||
const chargeData = dataJSON['charges'];
|
||||
|
||||
// Date that the user's charges will be refilled
|
||||
chargeRefillTimer.dataset['endDate'] = Date.now() + ((chargeData['max'] - chargeData['count']) * chargeData['cooldownMs']);
|
||||
}
|
||||
}
|
||||
|
||||
// Updates displayed droplet information
|
||||
overlay.updateInnerHTML('bm-user-droplets', `Droplets: <b>${localizeNumber(dataJSON['droplets'])}</b>`); // Updates the text content of the droplets field
|
||||
overlay.updateInnerHTML('bm-user-nextlevel', `Next level in <b>${localizeNumber(nextLevelPixels)}</b> pixel${nextLevelPixels == 1 ? '' : 's'}`); // Updates the text content of the next level field
|
||||
this.applyUserDataToOverlay(overlay, dataJSON);
|
||||
break;
|
||||
|
||||
case 'pixel': // Request to retrieve pixel data
|
||||
|
|
@ -195,6 +158,90 @@ export default class ApiManager {
|
|||
});
|
||||
}
|
||||
|
||||
/** Applies user data from the /me endpoint to the current overlay.
|
||||
* @param {Overlay} overlay
|
||||
* @param {Object.<string, any>} dataJSON
|
||||
* @since 0.92.1
|
||||
*/
|
||||
applyUserDataToOverlay(overlay, dataJSON) {
|
||||
|
||||
// If the game can not retrieve the userdata...
|
||||
if (dataJSON['status'] && dataJSON['status']?.toString()[0] != '2') {
|
||||
overlay.handleDisplayError(`You are not logged in or Wplace is offline!\nCould not fetch userdata.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLevelPixels = Math.ceil(Math.pow(Math.floor(dataJSON['level']) * Math.pow(30, 0.65), (1 / 0.65)) - dataJSON['pixelsPainted']);
|
||||
|
||||
console.log(dataJSON['id']);
|
||||
if (!!dataJSON['id'] || dataJSON['id'] === 0) {
|
||||
console.log(numberToEncoded(
|
||||
dataJSON['id'],
|
||||
'!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'
|
||||
));
|
||||
}
|
||||
this.templateManager.userID = dataJSON['id'];
|
||||
|
||||
// Obtains the refill timer for charges
|
||||
if (this.chargeRefillTimerID.length != 0) {
|
||||
const chargeRefillTimer = document.querySelector('#' + this.chargeRefillTimerID);
|
||||
|
||||
// If the refill timer exists...
|
||||
if (chargeRefillTimer) {
|
||||
/** Obtains the information about the user's charges @type {{cooldownMs: number, count: number, max: number}} */
|
||||
const chargeData = dataJSON['charges'];
|
||||
|
||||
// Date that the user's charges will be refilled
|
||||
chargeRefillTimer.dataset['endDate'] = Date.now() + ((chargeData['max'] - chargeData['count']) * chargeData['cooldownMs']);
|
||||
}
|
||||
}
|
||||
|
||||
overlay.updateInnerHTML('bm-user-droplets', `<b>${localizeNumber(dataJSON['droplets'])}</b>`);
|
||||
overlay.updateInnerHTML('bm-user-nextlevel', `<b>${localizeNumber(nextLevelPixels)}</b> px`);
|
||||
}
|
||||
|
||||
/** Requests the current /me payload directly so the overlay has initial user data
|
||||
* even if the first network response was missed during startup.
|
||||
* @param {Overlay} overlay
|
||||
* @since 0.92.1
|
||||
*/
|
||||
async requestCurrentUserData(overlay) {
|
||||
try {
|
||||
const response = await fetch(`${window.location.origin}/api/me`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
overlay.handleDisplayError(`Could not fetch userdata.\nHTTP ${response.status}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataJSON = await response.json();
|
||||
this.applyUserDataToOverlay(overlay, dataJSON);
|
||||
} catch (error) {
|
||||
consoleError('Failed to fetch current user data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies cached /me data from sessionStorage if it was captured during early startup.
|
||||
* @param {Overlay} overlay
|
||||
* @returns {boolean}
|
||||
* @since 0.92.1
|
||||
*/
|
||||
applyCachedUserData(overlay) {
|
||||
try {
|
||||
const cached = sessionStorage.getItem('bm-last-me');
|
||||
if (!cached) {return false;}
|
||||
|
||||
const dataJSON = JSON.parse(cached);
|
||||
this.applyUserDataToOverlay(overlay, dataJSON);
|
||||
return true;
|
||||
} catch (error) {
|
||||
consoleError('Failed to apply cached user data:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a heartbeat to the telemetry server
|
||||
async sendHeartbeat(version) {
|
||||
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
@import './confettiManager.css';
|
||||
@import './overlay.css';
|
||||
@import './WindowFilter.css';
|
||||
@import './WindowMain.css';
|
||||
@import './WindowSettings.css';
|
||||
@import './WindowWizard.css';
|
||||
|
|
|
|||
55
src/main.js
|
|
@ -92,6 +92,18 @@ inject(() => {
|
|||
// Sends a message about the endpoint it spied on
|
||||
cloned.json()
|
||||
.then(jsonData => {
|
||||
const endpointText = endpointName?.split('?')[0].split('/').filter(s => s && isNaN(Number(s))).filter(s => s && !s.includes('.')).pop();
|
||||
|
||||
// Cache the latest /me payload so the userscript can hydrate its UI
|
||||
// even if the first response arrives before listeners are attached.
|
||||
if (endpointText == 'me') {
|
||||
try {
|
||||
sessionStorage.setItem('bm-last-me', JSON.stringify(jsonData));
|
||||
} catch (error) {
|
||||
console.warn(`%c${name}%c: Failed to cache "/me" payload`, consoleStyle, '', error);
|
||||
}
|
||||
}
|
||||
|
||||
window.postMessage({
|
||||
source: 'blue-marble',
|
||||
endpoint: endpointName,
|
||||
|
|
@ -163,9 +175,23 @@ inject(() => {
|
|||
const cssOverlay = GM_getResourceText("CSS-BM-File");
|
||||
GM_addStyle(cssOverlay);
|
||||
|
||||
function appendFontStylesheet(href) {
|
||||
const stylesheetLink = document.createElement('link');
|
||||
stylesheetLink.href = href;
|
||||
stylesheetLink.rel = 'preload';
|
||||
stylesheetLink.as = 'style';
|
||||
stylesheetLink.onload = function () {
|
||||
this.onload = null;
|
||||
this.rel = 'stylesheet';
|
||||
};
|
||||
document.head?.appendChild(stylesheetLink);
|
||||
}
|
||||
|
||||
// Injection point for the Roboto Mono font file (only if this is the Standalone version)
|
||||
const robotoMonoInjectionPoint = 'robotoMonoInjectionPoint';
|
||||
|
||||
appendFontStylesheet('https://fonts.googleapis.com/css2?family=Michroma&family=Rajdhani:wght@400;500;600;700&display=swap');
|
||||
|
||||
// If the Roboto Mono injection point contains '@font-face'...
|
||||
if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
|
||||
// A very hacky way of doing truthy/falsy logic
|
||||
|
|
@ -176,15 +202,7 @@ if (!!(robotoMonoInjectionPoint.indexOf('@font-face') + 1)) {
|
|||
// Else, no Roboto Mono was found. We need to use a stylesheet.
|
||||
|
||||
// Imports the Roboto Mono font family as a stylesheet
|
||||
var stylesheetLink = document.createElement('link');
|
||||
stylesheetLink.href = 'https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap';
|
||||
stylesheetLink.rel = 'preload';
|
||||
stylesheetLink.as = 'style';
|
||||
stylesheetLink.onload = function () {
|
||||
this.onload = null;
|
||||
this.rel = 'stylesheet';
|
||||
};
|
||||
document.head?.appendChild(stylesheetLink);
|
||||
appendFontStylesheet('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
|
||||
}
|
||||
|
||||
const userSettings = JSON.parse(GM_getValue('bmUserSettings', '{}')); // Loads the user settings
|
||||
|
|
@ -204,7 +222,6 @@ templateManager.setSettingsManager(settingsManager); // Sets the settings manage
|
|||
|
||||
const storageTemplates = JSON.parse(GM_getValue('bmTemplates', '{}'));
|
||||
console.log(storageTemplates);
|
||||
templateManager.importJSON(storageTemplates); // Loads the templates
|
||||
|
||||
|
||||
console.log(userSettings);
|
||||
|
|
@ -236,13 +253,23 @@ if ((previousTelemetryVersion == undefined) || (previousTelemetryVersion > curre
|
|||
windowTelemetry.buildWindow(); // Asks the user if they want to enable telemetry
|
||||
}
|
||||
|
||||
windowMain.buildWindow(); // Builds the main Blue Marble window
|
||||
void initializeBlueMarble();
|
||||
|
||||
apiManager.spontaneousResponseListener(windowMain); // Reads spontaneous fetch responces
|
||||
async function initializeBlueMarble() {
|
||||
await templateManager.importJSON(storageTemplates); // Loads the templates
|
||||
|
||||
observeBlack(); // Observes the black palette color
|
||||
apiManager.spontaneousResponseListener(windowMain); // Reads spontaneous fetch responces
|
||||
|
||||
consoleLog(`%c${name}%c (${version}) userscript has loaded!`, 'color: cornflowerblue;', '');
|
||||
windowMain.buildWindow(); // Builds the main Blue Marble window
|
||||
windowMain.buildWindowFilter(); // Opens the Color Filter window automatically on page load
|
||||
|
||||
apiManager.applyCachedUserData(windowMain); // Hydrates the UI from the earliest cached /me response if it exists
|
||||
void apiManager.requestCurrentUserData(windowMain); // Ensures the main window gets current /me data even if startup missed it
|
||||
|
||||
observeBlack(); // Observes the black palette color
|
||||
|
||||
consoleLog(`%c${name}%c (${version}) userscript has loaded!`, 'color: cornflowerblue;', '');
|
||||
}
|
||||
|
||||
/** Observe the black color, and add the "Move" button.
|
||||
* @since 0.66.3
|
||||
|
|
|
|||
394
src/overlay.css
|
|
@ -15,32 +15,81 @@
|
|||
|
||||
/* The Blue Marble windows */
|
||||
.bm-window {
|
||||
--bm-surface-strong: rgba(9, 20, 42, 0.5);
|
||||
--bm-surface-soft: rgba(24, 41, 74, 0.28);
|
||||
--bm-surface-glass: rgba(255, 255, 255, 0.1);
|
||||
--bm-surface-glass-strong: rgba(255, 255, 255, 0.18);
|
||||
--bm-border-soft: rgba(255, 255, 255, 0.18);
|
||||
--bm-border-strong: rgba(163, 228, 255, 0.34);
|
||||
--bm-text-primary: rgba(17, 36, 66, 0.96);
|
||||
--bm-text-secondary: rgba(36, 57, 90, 0.84);
|
||||
--bm-accent-start: #baf6ff;
|
||||
--bm-accent-end: #81b6ff;
|
||||
--bm-accent-shadow: rgba(132, 182, 255, 0.22);
|
||||
--bm-font-body: "Rajdhani", "Segoe UI Variable Text", "Segoe UI", sans-serif;
|
||||
--bm-font-display: "Michroma", "Orbitron", "Segoe UI", sans-serif;
|
||||
--bm-font-mono: "Roboto Mono", "Rajdhani", "Courier New", monospace;
|
||||
position: fixed;
|
||||
background-color: rgba(21, 48, 99, 0.9);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 14% 12%, rgba(255, 255, 255, 0.24), transparent 18%),
|
||||
radial-gradient(circle at 86% 8%, rgba(186, 246, 255, 0.22), transparent 24%),
|
||||
radial-gradient(circle at 82% 84%, rgba(129, 182, 255, 0.18), transparent 28%),
|
||||
linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.06) 22%, rgba(105, 145, 212, 0.08) 54%, rgba(9, 20, 42, 0.18));
|
||||
color: var(--bm-text-primary);
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--bm-border-soft);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.05);
|
||||
z-index: 9000;
|
||||
transition: all 0.3s ease, transform 0s;
|
||||
transition:
|
||||
background 320ms ease,
|
||||
border-color 220ms ease,
|
||||
box-shadow 220ms ease,
|
||||
opacity 220ms ease,
|
||||
transform 0s,
|
||||
width 220ms ease,
|
||||
max-width 220ms ease,
|
||||
max-height 220ms ease;
|
||||
top: 75px;
|
||||
left: 60px;
|
||||
width: auto;
|
||||
max-height: fit-content;
|
||||
max-width: calc(100% - 135px);
|
||||
/* Font stack is as follows:
|
||||
* Highest Priority (Roboto Mono)
|
||||
* Windows fallback (Courier New)
|
||||
* macOS fallback (Monaco)
|
||||
* Linux fallback (DejaVu Sans Mono)
|
||||
* Any possible monospace font (monospace)
|
||||
* Last resort (Arial) */
|
||||
font-family: 'Roboto Mono', 'Courier New', 'Monaco', 'DejaVu Sans Mono', monospace, 'Arial';
|
||||
letter-spacing: 0.05em;
|
||||
backdrop-filter: blur(26px) saturate(1.25);
|
||||
font-family: var(--bm-font-body);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* The Blue Marble windowed windows */
|
||||
.bm-window.bm-windowed {
|
||||
max-width: 300px;
|
||||
.bm-window::before,
|
||||
.bm-window::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bm-window::before {
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.12) 24%, rgba(186, 246, 255, 0.22) 58%, rgba(129, 182, 255, 0.3));
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.bm-window::after {
|
||||
border-radius: inherit;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent 24%),
|
||||
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.22), transparent 22%),
|
||||
radial-gradient(circle at 88% 16%, rgba(186, 246, 255, 0.18), transparent 18%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* The drag bar */
|
||||
|
|
@ -48,12 +97,19 @@
|
|||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
/* For background circles, width & height should be odd, cx & cy should be half of width & height, and r should be less than or equal to cx & cy */
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue"/></svg>') repeat;
|
||||
gap: 0.28ch;
|
||||
padding: 0.18rem 0.24rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
background:
|
||||
radial-gradient(circle at 0 0, rgba(255, 255, 255, 0.22) 0, transparent 42%),
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
|
||||
cursor: grab;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.16),
|
||||
0 6px 18px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* When a window is being dragged */
|
||||
|
|
@ -78,37 +134,42 @@
|
|||
/* The Blue Marble Favicon */
|
||||
.bm-favicon {
|
||||
display: inline-block;
|
||||
height: 2.5em;
|
||||
margin-right: 1ch;
|
||||
height: 2.2em;
|
||||
margin-right: 0.45ch;
|
||||
padding: 0.2rem;
|
||||
border-radius: 12px;
|
||||
vertical-align: middle;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.08));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.16),
|
||||
0 8px 18px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* Header 1 */
|
||||
.bm-window h1 {
|
||||
display: inline-block;
|
||||
font-size: x-large;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
vertical-align: middle;
|
||||
font-family: var(--bm-font-display);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(16, 33, 60, 0.96);
|
||||
}
|
||||
|
||||
/* Header 1 when inside dragbar */
|
||||
/* Or, when the custom class is used */
|
||||
.bm-dragbar h1,
|
||||
.bm-dragbar-text {
|
||||
font-size: 1.2em;
|
||||
font-size: 0.78rem;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow:
|
||||
3px 0px rgba(21, 48, 99, 0.5),
|
||||
-3px 0px rgba(21, 48, 99, 0.5),
|
||||
0px 3px rgba(21, 48, 99, 0.5),
|
||||
0px -3px rgba(21, 48, 99, 0.5),
|
||||
3px 3px rgba(21, 48, 99, 0.5),
|
||||
-3px 3px rgba(21, 48, 99, 0.5),
|
||||
3px -3px rgba(21, 48, 99, 0.5),
|
||||
-3px -3px rgba(21, 48, 99, 0.5);
|
||||
font-family: var(--bm-font-display);
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(18, 37, 66, 0.95);
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
|
||||
/* Container for Header 1 when inside dragbar */
|
||||
|
|
@ -119,9 +180,12 @@
|
|||
/* Header 2 */
|
||||
.bm-window h2 {
|
||||
display: inline-block;
|
||||
font-size: larger;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 700;
|
||||
vertical-align: middle;
|
||||
font-family: var(--bm-font-display);
|
||||
letter-spacing: 0.1em;
|
||||
color: rgba(18, 35, 64, 0.96);
|
||||
}
|
||||
|
||||
/* Header 3 */
|
||||
|
|
@ -129,6 +193,24 @@
|
|||
display: inline-block;
|
||||
font-size: large;
|
||||
font-weight: 700;
|
||||
font-family: var(--bm-font-display);
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(18, 35, 64, 0.96);
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
.bm-window p {
|
||||
color: var(--bm-text-secondary);
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.035em;
|
||||
}
|
||||
|
||||
/* Horizontal dividers */
|
||||
.bm-window hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 0.32rem 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
|
||||
}
|
||||
|
||||
/* Container with a vertically centered header 1-6 */
|
||||
|
|
@ -140,42 +222,162 @@
|
|||
|
||||
/* Containers for "sections" of elements */
|
||||
.bm-container {
|
||||
margin: 0.5em 0;
|
||||
margin: 0.24em 0;
|
||||
}
|
||||
|
||||
/* Shared form controls */
|
||||
.bm-window input,
|
||||
.bm-window select,
|
||||
.bm-window textarea,
|
||||
.bm-window button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* All window buttons */
|
||||
.bm-window button {
|
||||
background-color: #144eb9;
|
||||
border-radius: 1em;
|
||||
padding: 0 0.75ch;
|
||||
appearance: none;
|
||||
color: var(--bm-text-primary);
|
||||
font-family: var(--bm-font-display);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(164, 208, 255, 0.14));
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
padding: 0.28em 0.62em;
|
||||
min-height: 1.78em;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22),
|
||||
0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
transition:
|
||||
background 180ms ease,
|
||||
border-color 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
filter 180ms ease,
|
||||
opacity 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
/* All window buttons when hovered/focused */
|
||||
.bm-window button:hover, .bm-window button:focus-visible {
|
||||
background-color: #1061e5;
|
||||
.bm-window button:hover,
|
||||
.bm-window button:focus-visible {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.28), rgba(186, 246, 255, 0.18));
|
||||
border-color: rgba(230, 245, 255, 0.36);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.26),
|
||||
0 10px 22px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* All window buttons when pressed (plus disabled color) */
|
||||
.bm-window button:active,
|
||||
.bm-window button:disabled {
|
||||
background-color: #2e97ff;
|
||||
/* Focus ring */
|
||||
.bm-window button:focus-visible,
|
||||
.bm-window input:focus-visible,
|
||||
.bm-window select:focus-visible,
|
||||
.bm-window textarea:focus-visible {
|
||||
outline: none;
|
||||
border-color: rgba(116, 231, 255, 0.62);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(116, 231, 255, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* All window buttons when pressed */
|
||||
.bm-window button:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
filter: brightness(0.98);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
0 8px 16px rgba(18, 36, 78, 0.24);
|
||||
}
|
||||
|
||||
/* All window buttons when disabled */
|
||||
.bm-window button:disabled, .bm-window button:disabled {
|
||||
text-decoration: line-through;
|
||||
.bm-window button:disabled {
|
||||
opacity: 0.56;
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
transform: none;
|
||||
filter: saturate(0.72);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Button variants */
|
||||
.bm-window button.bm-button-primary {
|
||||
color: #07203b;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.42), rgba(186, 246, 255, 0.34));
|
||||
border-color: rgba(239, 248, 255, 0.42);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.44),
|
||||
0 10px 22px rgba(129, 182, 255, 0.14);
|
||||
}
|
||||
|
||||
.bm-window button.bm-button-primary:hover,
|
||||
.bm-window button.bm-button-primary:focus-visible {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.52), rgba(204, 250, 255, 0.36));
|
||||
}
|
||||
|
||||
.bm-window button.bm-button-secondary {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08));
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.14),
|
||||
0 8px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.bm-window button.bm-button-secondary:hover,
|
||||
.bm-window button.bm-button-secondary:focus-visible {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(186, 246, 255, 0.12));
|
||||
}
|
||||
|
||||
/* Icon buttons (single character text content buttons) */
|
||||
.bm-button-circle {
|
||||
border: white 1px solid;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
margin-top: 2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
inline-size: 1.62rem;
|
||||
block-size: 1.62rem;
|
||||
min-height: 1.62rem !important;
|
||||
min-width: 1.62rem;
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
padding: 0 !important; /* Overrides the padding in ".bm-window button" */
|
||||
line-height: 1;
|
||||
padding: 0 !important;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 50% !important;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.1)) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||
0 8px 18px rgba(0, 0, 0, 0.1) !important;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.bm-button-circle svg {
|
||||
display: block;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
flex: 0 0 auto;
|
||||
margin: auto;
|
||||
color: currentColor;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.bm-button-circle .bm-button-icon-settings {
|
||||
width: 68%;
|
||||
height: 68%;
|
||||
transform: translateY(0.5px) translateZ(0);
|
||||
}
|
||||
|
||||
.bm-button-circle .bm-button-icon-minimize {
|
||||
width: 62%;
|
||||
height: 62%;
|
||||
transform: translateY(0.75px) translateZ(0);
|
||||
}
|
||||
|
||||
#bm-window-filter.bm-windowed .bm-button-circle .bm-button-icon-minimize {
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Pin button */
|
||||
|
|
@ -192,39 +394,52 @@
|
|||
|
||||
/* Transparent buttons */
|
||||
.bm-window button.bm-button-trans {
|
||||
background-color: unset;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Transparent buttons on dark backgrounds when hovered */
|
||||
.bm-button-trans.bm-button-hover-white:hover,
|
||||
.bm-button-trans.bm-button-hover-white:focus {
|
||||
background-color: rgba(255, 255, 255, 0.17);
|
||||
background-color: rgba(255, 255, 255, 0.18) !important;
|
||||
}
|
||||
|
||||
/* Transparent buttons on dark backgrounds when pressed */
|
||||
.bm-button-trans.bm-button-hover-white:active {
|
||||
background-color: rgba(255, 255, 255, 0.22);
|
||||
background-color: rgba(255, 255, 255, 0.24) !important;
|
||||
}
|
||||
|
||||
/* Transparent buttons on light backgrounds when hovered */
|
||||
.bm-button-trans.bm-button-hover-black:hover,
|
||||
.bm-button-trans.bm-button-hover-black:focus {
|
||||
background-color: rgba(0, 0, 0, 0.17);
|
||||
background-color: rgba(0, 0, 0, 0.14) !important;
|
||||
}
|
||||
|
||||
/* Transparent buttons on light backgrounds when pressed */
|
||||
.bm-button-trans.bm-button-hover-black:active {
|
||||
background-color: rgba(0, 0, 0, 0.22);
|
||||
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Shared inputs */
|
||||
.bm-window input[type="number"],
|
||||
.bm-window select,
|
||||
.bm-window textarea {
|
||||
color: var(--bm-text-primary);
|
||||
font-family: var(--bm-font-body);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.06));
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Tile (x, y) & Pixel (x, y) input fields */
|
||||
input[type="number"].bm-input-coords {
|
||||
appearance: auto;
|
||||
-moz-appearance: textfield;
|
||||
width: 5.5ch;
|
||||
width: 5.9ch;
|
||||
margin-left: 1ch;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 0 0.5ch;
|
||||
padding: 0.2em 0.35ch;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
|
|
@ -260,10 +475,7 @@ input[type="file"] {
|
|||
|
||||
/* Dropdown selection & dropdown (::picker) menus */
|
||||
.bm-window select {
|
||||
color: white;
|
||||
background-color: #144eb9;
|
||||
border-radius: 1em;
|
||||
padding: 0 0.5ch;
|
||||
padding: 0.22em 0.45ch;
|
||||
}
|
||||
|
||||
/* Checkbox container (the label element) */
|
||||
|
|
@ -271,37 +483,51 @@ input[type="file"] {
|
|||
display: flex;
|
||||
width: fit-content;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
color: var(--bm-text-secondary);
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
.bm-window input[type="checkbox"] {
|
||||
width: 1em;
|
||||
accent-color: #74e7ff;
|
||||
}
|
||||
|
||||
/* Window content container */
|
||||
.bm-window-content {
|
||||
overflow: hidden;
|
||||
max-height: calc(100% - 5px);
|
||||
transition: height 300ms cubic-bezier(.4, 0, .2, 1);
|
||||
}
|
||||
|
||||
/* Text areas */
|
||||
.bm-window textarea {
|
||||
font-size: small;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 0 0.5ch;
|
||||
height: 5.25em;
|
||||
padding: 0.38em 0.52em;
|
||||
height: 4em;
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.bm-window textarea::placeholder,
|
||||
.bm-window input::placeholder {
|
||||
color: rgba(47, 68, 102, 0.6);
|
||||
}
|
||||
|
||||
/* Anchor/Links with no children */
|
||||
.bm-window a:not(:has(*)) {
|
||||
color: rgba(21, 88, 164, 0.94);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(21, 88, 164, 0.35);
|
||||
}
|
||||
|
||||
/* Small elements */
|
||||
.bm-window small {
|
||||
font-size: x-small;
|
||||
color: lightgray;
|
||||
font-family: var(--bm-font-display);
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--bm-text-secondary);
|
||||
}
|
||||
|
||||
/* List items of unordered lists */
|
||||
|
|
@ -312,8 +538,27 @@ 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;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(146, 221, 255, 0.42) rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, rgba(116, 231, 255, 0.48), rgba(83, 141, 255, 0.4));
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.bm-window .bm-container.bm-scrollable::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* Flex children space between */
|
||||
|
|
@ -322,7 +567,7 @@ input[type="file"] {
|
|||
align-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
gap: 0.35ch;
|
||||
}
|
||||
|
||||
/* Flex children space center */
|
||||
|
|
@ -331,7 +576,7 @@ input[type="file"] {
|
|||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5ch;
|
||||
gap: 0.35ch;
|
||||
}
|
||||
|
||||
/* ASCII Art */
|
||||
|
|
@ -348,9 +593,8 @@ input[type="file"] {
|
|||
/* Containers for "sections" of elements in windowed mode */
|
||||
/* Does not apply to the main window */
|
||||
.bm-windowed .bm-container:not(#bm-window-main .bm-container) {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
/* Do not use 'margin' shorthand, as it will override left/right margin */
|
||||
margin-top: 0.18em;
|
||||
margin-bottom: 0.18em;
|
||||
}
|
||||
|
||||
/* Header 1 in windowed mode */
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,55 @@ export default class TemplateManager {
|
|||
*/
|
||||
setSettingsManager(settingsManager) {
|
||||
this.settingsManager = settingsManager;
|
||||
this.#restoreFilteredColorsFromSettings();
|
||||
}
|
||||
|
||||
/** Restores hidden colors from persisted user settings.
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#restoreFilteredColorsFromSettings() {
|
||||
const storedFilter = this.settingsManager?.userSettings?.filter;
|
||||
const filteredColors = Array.isArray(storedFilter) ? storedFilter : [];
|
||||
|
||||
this.shouldFilterColor.clear();
|
||||
|
||||
for (const colorID of filteredColors) {
|
||||
const parsedColorID = Number(colorID);
|
||||
if (!Number.isFinite(parsedColorID)) {continue;}
|
||||
this.shouldFilterColor.set(parsedColorID, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Persists hidden colors to user settings storage.
|
||||
* @since 0.92.1
|
||||
*/
|
||||
#persistFilteredColors() {
|
||||
if (!this.settingsManager) {return;}
|
||||
|
||||
this.settingsManager.userSettings.filter = Array.from(this.shouldFilterColor.keys())
|
||||
.map(colorID => Number(colorID))
|
||||
.filter(colorID => Number.isFinite(colorID))
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
void this.settingsManager.saveUserStorageNow();
|
||||
}
|
||||
|
||||
/** Updates whether a palette color should be hidden on the canvas.
|
||||
* @param {number} colorID
|
||||
* @param {boolean} shouldHide
|
||||
* @since 0.92.1
|
||||
*/
|
||||
setColorFiltered(colorID, shouldHide) {
|
||||
const parsedColorID = Number(colorID);
|
||||
if (!Number.isFinite(parsedColorID)) {return;}
|
||||
|
||||
if (shouldHide) {
|
||||
this.shouldFilterColor.set(parsedColorID, true);
|
||||
} else {
|
||||
this.shouldFilterColor.delete(parsedColorID);
|
||||
}
|
||||
|
||||
this.#persistFilteredColors();
|
||||
}
|
||||
|
||||
/** Creates the JSON object to store templates in
|
||||
|
|
@ -648,14 +697,14 @@ export default class TemplateManager {
|
|||
/** Imports the JSON object, and appends it to any JSON object already loaded
|
||||
* @param {string} json - The JSON string to parse
|
||||
*/
|
||||
importJSON(json) {
|
||||
async importJSON(json) {
|
||||
|
||||
console.log(`Importing JSON...`);
|
||||
console.log(json);
|
||||
|
||||
// If the passed in JSON is a Blue Marble template object...
|
||||
if (json?.whoami == 'BlueMarble') {
|
||||
this.#parseBlueMarble(json); // ...parse the template object as Blue Marble
|
||||
await this.#parseBlueMarble(json); // ...parse the template object as Blue Marble
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||