mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-24 18:41:48 +01:00
storage api + error boundary
This commit is contained in:
parent
f1ffa98a2b
commit
afb6958995
@ -16,6 +16,8 @@
|
|||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^5.0.0",
|
"react-scripts": "^5.0.0",
|
||||||
|
"react-tracked": "^1.7.6",
|
||||||
|
"scheduler": "^0.20.2",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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,
|
html,
|
||||||
body {
|
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 React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from "react-router-dom";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
|
import { ErrorBoundary } from "components/layout/ErrorBoundary";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<HashRouter>
|
<ErrorBoundary>
|
||||||
<App />
|
<HashRouter>
|
||||||
</HashRouter>
|
<App />
|
||||||
</React.StrictMode>,
|
</HashRouter>
|
||||||
document.getElementById('root')
|
</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>
|
<div>
|
||||||
<p>Movie view here</p>
|
<p>Movie view here</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export function SearchView() {
|
|||||||
))}
|
))}
|
||||||
</SectionHeading>
|
</SectionHeading>
|
||||||
) : null}
|
) : null}
|
||||||
{search.searchQuery !== "" && results.length == 0 ? <Loading /> : null}
|
{search.searchQuery !== "" && results.length === 0 ? <Loading /> : null}
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
20
yarn.lock
20
yarn.lock
@ -7255,6 +7255,11 @@ proxy-addr@~2.0.5:
|
|||||||
forwarded "0.2.0"
|
forwarded "0.2.0"
|
||||||
ipaddr.js "1.9.1"
|
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:
|
psl@^1.1.33:
|
||||||
version "1.8.0"
|
version "1.8.0"
|
||||||
resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
|
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"
|
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.1.tgz"
|
||||||
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
|
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:
|
react@^17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
|
||||||
@ -7820,7 +7833,7 @@ saxes@^5.0.1:
|
|||||||
|
|
||||||
scheduler@^0.20.2:
|
scheduler@^0.20.2:
|
||||||
version "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==
|
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
@ -8719,6 +8732,11 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
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:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user