Add dedicated injector

This commit is contained in:
2025-02-13 17:34:28 +01:00
parent f2e59cfda3
commit cdc69bd932
15 changed files with 77384 additions and 1070 deletions
+77211 -890
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,10 +8,10 @@ import { MovableWindow } from "./movableWindow";
import { Minimap } from "./minimap"; import { Minimap } from "./minimap";
import { Overlay } from "./overlay"; import { Overlay } from "./overlay";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { TC_LOGO } from "../../constants";
import styled from "styled-components"; import styled from "styled-components";
import { MuralEx } from "../../interfaces"; import { MuralEx } from "../../interfaces";
import { PieCharts } from "./pixelCharts"; import { PieCharts } from "./pixelCharts";
import { TC_LOGO } from "../../assets";
const Img = styled.img` const Img = styled.img`
width: 25px; width: 25px;
+5 -6
View File
@@ -1,10 +1,9 @@
import React from "react"; import React from "react";
import { PixelPlaced } from "../../lib/pixelPlaced";
import styled from "styled-components"; import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSquare } from "@fortawesome/free-solid-svg-icons"; import { faSquare } from "@fortawesome/free-solid-svg-icons";
import { formatNumber } from "../../lib/utils"; import { formatNumber } from "../../lib/utils";
import { StoreEvents } from "../../lib/store"; import { Store, StoreEvents } from "../../lib/store";
const CountBox = styled.div<{width: number}>` const CountBox = styled.div<{width: number}>`
position: fixed; position: fixed;
@@ -31,7 +30,7 @@ const Icon = styled.span`
`; `;
interface Props { interface Props {
pixelCount: PixelPlaced; store: Store;
} }
interface State { interface State {
@@ -50,15 +49,15 @@ export class PixelCount extends React.Component<Props, State> {
} }
componentDidMount() { componentDidMount() {
this.updateCount(); this.updateCount();
this.props.pixelCount.store.on(StoreEvents.PixelLog, this.updateCount); this.props.store.on(StoreEvents.PixelLog, this.updateCount);
window.addEventListener("resize", this.onResize); window.addEventListener("resize", this.onResize);
} }
componentWillUnmount() { componentWillUnmount() {
this.props.pixelCount.store.off(StoreEvents.PixelLog, this.updateCount); this.props.store.off(StoreEvents.PixelLog, this.updateCount);
window.removeEventListener("resize", this.onResize); window.removeEventListener("resize", this.onResize);
} }
updateCount = async () => { updateCount = async () => {
this.setState({count: await this.props.pixelCount.getCount()}); this.setState({count: await this.props.store.getPixelCount()});
}; };
onResize = () => { onResize = () => {
this.setState({ width: window.innerWidth }); this.setState({ width: window.innerWidth });
+5 -3
View File
@@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import { ImportOutput, MuralOld, RGB } from "../interfaces"; import { ImportOutput, MuralOld } from "../interfaces";
import { canvasToImageData, convertOldMuralToNewMural, createClosestIndexColor, createMuralExtended, getColorScore, getExtension, import {
canvasToImageData, convertOldMuralToNewMural,
createClosestIndexColor, createMuralExtended, getExtension,
imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent, imageDataToPaletteIndices, imageToCanvas, loadImageSource, processNumberEvent,
readAsArrayBuffer, readAsArrayBuffer,
readAsDataUrl, readAsString, resize, rgb } from "../lib/utils"; readAsDataUrl, readAsString, resize } from "../lib/utils";
import styled from "styled-components"; import styled from "styled-components";
import { Popup } from "./components/Popup"; import { Popup } from "./components/Popup";
import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant"; import RgbQuant, { DitheringKernel, RGBQuantOptions } from "rgbquant";
+2 -3
View File
@@ -5,14 +5,13 @@ import { Coordinates } from "../lib/coordinates";
import { Storage } from "../lib/storage"; import { Storage } from "../lib/storage";
import { Main } from "./components/main"; import { Main } from "./components/main";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { PixelPlaced } from "../lib/pixelPlaced";
import { PixelCount } from "./components/pixelCount"; import { PixelCount } from "./components/pixelCount";
export function createUI( export function createUI(
store: Store, storage: Storage, cords: Coordinates, palette: Palette, pixels: PixelPlaced store: Store, storage: Storage, cords: Coordinates, palette: Palette
) { ) {
const unmounts = [ const unmounts = [
appendWindow(<PixelCount pixelCount={pixels} />), appendWindow(<PixelCount store={store} />),
appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette}/>), appendWindow(<Main cords={cords} store={store} storage={storage} palette={palette}/>),
]; ];
return () => { return () => {
+1
View File
File diff suppressed because one or more lines are too long
+6 -2
View File
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -5,8 +5,8 @@ import { createUI } from "./UI/ui";
import { waitForDraw } from "./lib/utils"; import { waitForDraw } from "./lib/utils";
import { Storage } from "./lib/storage"; import { Storage } from "./lib/storage";
import { Store } from "./lib/store"; import { Store } from "./lib/store";
import { PixelPlaced } from "./lib/pixelPlaced";
import process from "process"; import process from "process";
import { Injector } from "./lib/injector";
async function main() { async function main() {
globalThis.process = process; globalThis.process = process;
@@ -16,11 +16,12 @@ async function main() {
const storage = new Storage(ENVIRONMENT === "browser-extension"); const storage = new Storage(ENVIRONMENT === "browser-extension");
const store = new Store(storage, palette); const store = new Store(storage, palette);
await store.load(); await store.load();
const pixelPlaced = PixelPlaced.create(store); const injector = new Injector(store);
await injector.inject();
const coordinates = new Coordinates(); const coordinates = new Coordinates();
await coordinates.init(); await coordinates.init();
createUI(store,storage, coordinates, palette, pixelPlaced); createUI(store,storage, coordinates, palette);
console.log( console.log(
"%cOverlay by 0xa663", "%cOverlay by 0xa663",
+13 -88
View File
@@ -1,99 +1,15 @@
// Creating artificial API between two worlds // Creating artificial API between two worlds
export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise<void> | void; import { EVENT_CROSS_WORLD_INJECTED, EVENT_CROSS_WORLD_PIXEL_PLACED, EVENT_CROSS_WORLD_URL_UPDATE } from "./constants";
export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise<void> | void; import { Interceptor } from "./lib/interceptor";
import { HistoryPushStateSpy } from "./lib/urlHistorySpy.ts";
export class Interceptor {
private static fetch = window.fetch;
private static overwritten = false;
private static before = new Map<string, InterceptBefore[]>();
private static after = new Map<string, InterceptAfter[]>();
private constructor() {}
private static overrideFetch() {
if (this.overwritten) {
return;
}
this.overwritten = true;
(window as any).fetch = async (...args: any) => {
const url = ((args[0] as RequestInfo | URL) || "").toString();
const init = args[1];
const befores = [...(this.before.get(url) || []), ...(this.before.get("*") || [])];
if (befores) {
for (const before of befores) {
await before(url, init);
}
}
const response = (this.fetch as any)(...args);
const afters = [...(this.after.get(url) || []), ...(this.after.get("*") || [])];
if (afters) {
const result = await response;
for (const after of afters) {
try {
await after(url, result, init);
} catch (_) {}
}
return response;
} else {
return response;
}
};
}
static onAfter(url: string, cb: InterceptAfter) {
Interceptor.overrideFetch();
const arr = Interceptor.after.get(url) || [];
if (!arr.includes(cb)) {
arr.push(cb);
}
Interceptor.after.set(url, arr);
}
static offAfter(url: string, cb: InterceptAfter) {
const arr = Interceptor.after.get(url) || [];
const index = arr.indexOf(cb);
if (index !== -1) {
arr.splice(index, 1);
}
if (arr.length) {
Interceptor.after.set(url, arr);
} else {
Interceptor.after.delete(url);
}
}
static onBefore(url: string, cb: InterceptBefore) {
Interceptor.overrideFetch();
const arr = Interceptor.before.get(url) || [];
if (!arr.includes(cb)) {
arr.push(cb);
}
Interceptor.before.set(url, arr);
}
static offBefore(url: string, cb: InterceptBefore) {
const arr = Interceptor.before.get(url) || [];
const index = arr.indexOf(cb);
if (index !== -1) {
arr.splice(index, 1);
}
if (arr.length) {
Interceptor.before.set(url, arr);
} else {
Interceptor.before.delete(url);
}
}
}
Interceptor.onAfter("/api/pixel", async (_, response, init) => { Interceptor.onAfter("/api/pixel", async (_, response, init) => {
try { try {
if (response.status === 200 && init && "body" in init) { if (response.status === 200 && init && "body" in init) {
const body = JSON.parse(init.body as any); const body = JSON.parse(init.body as any);
if ("x" in body && "y" in body && "color" in body) { if ("x" in body && "y" in body && "color" in body) {
const pixelsPlaced = new CustomEvent("__pixelsPlaced", { const pixelsPlaced = new CustomEvent(EVENT_CROSS_WORLD_PIXEL_PLACED, {
detail: { color: body.color, x: body.x, y: body.y }, detail: { color: body.color, x: body.x, y: body.y },
}); });
document.dispatchEvent(pixelsPlaced); document.dispatchEvent(pixelsPlaced);
@@ -103,3 +19,12 @@ Interceptor.onAfter("/api/pixel", async (_, response, init) => {
console.error(error); console.error(error);
} }
}); });
HistoryPushStateSpy.onPushState((_, __, url) => {
const pixelsPlaced = new CustomEvent(EVENT_CROSS_WORLD_URL_UPDATE, {
detail: url,
});
document.dispatchEvent(pixelsPlaced);
});
document.dispatchEvent(new CustomEvent(EVENT_CROSS_WORLD_INJECTED));
+2 -1
View File
@@ -1,9 +1,10 @@
import saveAs from "file-saver"; import saveAs from "file-saver";
import { createCanvas, CHUNK_SIZE, fetchTile } from "./utils"; import { createCanvas, fetchTile } from "./utils";
import { Coordinates } from "./coordinates"; import { Coordinates } from "./coordinates";
import { Rect } from "../interfaces"; import { Rect } from "../interfaces";
import { Popup } from "../UI/components/Popup"; import { Popup } from "../UI/components/Popup";
import React from "react"; import React from "react";
import { CHUNK_SIZE } from "../constants";
export function getSelectionArea() { export function getSelectionArea() {
return new Promise<Rect>(resolve => { return new Promise<Rect>(resolve => {
+2 -1
View File
@@ -1,5 +1,6 @@
import { CHUNK_SIZE } from "../constants";
import { Listener, BasicEventEmitter } from "./eventEmitter"; import { Listener, BasicEventEmitter } from "./eventEmitter";
import { CHUNK_SIZE, waitForDraw } from "./utils"; import { waitForDraw } from "./utils";
export enum CordType { export enum CordType {
Div, Div,
+82
View File
@@ -0,0 +1,82 @@
import {
EVENT_CROSS_WORLD_INJECTED,
EVENT_CROSS_WORLD_PIXEL_PLACED,
EVENT_CROSS_WORLD_URL_UPDATE,
INJECT_SCRIPT_PIXEL_OBSERVER
} from "../constants";
import { BasicEventEmitter } from "./eventEmitter";
import { Store } from "./store";
interface Pixel {
x: number;
y: number;
color: number;
}
export enum InjectEvents {
PixelPlaced = 1,
UrlChange
}
export class Injector {
private eventEmitter = new BasicEventEmitter();
constructor(private store: Store) {}
inject() {
const api = document.createElement("script");
api.addEventListener("load", () => {
document.body.removeChild(api);
}, { once: true });
api.addEventListener("error", error => {
console.error(error);
});
return new Promise<void>((resolve, reject) => {
const d = document as any;
api.addEventListener("error", (error) => {
reject(error.error);
}, { once: true });
d.addEventListener(EVENT_CROSS_WORLD_INJECTED, () => {
resolve();
}, { once: true});
if (ENVIRONMENT === "browser-extension") {
api.src = chrome.runtime.getURL("/assets/scripts/inject.js");
} else {
api.textContent = INJECT_SCRIPT_PIXEL_OBSERVER;
}
document.body.appendChild(api);
d.addEventListener(EVENT_CROSS_WORLD_PIXEL_PLACED, (event: CustomEvent<Pixel>) => {
const body = event.detail;
if (typeof body === "object" && "x" in body && "y" in body && "color" in body
&& typeof body.color === "number" && typeof body.x === "number"
&& typeof body.y === "number"
) {
this.store.addPixelLog(body.x, body.y, body.color);
this.eventEmitter.emit(InjectEvents.PixelPlaced, body);
}
});
d.addEventListener(EVENT_CROSS_WORLD_URL_UPDATE, (event: CustomEvent<string>) => {
const body = event.detail;
if (typeof body === "string") {
this.eventEmitter.emit(InjectEvents.UrlChange, body);
}
});
});
}
on(event: InjectEvents.UrlChange, cb: (pixel: Pixel) => any): this;
on(event: InjectEvents.UrlChange, cb: (url: string) => any): this;
on(event: InjectEvents, cb: any) {
this.eventEmitter.on(event, cb);
return this;
}
off(event: InjectEvents.UrlChange, cb: (pixel: Pixel) => any): this;
off(event: InjectEvents.UrlChange, cb: (url: string) => any): this;
off(event: InjectEvents, cb: any) {
this.eventEmitter.off(event, cb);
return this;
}
}
-56
View File
@@ -1,56 +0,0 @@
import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants";
import { BasicEventEmitter } from "./eventEmitter";
import { Store } from "./store";
interface Pixel {
x: number;
y: number;
color: number;
}
export class PixelPlaced {
private static instance: PixelPlaced;
private constructor(public readonly store: Store) {
// 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<Pixel>) => {
const body = event.detail;
console.log(body);
if (typeof body === "object" && "x" in body && "y" in body && "color" in body
&& typeof body.color === "number" && typeof body.x === "number"
&& typeof body.y === "number"
) {
const data = event.detail;
this.store.addPixelLog(data.x, data.y, data.color);
}
});
}
static create(store: Store) {
if (!this.instance) {
this.instance = new PixelPlaced(store);
}
return this.instance;
}
getCount() {
return this.store.getPixelCount();
}
}
+35
View File
@@ -0,0 +1,35 @@
export type SpyHistoryPushStateSpy = (data: any, unused: string, url?: string | URL | null) => Promise<void> | void;
export class HistoryPushStateSpy {
private static pushState = window.history.pushState;
private static overwritten = false;
private static listeners: SpyHistoryPushStateSpy[] = [];
private static overridePushState() {
if (this.overwritten) {
return;
}
this.overwritten = true;
const listeners = this.listeners;
const pushState = this.pushState;
(window as any).history.pushState = function (...args: any) {
for (const listener of listeners) {
(listener as any).apply(this, args);
}
return (pushState as any).apply(this, args);
};
}
static onPushState(cb: SpyHistoryPushStateSpy) {
this.overridePushState();
if (!this.listeners.includes(cb)) {
this.listeners.push(cb);
}
}
static offPushState(cb: SpyHistoryPushStateSpy) {
const index = this.listeners.indexOf(cb);
if (index !== -1) {
this.listeners.splice(index, 1);
}
}
}
+1 -2
View File
@@ -2,8 +2,7 @@ import { clone, isInteger } from "lodash";
import { MuralEx, MuralOld, MuralStatus, RGB } from "../interfaces"; import { MuralEx, MuralOld, MuralStatus, RGB } from "../interfaces";
import { fetchCombineTiledImage } from "./canvasShot"; import { fetchCombineTiledImage } from "./canvasShot";
import { Mural } from "./mural"; import { Mural } from "./mural";
import { CHUNK_SIZE } from "../constants";
export const CHUNK_SIZE = 512;
export function pushUnique<T>(items: T[], item: T) { export function pushUnique<T>(items: T[], item: T) {
const index = items.indexOf(item); const index = items.indexOf(item);