mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-29 18:26:07 +00:00
storage api + error boundary
This commit is contained in:
parent
f1ffa98a2b
commit
afb6958995
|
@ -16,6 +16,8 @@
|
|||
"react-helmet": "^6.1.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"react-tracked": "^1.7.6",
|
||||
"scheduler": "^0.20.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
64
src/components/layout/ErrorBoundary.tsx
Normal file
64
src/components/layout/ErrorBoundary.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { Title } from "components/Text/Title";
|
||||
import { Component } from "react";
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: {
|
||||
name: string;
|
||||
description: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<{}, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {
|
||||
hasError: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
console.error("Render error caught", error, errorInfo);
|
||||
if (error instanceof Error) {
|
||||
let realError: Error = error as Error;
|
||||
this.setState((s) => ({
|
||||
...s,
|
||||
hasError: true,
|
||||
error: {
|
||||
name: realError.name,
|
||||
description: realError.message,
|
||||
path: errorInfo.componentStack.split("\n")[1],
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.hasError) return this.props.children;
|
||||
|
||||
// TODO make pretty
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Title>Whoops, it broke</Title>
|
||||
<p>
|
||||
The app encountered an error and wasn't able to recover, please
|
||||
report it to the discord server or on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
{this.state.error ? (
|
||||
<div>
|
||||
<p className="txt-white">
|
||||
{this.state.error.name} - {this.state.error.description}
|
||||
</p>
|
||||
<p>{this.state.error.path}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,5 +4,5 @@
|
|||
|
||||
html,
|
||||
body {
|
||||
@apply min-h-screen bg-denim-100 text-white font-open-sans;
|
||||
@apply bg-denim-100 text-denim-700 font-open-sans min-h-screen;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { HashRouter } from "react-router-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { ErrorBoundary } from "components/layout/ErrorBoundary";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
50
src/state/watched/store.ts
Normal file
50
src/state/watched/store.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { versionedStoreBuilder } from 'utils/storage';
|
||||
|
||||
/*
|
||||
version 0
|
||||
{
|
||||
[{scraperid}]: {
|
||||
movie: {
|
||||
[{movie-id}]: {
|
||||
full: {
|
||||
currentlyAt: number,
|
||||
totalDuration: number,
|
||||
updatedAt: number, // unix timestamp in ms
|
||||
meta: FullMetaObject, // no idea whats in here
|
||||
}
|
||||
}
|
||||
},
|
||||
show: {
|
||||
[{show-id}]: {
|
||||
[{season}-{episode}]: {
|
||||
currentlyAt: number,
|
||||
totalDuration: number,
|
||||
updatedAt: number, // unix timestamp in ms
|
||||
show: {
|
||||
episode: string,
|
||||
season: string,
|
||||
},
|
||||
meta: FullMetaObject, // no idea whats in here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
export const VideoProgressStore = versionedStoreBuilder()
|
||||
.setKey('video-progress')
|
||||
.addVersion({
|
||||
version: 0,
|
||||
migrate(data: any) {
|
||||
// TODO migration
|
||||
throw new Error("Migration not been written yet!!")
|
||||
},
|
||||
})
|
||||
.addVersion({
|
||||
version: 1,
|
||||
create() {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
.build()
|
226
src/utils/storage.ts
Normal file
226
src/utils/storage.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
// TODO make type and react safe!!
|
||||
/*
|
||||
it needs to be react-ified by having a save function not on the instance itself.
|
||||
also type safety is important, this is all spaghetti with "any" everywhere
|
||||
*/
|
||||
|
||||
|
||||
function buildStoreObject(d: any) {
|
||||
const data: any = {
|
||||
versions: d.versions,
|
||||
currentVersion: d.maxVersion,
|
||||
id: d.storageString,
|
||||
};
|
||||
|
||||
function update(this: any, obj: any) {
|
||||
if (!obj) throw new Error("object to update is not an object");
|
||||
|
||||
// repeat until object fully updated
|
||||
if (obj["--version"] === undefined) obj["--version"] = 0;
|
||||
while (obj["--version"] !== this.currentVersion) {
|
||||
// get version
|
||||
let version: any = obj["--version"] || 0;
|
||||
if (version.constructor !== Number || version < 0) version = -42;
|
||||
// invalid on purpose so it will reset
|
||||
else {
|
||||
version = (version as number + 1).toString();
|
||||
}
|
||||
|
||||
// check if version exists
|
||||
if (!this.versions[version]) {
|
||||
console.error(
|
||||
`Version not found for storage item in store ${this.id}, resetting`
|
||||
);
|
||||
obj = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// update object
|
||||
obj = this.versions[version].update(obj);
|
||||
}
|
||||
|
||||
// if resulting obj is null, use latest version as init object
|
||||
if (obj === null) {
|
||||
console.error(
|
||||
`Storage item for store ${this.id} has been reset due to faulty updates`
|
||||
);
|
||||
return this.versions[this.currentVersion.toString()].init();
|
||||
}
|
||||
|
||||
// updates succesful, return
|
||||
return obj;
|
||||
}
|
||||
|
||||
function get(this: any) {
|
||||
// get from storage api
|
||||
const store = this;
|
||||
let data: any = localStorage.getItem(this.id);
|
||||
|
||||
// parse json if item exists
|
||||
if (data) {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
if (!data.constructor) {
|
||||
console.error(
|
||||
`Storage item for store ${this.id} has not constructor`
|
||||
);
|
||||
throw new Error("storage item has no constructor");
|
||||
}
|
||||
if (data.constructor !== Object) {
|
||||
console.error(`Storage item for store ${this.id} is not an object`);
|
||||
throw new Error("storage item is not an object");
|
||||
}
|
||||
} catch (_) {
|
||||
// if errored, set to null so it generates new one, see below
|
||||
console.error(`Failed to parse storage item for store ${this.id}`);
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
// if item doesnt exist, generate from version init
|
||||
if (!data) {
|
||||
data = this.versions[this.currentVersion.toString()].init();
|
||||
}
|
||||
|
||||
// update the data if needed
|
||||
data = this.update(data);
|
||||
|
||||
// add a save object to return value
|
||||
data.save = function save() {
|
||||
localStorage.setItem(store.id, JSON.stringify(data));
|
||||
};
|
||||
|
||||
// add instance helpers
|
||||
Object.entries(d.instanceHelpers).forEach(([name, helper]: any) => {
|
||||
if (data[name] !== undefined)
|
||||
throw new Error(
|
||||
`helper name: ${name} on instance of store ${this.id} is reserved`
|
||||
);
|
||||
data[name] = helper.bind(data);
|
||||
});
|
||||
|
||||
// return data
|
||||
return data;
|
||||
}
|
||||
|
||||
// add functions to store
|
||||
data.get = get.bind(data);
|
||||
data.update = update.bind(data);
|
||||
|
||||
// add static helpers
|
||||
Object.entries(d.staticHelpers).forEach(([name, helper]: any) => {
|
||||
if (data[name] !== undefined)
|
||||
throw new Error(`helper name: ${name} on store ${data.id} is reserved`);
|
||||
data[name] = helper.bind({});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/*
|
||||
* Builds a versioned store
|
||||
*
|
||||
* manages versioning of localstorage items
|
||||
*/
|
||||
export function versionedStoreBuilder(): any {
|
||||
return {
|
||||
_data: {
|
||||
versionList: [],
|
||||
maxVersion: 0,
|
||||
versions: {},
|
||||
storageString: undefined,
|
||||
instanceHelpers: {},
|
||||
staticHelpers: {},
|
||||
},
|
||||
|
||||
setKey(str: string) {
|
||||
this._data.storageString = str;
|
||||
return this;
|
||||
},
|
||||
|
||||
addVersion({ version, migrate, create }: any) {
|
||||
// input checking
|
||||
if (version < 0) throw new Error("Cannot add version below 0 in store");
|
||||
if (version > 0 && !migrate)
|
||||
throw new Error(
|
||||
`Missing migration on version ${version} (needed for any version above 0)`
|
||||
);
|
||||
|
||||
// update max version list
|
||||
if (version > this._data.maxVersion) this._data.maxVersion = version;
|
||||
// add to version list
|
||||
this._data.versionList.push(version);
|
||||
|
||||
// register version
|
||||
this._data.versions[version.toString()] = {
|
||||
version: version, // version number
|
||||
update: migrate
|
||||
? (data: any) => {
|
||||
// update function, and increment version
|
||||
migrate(data);
|
||||
data["--version"] = version;
|
||||
return data;
|
||||
}
|
||||
: undefined,
|
||||
init: create
|
||||
? () => {
|
||||
// return an initial object
|
||||
const data = create();
|
||||
data["--version"] = version;
|
||||
return data;
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
return this;
|
||||
},
|
||||
|
||||
registerHelper({ name, helper, type }: any) {
|
||||
// type
|
||||
if (!type) type = "instance";
|
||||
|
||||
// input checking
|
||||
if (!name || name.constructor !== String) {
|
||||
throw new Error("helper name is not a string");
|
||||
}
|
||||
if (!helper || helper.constructor !== Function) {
|
||||
throw new Error("helper function is not a function");
|
||||
}
|
||||
if (!["instance", "static"].includes(type)) {
|
||||
throw new Error("helper type must be either 'instance' or 'static'");
|
||||
}
|
||||
|
||||
// register helper
|
||||
if (type === "instance") this._data.instanceHelpers[name as string] = helper;
|
||||
else if (type === "static") this._data.staticHelpers[name as string] = helper;
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
build() {
|
||||
// check if version list doesnt skip versions
|
||||
const versionListSorted = this._data.versionList.sort((a: number, b: number) => a - b);
|
||||
versionListSorted.forEach((v: any, i: number, arr: any[]) => {
|
||||
if (i === 0) return;
|
||||
if (v !== arr[i - 1] + 1)
|
||||
throw new Error("Version list of store is not incremental");
|
||||
});
|
||||
|
||||
// version zero must exist
|
||||
if (versionListSorted[0] !== 0)
|
||||
throw new Error("Version 0 doesn't exist in version list of store");
|
||||
|
||||
// max version must have init function
|
||||
if (!this._data.versions[this._data.maxVersion.toString()].init)
|
||||
throw new Error(
|
||||
`Missing create function on version ${this._data.maxVersion} (needed for latest version of store)`
|
||||
);
|
||||
|
||||
// check storage string
|
||||
if (!this._data.storageString)
|
||||
throw new Error("storage key not set in store");
|
||||
|
||||
// build versioned store
|
||||
return buildStoreObject(this._data);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -3,5 +3,5 @@ export function MovieView() {
|
|||
<div>
|
||||
<p>Movie view here</p>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export function SearchView() {
|
|||
))}
|
||||
</SectionHeading>
|
||||
) : null}
|
||||
{search.searchQuery !== "" && results.length == 0 ? <Loading /> : null}
|
||||
{search.searchQuery !== "" && results.length === 0 ? <Loading /> : null}
|
||||
</ThinContainer>
|
||||
);
|
||||
}
|
||||
|
|
20
yarn.lock
20
yarn.lock
|
@ -7255,6 +7255,11 @@ proxy-addr@~2.0.5:
|
|||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-compare@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.0.2.tgz#343e624d0ec399dfbe575f1d365d4fa042c9fc69"
|
||||
integrity sha512-3qUXJBariEj3eO90M3Rgqq3+/P5Efl0t/dl9g/1uVzIQmO3M+ql4hvNH3mYdu8H+1zcKv07YvL55tsY74jmH1A==
|
||||
|
||||
psl@^1.1.33:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
|
||||
|
@ -7489,6 +7494,14 @@ react-side-effect@^2.1.0:
|
|||
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz"
|
||||
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
|
||||
|
||||
react-tracked@^1.7.6:
|
||||
version "1.7.6"
|
||||
resolved "https://registry.yarnpkg.com/react-tracked/-/react-tracked-1.7.6.tgz#11bccec80acccdf5029db20171a887b8b16b7ae1"
|
||||
integrity sha512-yqfkqj4UZpsadBLIHnPLrc8a0SLgjKoSQrdyipfWeXvLnPl+/AV8MrqRVbNogUJsqHOo+ojlWy2PMxuZPVcPnQ==
|
||||
dependencies:
|
||||
proxy-compare "2.0.2"
|
||||
use-context-selector "1.3.9"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
||||
|
@ -7820,7 +7833,7 @@ saxes@^5.0.1:
|
|||
|
||||
scheduler@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
@ -8719,6 +8732,11 @@ uri-js@^4.2.2:
|
|||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
use-context-selector@1.3.9:
|
||||
version "1.3.9"
|
||||
resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.3.9.tgz#d1527393839f0d790ccdd52e28e8f353b8be6c2e"
|
||||
integrity sha512-YgzRyeFjoJXwFn2qLVAuIbV6EQ8DOuzu3SS/eiCxyAyvBhcn02jYSz8c5v22QQU3LW6Ez/Iyo62kKvS7Kdqt3A==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
|
|
Loading…
Reference in a new issue