diff --git a/package.json b/package.json
index e14f5280..0050eeaf 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx
new file mode 100644
index 00000000..5af355e4
--- /dev/null
+++ b/src/components/layout/ErrorBoundary.tsx
@@ -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 (
+
+
+
Whoops, it broke
+
+ The app encountered an error and wasn't able to recover, please
+ report it to the discord server or on GitHub.
+
+
+ {this.state.error ? (
+
+
+ {this.state.error.name} - {this.state.error.description}
+
+
{this.state.error.path}
+
+ ) : null}
+
+ );
+ }
+}
diff --git a/src/components/layout/loading.css b/src/components/layout/loading.css
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/index.css b/src/index.css
index 4775d9ef..a433960f 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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;
}
diff --git a/src/index.tsx b/src/index.tsx
index ef47def1..dc8f917a 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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(
-
-
-
-
- ,
- document.getElementById('root')
+
+
+
+
+
+
+ ,
+ document.getElementById("root")
);
diff --git a/src/state/watched/store.ts b/src/state/watched/store.ts
new file mode 100644
index 00000000..30380675
--- /dev/null
+++ b/src/state/watched/store.ts
@@ -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()
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
new file mode 100644
index 00000000..e662e74b
--- /dev/null
+++ b/src/utils/storage.ts
@@ -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);
+ },
+ };
+}
diff --git a/src/views/MovieView.tsx b/src/views/MovieView.tsx
index 91e86511..d18f71eb 100644
--- a/src/views/MovieView.tsx
+++ b/src/views/MovieView.tsx
@@ -3,5 +3,5 @@ export function MovieView() {
- )
+ );
}
diff --git a/src/views/SearchView.tsx b/src/views/SearchView.tsx
index 282ccda2..6ae0b228 100644
--- a/src/views/SearchView.tsx
+++ b/src/views/SearchView.tsx
@@ -50,7 +50,7 @@ export function SearchView() {
))}
) : null}
- {search.searchQuery !== "" && results.length == 0 ? : null}
+ {search.searchQuery !== "" && results.length === 0 ? : null}
);
}
diff --git a/yarn.lock b/yarn.lock
index f8dc0998..9cf54e9d 100644
--- a/yarn.lock
+++ b/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"