8 changed files with 257 additions and 7 deletions
@ -0,0 +1,12 @@ |
|||||||
|
import classNames from "classnames"; |
||||||
|
|
||||||
|
export function Divider(props: { marginClass?: string }) { |
||||||
|
return ( |
||||||
|
<hr |
||||||
|
className={classNames( |
||||||
|
"w-full h-px border-0 bg-utils-divider bg-opacity-50", |
||||||
|
props.marginClass ?? "my-8" |
||||||
|
)} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
@ -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 ( |
||||||
|
<section> |
||||||
|
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2"> |
||||||
|
{props.title} |
||||||
|
</p> |
||||||
|
{props.children} |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function SidebarLink(props: { children: React.ReactNode; icon: Icons }) { |
||||||
|
return ( |
||||||
|
<div className="w-full px-2 py-1 flex items-center space-x-3"> |
||||||
|
<Icon |
||||||
|
className="text-2xl text-settings-sidebar-type-icon" |
||||||
|
icon={props.icon} |
||||||
|
/> |
||||||
|
<span>{props.children}</span> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function SettingsSidebar() { |
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
const hostname = location.hostname; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div className="sticky top-24 text-settings-sidebar-type-inactive"> |
||||||
|
<SidebarSection title="Settings"> |
||||||
|
<SidebarLink icon={Icons.WAND}>Account</SidebarLink> |
||||||
|
</SidebarSection> |
||||||
|
<Divider /> |
||||||
|
<SidebarSection title="App information"> |
||||||
|
<div className="flex justify-between items-center space-x-3"> |
||||||
|
<span>Version</span> |
||||||
|
<span>{conf().APP_VERSION}</span> |
||||||
|
</div> |
||||||
|
<div className="flex justify-between items-center space-x-3"> |
||||||
|
<span>Domain</span> |
||||||
|
<span className="text-right">{hostname}</span> |
||||||
|
</div> |
||||||
|
</SidebarSection> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function SettingsLayout(props: { children: React.ReactNode }) { |
||||||
|
return ( |
||||||
|
<WideContainer ultraWide> |
||||||
|
<div className="grid grid-cols-[260px,1fr] gap-12"> |
||||||
|
<SettingsSidebar /> |
||||||
|
{props.children} |
||||||
|
</div> |
||||||
|
</WideContainer> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function SettingsPage() { |
||||||
|
return ( |
||||||
|
<SubPageLayout> |
||||||
|
<SettingsLayout> |
||||||
|
<Heading1>Setting</Heading1> |
||||||
|
</SettingsLayout> |
||||||
|
</SubPageLayout> |
||||||
|
); |
||||||
|
} |
@ -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 ( |
||||||
|
<> |
||||||
|
<div className="flex"> |
||||||
|
<p className="flex-1 font-bold text-white">{props.name}</p> |
||||||
|
<p>{props.children}</p> |
||||||
|
</div> |
||||||
|
<Divider marginClass="my-3" /> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function ConfigValuesPart() { |
||||||
|
const normalRouter = conf().NORMAL_ROUTER; |
||||||
|
const appVersion = conf().APP_VERSION; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Heading2 className="mb-8 mt-12">Configured values</Heading2> |
||||||
|
<ConfigValue name="Routing mode"> |
||||||
|
{normalRouter ? "Normal routing" : "Hash based routing"} |
||||||
|
</ConfigValue> |
||||||
|
<ConfigValue name="Application version">v{appVersion}</ConfigValue> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -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 ( |
||||||
|
<> |
||||||
|
<Heading2 className="mb-8 mt-12">TMDB tests</Heading2> |
||||||
|
<Box> |
||||||
|
<div className="flex items-center"> |
||||||
|
<div className="flex-1"> |
||||||
|
{!status.hasTested ? ( |
||||||
|
<p>Run the test to validate TMDB</p> |
||||||
|
) : status.success ? ( |
||||||
|
<p className="flex items-center"> |
||||||
|
<Icon |
||||||
|
icon={Icons.CIRCLE_CHECK} |
||||||
|
className="text-video-scraping-success mr-2" |
||||||
|
/> |
||||||
|
TMDB is working as expected |
||||||
|
</p> |
||||||
|
) : ( |
||||||
|
<> |
||||||
|
<p className="text-white font-bold">TMDB is not working</p> |
||||||
|
<p>{status.errorText}</p> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
<Button theme="purple" onClick={runTests}> |
||||||
|
{testState.loading ? <Spinner className="text-base mr-2" /> : null} |
||||||
|
Test TMDB |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue