Add dedicated injector
This commit is contained in:
Vendored
+77225
-904
File diff suppressed because one or more lines are too long
@@ -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;
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+6
-2
File diff suppressed because one or more lines are too long
+4
-3
@@ -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
@@ -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));
|
||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user