Add website's pixel count tracker
This commit is contained in:
@@ -38,6 +38,9 @@ async function main(dev) {
|
||||
const scriptInput = path.join(process.cwd(), "src", "index.ts");
|
||||
const scriptOutput = path.join(buildVersionDir, "assets", "scripts", "content.js");
|
||||
|
||||
const injectScriptInput = path.join(process.cwd(), "src", "inject.ts");
|
||||
const injectScriptOutput = path.join(buildVersionDir, "assets", "scripts", "inject.js");
|
||||
|
||||
deleteFolder(distDir);
|
||||
ensureFolder(buildDir);
|
||||
ensureFolder(distDir);
|
||||
@@ -52,6 +55,19 @@ async function main(dev) {
|
||||
platform: "browser",
|
||||
}
|
||||
|
||||
await esbuild.build({
|
||||
...common,
|
||||
entryPoints: [injectScriptInput],
|
||||
sourcemap: !dev,
|
||||
define: {
|
||||
DEV: JSON.stringify(dev),
|
||||
ENVIRONMENT: JSON.stringify("user-script")
|
||||
},
|
||||
outfile: injectScriptOutput,
|
||||
});
|
||||
|
||||
const raw = JSON.stringify(fs.readFileSync(injectScriptOutput, "utf-8"));
|
||||
const injectCode = raw.substring(1, raw.length - 1);
|
||||
await esbuild.build({
|
||||
...common,
|
||||
sourcemap: !dev,
|
||||
@@ -75,6 +91,9 @@ async function main(dev) {
|
||||
outfile: userScriptOutput,
|
||||
});
|
||||
|
||||
[scriptOutput, userScriptOutput].forEach(path => {
|
||||
fs.writeFileSync(path, fs.readFileSync(path, "utf-8").replace("/* CROSS_WORLD_INJECT_CODE */", injectCode));
|
||||
});
|
||||
const code = fs.readFileSync(userScriptOutput, "utf-8");
|
||||
let template = fs.readFileSync(userScriptTemplateLocation, "utf-8");
|
||||
const date = new Date();
|
||||
|
||||
Vendored
+798
-37046
File diff suppressed because one or more lines are too long
@@ -6,7 +6,12 @@
|
||||
"48": "assets/images/icon_48.png",
|
||||
"128": "assets/images/icon_128.png"
|
||||
},
|
||||
"web_accessible_resources": [],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["assets/scripts/inject.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pixelcanvas-overlay",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"description": "Pixel canvas overlay",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -9,11 +9,12 @@ import { SelectedMural } from "../../interfaces";
|
||||
import { Minimap } from "./minimap";
|
||||
import { Overlay } from "./overlay";
|
||||
import { debounce } from "lodash";
|
||||
import { TCLogo } from "../../constants";
|
||||
import { TC_LOGO } from "../../constants";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Img = styled.img`
|
||||
width: 25px;
|
||||
margin-right: 5px;
|
||||
display: inline;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
@@ -98,7 +99,7 @@ export class Main extends React.Component<Props, State> {
|
||||
}
|
||||
title() {
|
||||
return <span title={"Terncode's fork"}>
|
||||
<Img src={TCLogo} alt="TC-Logo"/>
|
||||
<Img src={TC_LOGO} alt="TC-Logo"/>
|
||||
<span>Overlay</span>
|
||||
</span>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { PixelPlaced } from "../../lib/pixelPlaced";
|
||||
import styled from "styled-components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatNumber } from "../../lib/utils";
|
||||
|
||||
const CountBox = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
bottom: 107px;
|
||||
right: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.75);
|
||||
border: 2px solid black;
|
||||
border-radius: 12px;
|
||||
margin: 5px auto;
|
||||
padding: 5px;
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
const Count = styled.span`
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const Icon = styled.span`
|
||||
margin: 2px 5px;
|
||||
width: 19px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
pixelCount: PixelPlaced;
|
||||
}
|
||||
|
||||
interface State {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class PixelCount extends React.Component<Props, State> {
|
||||
|
||||
constructor(props:Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
count: 0
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.pixelCount.count.then(countMaybe => {
|
||||
if (typeof countMaybe === "number") {
|
||||
this.updateCount(countMaybe);
|
||||
}
|
||||
});
|
||||
this.props.pixelCount.on(this.updateCount);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.pixelCount.on(this.updateCount);
|
||||
}
|
||||
updateCount = (count: number) => {
|
||||
this.setState({count});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.state.count) return null;
|
||||
|
||||
return (
|
||||
<CountBox>
|
||||
<Count>{formatNumber(this.state.count)}</Count>
|
||||
<Icon><FontAwesomeIcon icon={ faSquare }></FontAwesomeIcon></Icon>
|
||||
</CountBox>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+10
-201
@@ -1,8 +1,3 @@
|
||||
import { Mural } from "../interfaces";
|
||||
import { canvasToImageData, createCanvas, flatQuantizeImageData,
|
||||
getExtension, getMuralHeight, getMuralWidth, imageDataToPaletteIndices,
|
||||
imageToCanvas, loadImageSource, readAsDataUrl, readAsString, resize, validateMural } from "../lib/utils";
|
||||
import { FileInput } from "../lib/fileinput";
|
||||
import { Palette } from "../lib/palette";
|
||||
import { Store } from "../lib/store";
|
||||
import React from "react";
|
||||
@@ -10,11 +5,17 @@ import { Coordinates } from "../lib/coordinates";
|
||||
import { Storage } from "../lib/storage";
|
||||
import { Main } from "./components/main";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { PixelPlaced } from "../lib/pixelPlaced";
|
||||
import { PixelCount } from "./components/pixelCount";
|
||||
|
||||
const TEXT_FORMATS = ["muraljson", "json"];
|
||||
|
||||
export function createUI(store: Store, storage: Storage, cords: Coordinates, palette: Palette) {
|
||||
return appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette} />);
|
||||
export function createUI(store: Store, storage: Storage, cords: Coordinates, palette: Palette, pixels: PixelPlaced) {
|
||||
const unmounts = [
|
||||
appendWindow(<PixelCount pixelCount={pixels} />),
|
||||
appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette}/>),
|
||||
];
|
||||
return () => {
|
||||
return unmounts.map(e => e());
|
||||
};
|
||||
}
|
||||
|
||||
function appendWindow(children: React.ReactNode) {
|
||||
@@ -25,195 +26,3 @@ function appendWindow(children: React.ReactNode) {
|
||||
rootElement.parentElement?.removeChild(rootElement);
|
||||
};
|
||||
}
|
||||
|
||||
export class UI {
|
||||
private container: HTMLDivElement;
|
||||
private list: HTMLDivElement;
|
||||
|
||||
constructor(private store: Store, private palette: Palette) {
|
||||
this.container = document.createElement("div");
|
||||
const s = this.container.style;
|
||||
this.container.setAttribute("mod", "pixel-canvas-overlay");
|
||||
s.position = "fixed";
|
||||
s.zIndex = "1000";
|
||||
s.left = "0px";
|
||||
s.bottom = "0px";
|
||||
const container = document.createElement("div");
|
||||
const ss = container.style;
|
||||
this.container.appendChild(container);
|
||||
ss.backgroundColor = "black";
|
||||
ss.color = "white";
|
||||
ss.margin = "5px";
|
||||
ss.padding = "5px";
|
||||
ss.border = "1px solid white";
|
||||
|
||||
const h = document.createElement("h3");
|
||||
h.textContent = "Pixel Canvas overlay";
|
||||
|
||||
const input = this.btn("Import", async () => {
|
||||
const mural = await this.import();
|
||||
if (mural) {
|
||||
this.store.add(mural);
|
||||
this.renderList();
|
||||
}
|
||||
});
|
||||
container.appendChild(h);
|
||||
container.appendChild(input);
|
||||
this.list = document.createElement("div");
|
||||
this.list.style.display = "flex";
|
||||
this.list.style.flexDirection = "column";
|
||||
container.appendChild(this.list);
|
||||
}
|
||||
|
||||
private btn(value: string, onClick: () => void) {
|
||||
const button = document.createElement("button");
|
||||
button.style.backgroundColor = "black";
|
||||
button.style.color = "white";
|
||||
button.style.border = "1px solid white";
|
||||
button.style.padding = "2px";
|
||||
button.textContent = value;
|
||||
button.addEventListener("click", onClick);
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
import = async () => {
|
||||
const file = await this.importFile();
|
||||
if (file) {
|
||||
if (file.type === "mural") {
|
||||
return file.data as Mural;
|
||||
} else {
|
||||
const pixels = await this.imageToMural(file.data as HTMLImageElement);
|
||||
await new Promise<Mural>((resolve, reject)=> {
|
||||
this.store.setOverlayModify({
|
||||
pixels,
|
||||
cb: (name, x, y, confirm) => {
|
||||
if (confirm) {
|
||||
resolve({ name, pixels, x, y });
|
||||
} else {
|
||||
reject(new Error("Canceled by user"));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
const heights = prompt(`Enter name and location [name, x, y]`) || "";
|
||||
const data = heights.split(",");
|
||||
const numbers = [data[1], data[2]].map(e=> parseInt(e.trim(), 10));
|
||||
if (typeof numbers[1] === "number" && typeof numbers[2] === "number") {
|
||||
alert("Invalid size");
|
||||
}
|
||||
const mural: Mural = {
|
||||
name: data[0],
|
||||
x: numbers[0],
|
||||
y: numbers[1],
|
||||
pixels
|
||||
};
|
||||
try {
|
||||
validateMural(mural);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
return mural;
|
||||
}
|
||||
}
|
||||
};
|
||||
private async importFile() {
|
||||
const fileInput = new FileInput();
|
||||
fileInput.setAcceptType(["png", "jpg", "jpeg", ...TEXT_FORMATS]);
|
||||
const files = await fileInput.show();
|
||||
const fileData = files[0];
|
||||
if (!fileData) {
|
||||
throw new Error("Empty file");
|
||||
}
|
||||
const ex = getExtension(fileData.name);
|
||||
const name = ex.text;
|
||||
if (TEXT_FORMATS.includes(ex.ex)) {
|
||||
const content = await readAsString(fileData);
|
||||
const mural = JSON.parse(content) as Mural;
|
||||
if (!mural.name) {
|
||||
mural.name = prompt("Missing name for this mural. Please enter it manually", name) || "";
|
||||
}
|
||||
validateMural(mural);
|
||||
return {
|
||||
type: "mural",
|
||||
data: mural,
|
||||
};
|
||||
} else {
|
||||
const readData = await readAsDataUrl(fileData);
|
||||
return {
|
||||
type: "image",
|
||||
data: await loadImageSource(`${ex.text}.${ex.ex}`, readData),
|
||||
};
|
||||
}
|
||||
}
|
||||
private async imageToMural(image: HTMLImageElement) {
|
||||
const actualImage = await this.getImageSettingFromUser(image);
|
||||
const canvas = imageToCanvas(actualImage);
|
||||
const imageData = canvasToImageData(canvas);
|
||||
flatQuantizeImageData(imageData, this.palette.palette);
|
||||
return imageDataToPaletteIndices(imageData, this.palette.palette);
|
||||
}
|
||||
private async getImageSettingFromUser(image: HTMLImageElement) {
|
||||
const should = confirm(`You are importing image with size ${image.width}x${image.height}. Do you want to preform any image manipulations?`);
|
||||
if (should) {
|
||||
if (confirm(`Do you want to resize?\n Image ${image.width}x${image.height}?`)) {
|
||||
const heights = prompt(`Enter new [width x height]`) || "";
|
||||
const numbers = heights.split("x").map(e=> parseInt(e.trim(), 10));
|
||||
if (typeof numbers[0] === "number" && typeof numbers[1] === "number" && numbers[0] > 0 && numbers[1] > 0) {
|
||||
alert("Invalid size");
|
||||
return image;
|
||||
}
|
||||
image = await resize(image, numbers[0], numbers[1]);
|
||||
}
|
||||
}
|
||||
return image;
|
||||
}
|
||||
private renderList() {
|
||||
while (this.list.children.length) {
|
||||
this.list.removeChild(this.list.children[0]);
|
||||
}
|
||||
|
||||
for (const mural of this.store.murals) {
|
||||
const container = document.createElement("div");
|
||||
container.style.border = `${this.store.selected?.m === mural ? 3 : 1}px solid white`;
|
||||
container.style.padding = "2px";
|
||||
const h5 = document.createElement("h5");
|
||||
h5.textContent = mural.name;
|
||||
const canvas = createCanvas();
|
||||
canvas.style.maxHeight = "100px";
|
||||
canvas.style.maxWidth = "100px";
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
canvas.width = getMuralWidth(mural);
|
||||
canvas.height = getMuralHeight(mural);
|
||||
ctx.drawImage(mural.ref, 0, 0, canvas.width, canvas.height);
|
||||
const wh = document.createElement("div");
|
||||
wh.textContent = `Size ${getMuralWidth(mural)}x${getMuralHeight(mural)}`;
|
||||
const at = document.createElement("div");
|
||||
at.textContent = `At: ${mural.x}x${mural.y}`;
|
||||
container.appendChild(h5);
|
||||
container.appendChild(wh);
|
||||
container.appendChild(at);
|
||||
container.appendChild(canvas);
|
||||
const select = this.btn("select", () => {
|
||||
this.store.select(mural);
|
||||
this.renderList();
|
||||
});
|
||||
const del = this.btn("delete", () => {
|
||||
this.store.remove(mural);
|
||||
this.renderList();
|
||||
});
|
||||
container.appendChild(select);
|
||||
container.appendChild(del);
|
||||
this.list.appendChild(container);
|
||||
}
|
||||
}
|
||||
async append() {
|
||||
document.body.appendChild(this.container);
|
||||
this.renderList();
|
||||
}
|
||||
destroy() {
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
+5
-2
@@ -5,19 +5,22 @@ import { createUI } from "./UI/ui";
|
||||
import { waitForDraw } from "./lib/utils";
|
||||
import { Storage } from "./lib/storage";
|
||||
import { Store } from "./lib/store";
|
||||
import { PixelPlaced } from "./lib/pixelPlaced";
|
||||
|
||||
async function main() {
|
||||
const palette = new Palette();
|
||||
while(!palette.init()) { await waitForDraw();}
|
||||
const storage = new Storage(ENVIRONMENT === "browser-extension");
|
||||
const pixelPlaced = PixelPlaced.create(storage);
|
||||
const store = new Store(storage, palette);
|
||||
await store.load();
|
||||
const coordinates = new Coordinates();
|
||||
await coordinates.init();
|
||||
|
||||
createUI(store,storage, coordinates, palette);
|
||||
createUI(store,storage, coordinates, palette, pixelPlaced);
|
||||
|
||||
console.log("%cOverlay by 0xa663 loaded", "color: red; font-size: 20px; font-weight: bold; text-shadow: 2px 2px 4px #000000;");
|
||||
console.log("%cOverlay by 0xa663", "color: red; font-size: 15px; font-weight: bold; text-shadow: 2px 2px 4px #000000;");
|
||||
console.log("%cTerncode's fork", `color: #4a94cc; font-size: 10px; font-weight: bold; text-shadow: 1px 1px 0px #000000, -1px 1px 0px #000000, 1px -1px 0px #000000, -1px -1px 0px #000000;`);
|
||||
}
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Creating artificial API between two worlds
|
||||
|
||||
const log = console.log;
|
||||
console.log = (...args: any) => {
|
||||
const firstArg = args[0];
|
||||
if (firstArg && typeof firstArg === "object" && "pixelsPlaced" in firstArg) {
|
||||
const pixelsPlaced = new CustomEvent("__pixelsPlaced", {
|
||||
detail: firstArg.pixelsPlaced,
|
||||
});
|
||||
document.dispatchEvent(pixelsPlaced);
|
||||
}
|
||||
log(...args);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants";
|
||||
import { BasicEventEmitter } from "./eventEmitter";
|
||||
import { Storage } from "./storage";
|
||||
|
||||
|
||||
export class PixelPlaced {
|
||||
private emitter = new BasicEventEmitter();
|
||||
private static instance: PixelPlaced;
|
||||
private readonly storageKey = "__pixelsPlaced";
|
||||
private constructor(private storage: Storage) {
|
||||
// The website is logging pixels as object onto pixelsPlaced onto console with .log.
|
||||
// With extension we need to cross 2 worlds. Adding artificial api to document
|
||||
|
||||
const api = document.createElement("script");
|
||||
api.addEventListener("load", () => {
|
||||
document.body.removeChild(api);
|
||||
}, { once: true });
|
||||
api.addEventListener("error", error => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
if (ENVIRONMENT === "browser-extension") {
|
||||
api.src = chrome.runtime.getURL("/assets/scripts/inject.js");
|
||||
} else {
|
||||
api.textContent = INJECT_SCRIPT_PIXEL_OBSERVER;
|
||||
}
|
||||
|
||||
document.body.appendChild(api);
|
||||
(document as any).addEventListener("__pixelsPlaced", (event: CustomEvent<number>) => {
|
||||
if (typeof event.detail === "number") {
|
||||
this.storage.setItem(this.storageKey, event.detail);
|
||||
this.emitter.emit(0, event.detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static create(storage: Storage) {
|
||||
if (!this.instance) {
|
||||
this.instance = new PixelPlaced(storage);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
on(fn: (count: number) => void) {
|
||||
this.emitter.on(0, fn);
|
||||
}
|
||||
off(fn: (count: number) => void) {
|
||||
this.emitter.off(0, fn);
|
||||
}
|
||||
get count() {
|
||||
return this.storage.getItem(this.storageKey);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user