diff --git a/src/components/utils/Divider.tsx b/src/components/utils/Divider.tsx
new file mode 100644
index 00000000..470bf17d
--- /dev/null
+++ b/src/components/utils/Divider.tsx
@@ -0,0 +1,12 @@
+import classNames from "classnames";
+
+export function Divider(props: { marginClass?: string }) {
+ return (
+
+ );
+}
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
new file mode 100644
index 00000000..845d5d4f
--- /dev/null
+++ b/src/pages/Settings.tsx
@@ -0,0 +1,79 @@
+import { Icon, Icons } from "@/components/Icon";
+import { WideContainer } from "@/components/layout/WideContainer";
+import { Divider } from "@/components/utils/Divider";
+import { Heading1 } from "@/components/utils/Text";
+import { conf } from "@/setup/config";
+
+import { SubPageLayout } from "./layouts/SubPageLayout";
+
+// TODO Put all of this not here (when I'm done writing them)
+
+function SidebarSection(props: { title: string; children: React.ReactNode }) {
+ return (
+
+
+ {props.title}
+
+ {props.children}
+
+ );
+}
+
+function SidebarLink(props: { children: React.ReactNode; icon: Icons }) {
+ return (
+
+
+ {props.children}
+
+ );
+}
+
+function SettingsSidebar() {
+ // eslint-disable-next-line no-restricted-globals
+ const hostname = location.hostname;
+
+ return (
+
+
+
+ Account
+
+
+
+
+ Version
+ {conf().APP_VERSION}
+
+
+ Domain
+ {hostname}
+
+
+
+
+ );
+}
+
+function SettingsLayout(props: { children: React.ReactNode }) {
+ return (
+
+
+
+ {props.children}
+
+
+ );
+}
+
+export function SettingsPage() {
+ return (
+
+
+ Setting
+
+
+ );
+}
diff --git a/src/pages/admin/AdminPage.tsx b/src/pages/admin/AdminPage.tsx
index 5572912f..d98660d2 100644
--- a/src/pages/admin/AdminPage.tsx
+++ b/src/pages/admin/AdminPage.tsx
@@ -1,6 +1,8 @@
import { ThinContainer } from "@/components/layout/ThinContainer";
import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
+import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
+import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
export function AdminPage() {
@@ -10,7 +12,9 @@ export function AdminPage() {
Admin tools
Useful tools to test out your current deployment
+
+
);
diff --git a/src/pages/parts/admin/ConfigValuesPart.tsx b/src/pages/parts/admin/ConfigValuesPart.tsx
new file mode 100644
index 00000000..8947a5ab
--- /dev/null
+++ b/src/pages/parts/admin/ConfigValuesPart.tsx
@@ -0,0 +1,32 @@
+import { ReactNode } from "react";
+
+import { Divider } from "@/components/utils/Divider";
+import { Heading2 } from "@/components/utils/Text";
+import { conf } from "@/setup/config";
+
+function ConfigValue(props: { name: string; children?: ReactNode }) {
+ return (
+ <>
+
+
{props.name}
+
{props.children}
+
+
+ >
+ );
+}
+
+export function ConfigValuesPart() {
+ const normalRouter = conf().NORMAL_ROUTER;
+ const appVersion = conf().APP_VERSION;
+
+ return (
+ <>
+ Configured values
+
+ {normalRouter ? "Normal routing" : "Hash based routing"}
+
+ v{appVersion}
+ >
+ );
+}
diff --git a/src/pages/parts/admin/TMDBTestPart.tsx b/src/pages/parts/admin/TMDBTestPart.tsx
new file mode 100644
index 00000000..33d35144
--- /dev/null
+++ b/src/pages/parts/admin/TMDBTestPart.tsx
@@ -0,0 +1,93 @@
+import { useState } from "react";
+import { useAsyncFn } from "react-use";
+
+import { getMediaDetails } from "@/backend/metadata/tmdb";
+import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
+import { Button } from "@/components/Button";
+import { Icon, Icons } from "@/components/Icon";
+import { Box } from "@/components/layout/Box";
+import { Spinner } from "@/components/layout/Spinner";
+import { Heading2 } from "@/components/utils/Text";
+import { conf } from "@/setup/config";
+
+export function TMDBTestPart() {
+ const tmdbApiKey = conf().TMDB_READ_API_KEY;
+ const [status, setStatus] = useState({
+ hasTested: false,
+ success: false,
+ errorText: "",
+ });
+
+ const [testState, runTests] = useAsyncFn(async () => {
+ setStatus({
+ hasTested: false,
+ success: false,
+ errorText: "",
+ });
+
+ if (tmdbApiKey.length === 0) {
+ return setStatus({
+ hasTested: true,
+ success: false,
+ errorText: "TMDB api key is not set",
+ });
+ }
+ const isJWT = tmdbApiKey.split(".").length > 2;
+ if (!isJWT) {
+ return setStatus({
+ hasTested: true,
+ success: false,
+ errorText: "TMDB api key is not a read only key",
+ });
+ }
+
+ try {
+ await getMediaDetails("556574", TMDBContentTypes.MOVIE);
+ } catch (err) {
+ return setStatus({
+ hasTested: true,
+ success: false,
+ errorText:
+ "Failed to call tmdb, double check api key and your internet connection",
+ });
+ }
+
+ return setStatus({
+ hasTested: true,
+ success: true,
+ errorText: "",
+ });
+ }, [tmdbApiKey, setStatus]);
+
+ return (
+ <>
+ TMDB tests
+
+
+
+ {!status.hasTested ? (
+
Run the test to validate TMDB
+ ) : status.success ? (
+
+
+ TMDB is working as expected
+
+ ) : (
+ <>
+
TMDB is not working
+
{status.errorText}
+ >
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/src/pages/parts/admin/WorkerTestPart.tsx b/src/pages/parts/admin/WorkerTestPart.tsx
index f687e31f..2b33d61a 100644
--- a/src/pages/parts/admin/WorkerTestPart.tsx
+++ b/src/pages/parts/admin/WorkerTestPart.tsx
@@ -1,13 +1,13 @@
import classNames from "classnames";
-import { f } from "ofetch/dist/shared/ofetch.441891d5";
-import { useCallback, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
import { useAsyncFn } from "react-use";
import { mwFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
-import { Divider } from "@/components/player/internals/ContextMenu/Misc";
+import { Spinner } from "@/components/layout/Spinner";
+import { Divider } from "@/components/utils/Divider";
import { Heading2 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
@@ -53,7 +53,7 @@ export function WorkerTestPart() {
{ id: string; status: "error" | "success"; error?: Error }[]
>([]);
- const runTests = useAsyncFn(async () => {
+ const [testState, runTests] = useAsyncFn(async () => {
function updateWorker(id: string, data: (typeof workerState)[number]) {
setWorkerState((s) => {
return [...s.filter((v) => v.id !== id), data];
@@ -62,6 +62,14 @@ export function WorkerTestPart() {
setWorkerState([]);
for (const worker of workerList) {
try {
+ if (worker.url.endsWith("/")) {
+ updateWorker(worker.id, {
+ id: worker.id,
+ status: "error",
+ error: new Error("URL ends with slash"),
+ });
+ continue;
+ }
await mwFetch(worker.url, {
query: {
destination: "https://postman-echo.com/get",
@@ -83,8 +91,8 @@ export function WorkerTestPart() {
return (
<>
- Worker tests
- 15 workers registered
+ Worker tests
+ {workerList.length} worker(s) registered
{workerList.map((v, i) => {
const s = workerState.find((segment) => segment.id);
@@ -105,7 +113,10 @@ export function WorkerTestPart() {
})}
-
+
>
diff --git a/src/setup/App.tsx b/src/setup/App.tsx
index f571b65a..c09c816e 100644
--- a/src/setup/App.tsx
+++ b/src/setup/App.tsx
@@ -17,6 +17,7 @@ import { DmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage";
import { PlayerView } from "@/pages/PlayerView";
+import { SettingsPage } from "@/pages/Settings";
import { Layout } from "@/setup/Layout";
import { BookmarkContextProvider } from "@/state/bookmark";
import { SettingsProvider } from "@/state/settings";
@@ -103,6 +104,9 @@ function App() {
+ {/* Settings page */}
+
+
{/* admin routes */}
diff --git a/tailwind.config.js b/tailwind.config.js
index 3783226d..dbefe882 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -109,6 +109,21 @@ module.exports = {
badgeText: "#5F5F7A"
},
+ settings: {
+ sidebar: {
+ type: {
+ secondary: "#4B395F",
+ inactive: "#8D68A9",
+ icon: "#926CAD",
+ activated: "#CBA1E8"
+ }
+ }
+ },
+
+ utils: {
+ divider: "#353549"
+ },
+
// Error page
errors: {
card: "#12121B",