Add pixel statistics
This commit is contained in:
@@ -11,6 +11,7 @@ import { debounce } from "lodash";
|
||||
import { TC_LOGO } from "../../constants";
|
||||
import styled from "styled-components";
|
||||
import { MuralEx } from "../../interfaces";
|
||||
import { PieCharts } from "./pixelCharts";
|
||||
|
||||
const Img = styled.img`
|
||||
width: 25px;
|
||||
@@ -35,6 +36,7 @@ interface Props {
|
||||
interface State extends StoreSettings{
|
||||
selected?: MuralEx;
|
||||
overlays: number[];
|
||||
showCharts: boolean;
|
||||
phantomOverlay: number;
|
||||
overlayModify?: OverlayReturn;
|
||||
}
|
||||
@@ -46,6 +48,7 @@ export class Main extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showCharts: false,
|
||||
overlays: [],
|
||||
phantomOverlay: -1,
|
||||
opacity: 50,
|
||||
@@ -106,8 +109,14 @@ export class Main extends React.Component<Props, State> {
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<MovableWindow title={this.title()} storage={this.props.storage} storageKey="main" >
|
||||
{this.state.showCharts ?
|
||||
<MovableWindow title={"Pixel chart"} storage={this.props.storage} storageKey="charts">
|
||||
<PieCharts palette={this.props.palette} store={this.props.store}/>
|
||||
</MovableWindow>
|
||||
: null}
|
||||
<MovableWindow title={this.title()} storage={this.props.storage} storageKey="main">
|
||||
<Menu
|
||||
showChart={() => this.setState({showCharts: !this.state.showCharts})}
|
||||
onOpacityChange={this.opacityChange}
|
||||
cords={this.props.cords}
|
||||
store={this.props.store}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { faCamera, faCaretDown, faCaretUp, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCamera, faCaretDown, faCaretUp, faPieChart, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
@@ -76,6 +76,7 @@ interface Props {
|
||||
storage: Storage;
|
||||
opacity: number;
|
||||
collapsed: boolean;
|
||||
showChart: () => void;
|
||||
onOpacityChange: (n: number) => void;
|
||||
onCollapsedChanged: (b: boolean) => void;
|
||||
}
|
||||
@@ -130,13 +131,30 @@ export class Menu extends React.Component<Props, State> {
|
||||
this.setState({takingCanvasShot: false});
|
||||
};
|
||||
render() {
|
||||
return <Container>
|
||||
return<Container>
|
||||
<Flex>
|
||||
<Btn onClick={this.screenshot} disabled={this.state.takingCanvasShot || !this.state.canvasShotAvailable} title="Screenshot"><FontAwesomeIcon icon={ faCamera } /></Btn>
|
||||
<Btn onClick={this.import} title="Import"><FontAwesomeIcon icon={ faUpload } /></Btn>
|
||||
<Btn
|
||||
onClick={this.screenshot}
|
||||
disabled={this.state.takingCanvasShot || !this.state.canvasShotAvailable}
|
||||
title="Screenshot">
|
||||
<FontAwesomeIcon icon={ faCamera } />
|
||||
</Btn>
|
||||
<Btn
|
||||
onClick={this.import}
|
||||
title="Import">
|
||||
<FontAwesomeIcon icon={ faUpload } />
|
||||
</Btn>
|
||||
<Btn
|
||||
onClick={() => this.props.showChart()}
|
||||
title="Pixels graph">
|
||||
<FontAwesomeIcon icon={ faPieChart } />
|
||||
</Btn>
|
||||
<InputRange type="range" min={0} max={100} value={this.props.opacity} onInput={this.inputElement} />
|
||||
<PercentageDiv>{this.props.opacity}%</PercentageDiv>
|
||||
<Btn onClick={() => this.props.onCollapsedChanged(!this.props.collapsed)}><FontAwesomeIcon icon={ this.props.collapsed ? faCaretDown : faCaretUp } /></Btn>
|
||||
<Btn
|
||||
onClick={() => this.props.onCollapsedChanged(!this.props.collapsed)}>
|
||||
<FontAwesomeIcon icon={ this.props.collapsed ? faCaretDown : faCaretUp } />
|
||||
</Btn>
|
||||
</Flex>
|
||||
{this.props.collapsed ? null : <MuralList store={this.props.store} palette={this.props.palette} cords={this.props.cords}/> }
|
||||
</Container>;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import { Palette } from "../../lib/palette";
|
||||
import { Store, StoreEvents } from "../../lib/store";
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
|
||||
import styled from "styled-components";
|
||||
import { formatNumber } from "../../lib/utils";
|
||||
|
||||
const ColorBlock = styled.div`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
border: 1px solid black;
|
||||
border-radius: 12px;
|
||||
`;
|
||||
|
||||
const ColorLine = styled.div`
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 15px;
|
||||
`;
|
||||
const FlexList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
`;
|
||||
interface PixelData {
|
||||
color: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
total: number;
|
||||
groupData?: PixelData[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
store: Store;
|
||||
palette: Palette;
|
||||
}
|
||||
|
||||
export class PieCharts extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.refresh();
|
||||
this.props.store.on(StoreEvents.PixelLog, this.refresh);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.store.off(StoreEvents.PixelLog, this.refresh);
|
||||
}
|
||||
private refresh = async () => {
|
||||
const colors: string[] = [];
|
||||
const groupData: PixelData[] = [];
|
||||
let total = 0;
|
||||
for (let i = 0; i < this.props.palette.hex.length; i++) {
|
||||
const hex = this.props.palette.hex[i];
|
||||
colors.push(hex);
|
||||
const logs = await this.props.store.fetchPixelsLogByColor(i);
|
||||
if (logs.length) {
|
||||
total += logs.length;
|
||||
groupData.push({
|
||||
color: hex,
|
||||
value: logs.length
|
||||
});
|
||||
}
|
||||
}
|
||||
groupData.sort((a,b) => a.value > b.value ? -1 : 1);
|
||||
this.setState(({
|
||||
groupData,
|
||||
total
|
||||
}));
|
||||
};
|
||||
render(): React.ReactNode
|
||||
{
|
||||
if (!this.state.groupData ) {
|
||||
return <div>Loading</div>;
|
||||
}
|
||||
|
||||
if (!this.state.groupData.length) {
|
||||
return <div>No data! Please a pixel to see pixel chart</div>;
|
||||
}
|
||||
const style:React.CSSProperties = {
|
||||
width: Math.min(250, window.innerWidth - 20),
|
||||
height: Math.min(250, window.innerHeight - 20)
|
||||
};
|
||||
|
||||
return <div>
|
||||
<h5>Total pixels: {formatNumber(this.state.total)}</h5>
|
||||
<div style={style}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart width={400} height={400}>
|
||||
<Pie
|
||||
data={this.state.groupData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{this.state.groupData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.color}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<FlexList style={{ width: Math.min(250, window.innerWidth - 20)}}>
|
||||
{this.state.groupData.map((e, i) => {
|
||||
return <ColorLine key={i}>
|
||||
<ColorBlock style={{
|
||||
backgroundColor: e.color
|
||||
}}></ColorBlock>{formatNumber(e.value)}</ColorLine>;
|
||||
})}
|
||||
|
||||
</FlexList>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import styled from "styled-components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { formatNumber } from "../../lib/utils";
|
||||
import { StoreEvents } from "../../lib/store";
|
||||
|
||||
const CountBox = styled.div<{width: number}>`
|
||||
position: fixed;
|
||||
@@ -48,20 +49,16 @@ export class PixelCount extends React.Component<Props, State> {
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.pixelCount.count.then(countMaybe => {
|
||||
if (typeof countMaybe === "number") {
|
||||
this.updateCount(countMaybe);
|
||||
}
|
||||
});
|
||||
this.props.pixelCount.on(this.updateCount);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
this.updateCount();
|
||||
this.props.pixelCount.store.on(StoreEvents.PixelLog, this.updateCount);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.props.pixelCount.on(this.updateCount);
|
||||
this.props.pixelCount.store.off(StoreEvents.PixelLog, this.updateCount);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
updateCount = (count: number) => {
|
||||
this.setState({count});
|
||||
updateCount = async () => {
|
||||
this.setState({count: await this.props.pixelCount.getCount()});
|
||||
};
|
||||
onResize = () => {
|
||||
this.setState({ width: window.innerWidth });
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function importArtWorks(
|
||||
const pixels = await imageToMural(img, palette);
|
||||
const mural = await new Promise<Mural>((resolve, reject)=> {
|
||||
store.setOverlayModify({
|
||||
pixels,
|
||||
mural,
|
||||
muralObj: {
|
||||
name: img.alt,
|
||||
x: Math.floor(cords.ux - (img.width / 2)),
|
||||
|
||||
+2
-2
@@ -14,12 +14,12 @@ 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 pixelPlaced = PixelPlaced.create(store);
|
||||
const coordinates = new Coordinates();
|
||||
await coordinates.init();
|
||||
|
||||
|
||||
createUI(store,storage, coordinates, palette, pixelPlaced);
|
||||
|
||||
console.log(
|
||||
|
||||
+102
-11
@@ -1,14 +1,105 @@
|
||||
// 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);
|
||||
};
|
||||
export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise<void> | void;
|
||||
export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise<void> | void;
|
||||
|
||||
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) => {
|
||||
try {
|
||||
if (response.status === 200 && init && "body" in init) {
|
||||
const body = JSON.parse(init.body as any);
|
||||
if ("x" in body && "y" in body && "color" in body) {
|
||||
const pixelsPlaced = new CustomEvent("__pixelsPlaced", {
|
||||
detail: { color: body.color, x: body.x, y: body.y },
|
||||
});
|
||||
document.dispatchEvent(pixelsPlaced);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
export type InterceptBefore = (requestInfo: string, init?: RequestInit) => Promise<void> | void;
|
||||
export type InterceptAfter = (requestInfo: string, response: Response, init?: RequestInit) => Promise<void> | void;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-20
@@ -1,13 +1,16 @@
|
||||
import { INJECT_SCRIPT_PIXEL_OBSERVER } from "../constants";
|
||||
import { BasicEventEmitter } from "./eventEmitter";
|
||||
import { Storage } from "./storage";
|
||||
import { Store } from "./store";
|
||||
|
||||
interface Pixel {
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
export class PixelPlaced {
|
||||
private emitter = new BasicEventEmitter();
|
||||
export class PixelPlaced {
|
||||
private static instance: PixelPlaced;
|
||||
private readonly storageKey = "__pixelsPlaced";
|
||||
private constructor(private storage: Storage) {
|
||||
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
|
||||
|
||||
@@ -26,28 +29,28 @@ export class PixelPlaced {
|
||||
}
|
||||
|
||||
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);
|
||||
(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(storage: Storage) {
|
||||
static create(store: Store) {
|
||||
if (!this.instance) {
|
||||
this.instance = new PixelPlaced(storage);
|
||||
this.instance = new PixelPlaced(store);
|
||||
}
|
||||
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);
|
||||
getCount() {
|
||||
return this.store.getPixelCount();
|
||||
}
|
||||
|
||||
}
|
||||
+133
-4
@@ -16,6 +16,7 @@ export enum StoreEvents {
|
||||
MuralSelect,
|
||||
MuralOverlay,
|
||||
MuralPhantomOverlay,
|
||||
PixelLog,
|
||||
Any,
|
||||
}
|
||||
|
||||
@@ -25,23 +26,75 @@ export interface OverlayReturn {
|
||||
cb: (name: string, x: number, y: number, confirm?: boolean) => void;
|
||||
}
|
||||
|
||||
export interface PixelLog {
|
||||
timestamp: string;
|
||||
x: number;
|
||||
y: number;
|
||||
color: number;
|
||||
}
|
||||
|
||||
export interface PixelLogEx extends PixelLog {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export class Store implements LoadUnload {
|
||||
private readonly STORAGE_KEY_MURAL = "_murals";
|
||||
private readonly STORAGE_KEY_SELECTED = "_mural";
|
||||
private readonly STORAGE_KEY_LOG_STORE = "records";
|
||||
private readonly STORAGE_KEY_PIXEL_LOG = "_pixel-log";
|
||||
private _murals: MuralEx[] = [];
|
||||
private _selected?: MuralEx;
|
||||
private emitter = new BasicEventEmitter();
|
||||
private _overlayIndices: number[] = [];
|
||||
private _phantomOverlay = -1;
|
||||
private _overlayModify?: OverlayReturn;
|
||||
private dbLog?: IDBDatabase;
|
||||
private db = localforage.createInstance({
|
||||
name: "pixel-canvas-overlay"
|
||||
});
|
||||
|
||||
});
|
||||
constructor(private storage: Storage, private palette: Palette) {}
|
||||
|
||||
async load() {
|
||||
console.log("loaded");
|
||||
globalThis.store = this;
|
||||
const pixelCanvasLog = "pixel-canvas-log";
|
||||
const logDb = indexedDB.open(pixelCanvasLog, 1);
|
||||
|
||||
logDb.addEventListener("upgradeneeded", event => {
|
||||
if (event) {
|
||||
const db = (event?.target as any).result;
|
||||
const store = db
|
||||
.createObjectStore(this.STORAGE_KEY_LOG_STORE,
|
||||
{ keyPath: "id", autoIncrement: true }
|
||||
);
|
||||
store.createIndex("by_timestamp", "timestamp");
|
||||
store.createIndex("by_color", "color");
|
||||
}
|
||||
});
|
||||
|
||||
this.dbLog = await new Promise<IDBDatabase | undefined>((resolve, reject) => {
|
||||
logDb.addEventListener("success", event => {
|
||||
const db = (event.target as any)?.result as IDBDatabase;
|
||||
if (db) {
|
||||
resolve(db);
|
||||
} else {
|
||||
reject(new Error("IndexDB did not return db object"));
|
||||
}
|
||||
}, { once: true });
|
||||
logDb.addEventListener("error", event => {
|
||||
if (confirm(
|
||||
"Unable to load data. Would you like to continue? Some feature will not work correctly"
|
||||
)) {
|
||||
indexedDB.deleteDatabase(pixelCanvasLog);
|
||||
|
||||
resolve(undefined);
|
||||
} else {
|
||||
reject((event.target as IDBOpenDBRequest).error);
|
||||
}
|
||||
}, { once: true });
|
||||
});
|
||||
|
||||
|
||||
|
||||
this._murals = [];
|
||||
const rawMurals = await this.db.getItem<Uint8Array[]>(this.STORAGE_KEY_MURAL) || [];
|
||||
|
||||
@@ -77,6 +130,83 @@ export class Store implements LoadUnload {
|
||||
}
|
||||
}
|
||||
}
|
||||
addPixelLog(x: number, y: number, color: number) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const data: PixelLog = {
|
||||
timestamp,
|
||||
color,
|
||||
x,
|
||||
y
|
||||
};
|
||||
|
||||
return new Promise<IDBValidKey | null>((resolve, reject) => {
|
||||
if (!this.dbLog) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readwrite");
|
||||
const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE);
|
||||
const request = store.add(data);
|
||||
request.addEventListener("success", () => {
|
||||
resolve(request.result);
|
||||
this.emit(StoreEvents.PixelLog);
|
||||
}, { once: true });
|
||||
request.addEventListener("error", () => {
|
||||
reject(request.error);
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
async fetchPixelsLogByColor(color: number) {
|
||||
if (!this.dbLog) return Promise.resolve([]);
|
||||
const tx = this.dbLog.transaction("records", "readonly");
|
||||
const store = tx.objectStore("records");
|
||||
const index = store.index("by_color");
|
||||
const range = IDBKeyRange.only(color);
|
||||
const request = index.openCursor(range);
|
||||
return this.cursorIterator(request);
|
||||
}
|
||||
|
||||
async fetchPixelsLogByTime(startDate: Date, endDate: Date) {
|
||||
if (!this.dbLog) return Promise.resolve([]);
|
||||
const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readonly");
|
||||
const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE);
|
||||
const index = store.index("by_timestamp");
|
||||
const range = IDBKeyRange.bound(startDate.toISOString(), endDate.toISOString());
|
||||
const request = index.openCursor(range);
|
||||
return this.cursorIterator(request);
|
||||
}
|
||||
|
||||
async getPixelCount() {
|
||||
if (!this.dbLog) return Promise.resolve(0);
|
||||
const tx = this.dbLog.transaction(this.STORAGE_KEY_LOG_STORE, "readonly");
|
||||
const store = tx.objectStore(this.STORAGE_KEY_LOG_STORE);
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const request = store.count();
|
||||
request.addEventListener("success", () => {
|
||||
resolve(request.result);
|
||||
}, { once: true });
|
||||
request.addEventListener("error", () => {
|
||||
reject(request.error);
|
||||
}, { once: true });
|
||||
});
|
||||
}
|
||||
private cursorIterator(request: IDBRequest<IDBCursorWithValue | null>) {
|
||||
const results: PixelLogEx[] = [];
|
||||
return new Promise<PixelLogEx[]>((resolve) => {
|
||||
request.addEventListener("success", event => {
|
||||
const cursor = (event.target as IDBRequest).result;
|
||||
if (cursor) {
|
||||
results.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(results);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateMural(_mural: Mural) {
|
||||
this.save();
|
||||
this.emit(StoreEvents.MuralUpdated);
|
||||
@@ -90,7 +220,6 @@ export class Store implements LoadUnload {
|
||||
this.emit(StoreEvents.Any);
|
||||
this.save();
|
||||
}
|
||||
|
||||
remove(mural: MuralEx) {
|
||||
this._overlayIndices = [];
|
||||
this._overlayModify = undefined;
|
||||
|
||||
Reference in New Issue
Block a user