|
|
|
@ -1,11 +1,11 @@
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
|
|
|
|
import { NextPage } from "next";
|
|
|
|
|
import { NextSeo } from "next-seo";
|
|
|
|
|
|
|
|
|
|
import { FC, useState } from "react";
|
|
|
|
|
|
|
|
|
|
import { useQuery } from "react-query";
|
|
|
|
|
import { useMutation, useQuery } from "react-query";
|
|
|
|
|
|
|
|
|
|
import axios from "axios";
|
|
|
|
|
|
|
|
|
|
import Box from "@mui/material/Box";
|
|
|
|
|
import Button from "@mui/material/Button";
|
|
|
|
@ -18,44 +18,103 @@ import InputLabel from "@mui/material/InputLabel";
|
|
|
|
|
import MenuItem from "@mui/material/MenuItem";
|
|
|
|
|
import Modal from "@mui/material/Modal";
|
|
|
|
|
import Select from "@mui/material/Select";
|
|
|
|
|
import TextField from "@mui/material/TextField";
|
|
|
|
|
import Typography from "@mui/material/Typography";
|
|
|
|
|
import {
|
|
|
|
|
blue,
|
|
|
|
|
red,
|
|
|
|
|
green,
|
|
|
|
|
cyan,
|
|
|
|
|
purple,
|
|
|
|
|
yellow,
|
|
|
|
|
orange
|
|
|
|
|
} from "@mui/material/colors/";
|
|
|
|
|
import { red, green } from "@mui/material/colors/";
|
|
|
|
|
import { useTheme } from "@mui/material/styles/";
|
|
|
|
|
|
|
|
|
|
import Done from "@mui/icons-material/Done";
|
|
|
|
|
import Error from "@mui/icons-material/Error";
|
|
|
|
|
|
|
|
|
|
import { toCamelCase } from "@src/utils";
|
|
|
|
|
import Refresh from "@mui/icons-material/Refresh";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ServerInstance,
|
|
|
|
|
ServerInstanceType,
|
|
|
|
|
Stats
|
|
|
|
|
} from "@interfaces/api/instances";
|
|
|
|
|
import { StorageType } from "@interfaces/settings";
|
|
|
|
|
|
|
|
|
|
import { useSettings } from "@utils/hooks";
|
|
|
|
|
|
|
|
|
|
import Layout from "@components/Layout";
|
|
|
|
|
import MaterialColorPicker from "@components/MaterialColorPicker";
|
|
|
|
|
import ColorBox from "@components/MaterialColorPicker/ColorBox";
|
|
|
|
|
import ModalBox from "@components/ModalBox";
|
|
|
|
|
|
|
|
|
|
const InfoModal: FC<{
|
|
|
|
|
modalIsOpen: boolean;
|
|
|
|
|
setModalState: (isOpen: boolean) => void;
|
|
|
|
|
data: Stats;
|
|
|
|
|
}> = ({ modalIsOpen, setModalState, data }) => {
|
|
|
|
|
const [settings] = useSettings();
|
|
|
|
|
|
|
|
|
|
const lastUpdated = new Date(data.metadata.updatedAt * 1000);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={modalIsOpen}
|
|
|
|
|
onClose={() => setModalState(false)}
|
|
|
|
|
aria-labelledby="stats-modal"
|
|
|
|
|
aria-describedby="Shows server stats"
|
|
|
|
|
>
|
|
|
|
|
<ModalBox>
|
|
|
|
|
<Typography id="modal-modal-title" variant="h4">
|
|
|
|
|
Stats for {settings.invidiousServer}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
|
|
|
|
|
Version: {data.version} <br /> <br />
|
|
|
|
|
Software name: {data.software.name} <br />
|
|
|
|
|
Software version: {data.software.version} <br />
|
|
|
|
|
Software branch: {data.software.branch} <br /> <br />
|
|
|
|
|
Is accepting registrations: {data.openRegistrations ? "Yes" : "No"}
|
|
|
|
|
<br /> <br />
|
|
|
|
|
Total users: {data.usage.users.total} <br />
|
|
|
|
|
Active in the past half year: {data.usage.users.activeHalfyear}
|
|
|
|
|
<br />
|
|
|
|
|
Active in the past month: {data.usage.users.activeMonth} <br /> <br />
|
|
|
|
|
Stats updated at: {lastUpdated.toLocaleDateString()} -{" "}
|
|
|
|
|
{lastUpdated.toLocaleTimeString()}
|
|
|
|
|
</Typography>
|
|
|
|
|
</ModalBox>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Setting: FC<{ title: string; description?: string }> = ({
|
|
|
|
|
title,
|
|
|
|
|
children,
|
|
|
|
|
description
|
|
|
|
|
}) => {
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{ my: 3, display: "flex", alignItems: "center" }}>
|
|
|
|
|
<Box sx={{ flexGrow: 1 }}>
|
|
|
|
|
<Typography variant="h5">{title}</Typography>
|
|
|
|
|
{description && (
|
|
|
|
|
<Typography variant="subtitle1" color={theme.palette.text.secondary}>
|
|
|
|
|
{description}
|
|
|
|
|
</Typography>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
{children}
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Settings: NextPage = () => {
|
|
|
|
|
const [settings, setSettings] = useSettings();
|
|
|
|
|
|
|
|
|
|
const setSetting = (key: string, value?: string): void => {
|
|
|
|
|
const setSetting = (key: string, value?: string): void =>
|
|
|
|
|
setSettings({ ...settings, [key]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const [modalIsOpen, setModalState] = useState(false);
|
|
|
|
|
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
|
|
|
|
|
const [primaryColorModalIsOpen, setPrimaryColorModal] = useState(false);
|
|
|
|
|
const [accentColorModalIsOpen, setAccentColorModal] = useState(false);
|
|
|
|
|
|
|
|
|
|
const [modalIsOpen, setModalState] = useState(false);
|
|
|
|
|
|
|
|
|
|
const instances = useQuery<[string, ServerInstance][]>(
|
|
|
|
|
"invidiousInstances",
|
|
|
|
|
() =>
|
|
|
|
@ -65,103 +124,13 @@ const Settings: NextPage = () => {
|
|
|
|
|
{ retry: false }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const invidiousServerResponse = useQuery<Stats>("invidiousInstance", () =>
|
|
|
|
|
axios
|
|
|
|
|
.get(`https://${settings.invidiousServer}/api/v1/stats`)
|
|
|
|
|
.then((res) => res.data)
|
|
|
|
|
const invidiousServerResponse = useMutation<Stats, unknown, string>(
|
|
|
|
|
"invidiousInstance",
|
|
|
|
|
(server) =>
|
|
|
|
|
axios.get(`https://${server}/api/v1/stats`).then((res) => res.data)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ColorSetting: FC<{ type: string }> = ({ type }) => {
|
|
|
|
|
const camelCase = toCamelCase(type) as "primaryColor" | "accentColor";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{ my: 3, display: "flex" }}>
|
|
|
|
|
<Typography variant="h5" sx={{ flexGrow: 1 }}>
|
|
|
|
|
{type}
|
|
|
|
|
</Typography>
|
|
|
|
|
{[
|
|
|
|
|
blue[800],
|
|
|
|
|
red[800],
|
|
|
|
|
green[800],
|
|
|
|
|
cyan[800],
|
|
|
|
|
purple[800],
|
|
|
|
|
yellow[800],
|
|
|
|
|
orange[800]
|
|
|
|
|
].map((color) => (
|
|
|
|
|
<Box
|
|
|
|
|
onClick={() => setSetting(camelCase, color)}
|
|
|
|
|
key={color}
|
|
|
|
|
component="span"
|
|
|
|
|
sx={{
|
|
|
|
|
width: 24,
|
|
|
|
|
height: 24,
|
|
|
|
|
backgroundColor: color,
|
|
|
|
|
borderRadius: "50%",
|
|
|
|
|
border:
|
|
|
|
|
color == settings[camelCase]
|
|
|
|
|
? {
|
|
|
|
|
borderColor: theme.palette.text.primary,
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderStyle: "solid"
|
|
|
|
|
}
|
|
|
|
|
: null,
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
ml: 1.5
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const InfoModal: FC = () => {
|
|
|
|
|
const data = invidiousServerResponse.data!;
|
|
|
|
|
|
|
|
|
|
const lastUpdated = new Date(data.metadata.updatedAt * 1000);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={modalIsOpen}
|
|
|
|
|
onClose={() => setModalState(false)}
|
|
|
|
|
aria-labelledby="modal-modal-title"
|
|
|
|
|
aria-describedby="modal-modal-description"
|
|
|
|
|
>
|
|
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
p: 4,
|
|
|
|
|
position: "absolute",
|
|
|
|
|
left: "50%",
|
|
|
|
|
top: "50%",
|
|
|
|
|
transform: "translate(-50%, -50%)",
|
|
|
|
|
backgroundColor: theme.palette.background.paper,
|
|
|
|
|
borderRadius: 1,
|
|
|
|
|
outline: "none"
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Typography id="modal-modal-title" variant="h4">
|
|
|
|
|
Stats for {settings.invidiousServer}
|
|
|
|
|
</Typography>
|
|
|
|
|
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
|
|
|
|
|
Version: {data.version} <br /> <br />
|
|
|
|
|
Software name: {data.software.name} <br />
|
|
|
|
|
Software version: {data.software.version} <br />
|
|
|
|
|
Software branch: {data.software.branch} <br /> <br />
|
|
|
|
|
Is accepting registrations: {data.openRegistrations
|
|
|
|
|
? "Yes"
|
|
|
|
|
: "No"}{" "}
|
|
|
|
|
<br /> <br />
|
|
|
|
|
Total users: {data.usage.users.total} <br />
|
|
|
|
|
Active in the past half year: {data.usage.users.activeHalfyear}
|
|
|
|
|
<br />
|
|
|
|
|
Active in the past month: {data.usage.users.activeMonth} <br />{" "}
|
|
|
|
|
<br />
|
|
|
|
|
Stats updated at: {lastUpdated.toLocaleDateString()} -{" "}
|
|
|
|
|
{lastUpdated.toLocaleTimeString()}
|
|
|
|
|
</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
const allowsRegistrations = invidiousServerResponse.data?.openRegistrations;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
@ -179,10 +148,10 @@ const Settings: NextPage = () => {
|
|
|
|
|
|
|
|
|
|
<Typography variant="h4">Theme</Typography>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ my: 3, display: "flex" }}>
|
|
|
|
|
<Typography variant="h5" sx={{ flexGrow: 1 }}>
|
|
|
|
|
General theme
|
|
|
|
|
</Typography>
|
|
|
|
|
<Setting
|
|
|
|
|
title="General Theme"
|
|
|
|
|
description="Sets the background color"
|
|
|
|
|
>
|
|
|
|
|
<ButtonGroup
|
|
|
|
|
variant="contained"
|
|
|
|
|
color="primary"
|
|
|
|
@ -194,11 +163,39 @@ const Settings: NextPage = () => {
|
|
|
|
|
<Button onClick={() => setSetting("theme")}>System</Button>
|
|
|
|
|
<Button onClick={() => setSetting("theme", "dark")}>Dark</Button>
|
|
|
|
|
</ButtonGroup>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<ColorSetting type="Primary Color" />
|
|
|
|
|
</Setting>
|
|
|
|
|
|
|
|
|
|
<ColorSetting type="Accent Color" />
|
|
|
|
|
<Setting title="Primary Color">
|
|
|
|
|
<ColorBox marginRight={1} color={settings.primaryColor} />
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setPrimaryColorModal(true)}
|
|
|
|
|
variant="contained"
|
|
|
|
|
>
|
|
|
|
|
Pick Color
|
|
|
|
|
</Button>
|
|
|
|
|
<MaterialColorPicker
|
|
|
|
|
setState={setPrimaryColorModal}
|
|
|
|
|
isOpen={primaryColorModalIsOpen}
|
|
|
|
|
setColor={(color) => setSetting("primaryColor", color)}
|
|
|
|
|
selectedColor={settings.primaryColor}
|
|
|
|
|
/>
|
|
|
|
|
</Setting>
|
|
|
|
|
|
|
|
|
|
<Setting title="Accent Color">
|
|
|
|
|
<ColorBox marginRight={1} color={settings.accentColor} />
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setAccentColorModal(true)}
|
|
|
|
|
variant="contained"
|
|
|
|
|
>
|
|
|
|
|
Pick Color
|
|
|
|
|
</Button>
|
|
|
|
|
<MaterialColorPicker
|
|
|
|
|
setState={setAccentColorModal}
|
|
|
|
|
isOpen={accentColorModalIsOpen}
|
|
|
|
|
setColor={(color) => setSetting("accentColor", color)}
|
|
|
|
|
selectedColor={settings.accentColor}
|
|
|
|
|
/>
|
|
|
|
|
</Setting>
|
|
|
|
|
|
|
|
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
|
|
|
|
|
@ -206,13 +203,28 @@ const Settings: NextPage = () => {
|
|
|
|
|
|
|
|
|
|
<Divider sx={{ my: 4 }} />
|
|
|
|
|
|
|
|
|
|
<Typography variant="h4">Miscellaneous</Typography>
|
|
|
|
|
<Typography variant="h4">Data</Typography>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ my: 3, display: "flex", alignItems: "center" }}>
|
|
|
|
|
<Typography variant="h5" sx={{ flexGrow: 1 }}>
|
|
|
|
|
Invidious Server
|
|
|
|
|
</Typography>
|
|
|
|
|
<Setting
|
|
|
|
|
title="Invidious Server"
|
|
|
|
|
description={`Where to fetch data from ${
|
|
|
|
|
settings.storageType == "invidious" ? "and login into" : ""
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{ mr: 2 }}>
|
|
|
|
|
{!invidiousServerResponse.data &&
|
|
|
|
|
!invidiousServerResponse.error &&
|
|
|
|
|
!invidiousServerResponse.isLoading && (
|
|
|
|
|
<Refresh
|
|
|
|
|
sx={{
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
color: theme.palette.text.secondary
|
|
|
|
|
}}
|
|
|
|
|
onClick={() =>
|
|
|
|
|
invidiousServerResponse.mutate(settings.invidiousServer)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{invidiousServerResponse.data &&
|
|
|
|
|
!invidiousServerResponse.isLoading && (
|
|
|
|
|
<>
|
|
|
|
@ -220,9 +232,14 @@ const Settings: NextPage = () => {
|
|
|
|
|
onClick={() => setModalState(true)}
|
|
|
|
|
sx={{ color: green[800], cursor: "pointer" }}
|
|
|
|
|
/>
|
|
|
|
|
<InfoModal />
|
|
|
|
|
<InfoModal
|
|
|
|
|
modalIsOpen={modalIsOpen}
|
|
|
|
|
setModalState={setModalState}
|
|
|
|
|
data={invidiousServerResponse.data!}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{invidiousServerResponse.error && (
|
|
|
|
|
<Error sx={{ color: red[800] }} />
|
|
|
|
|
)}
|
|
|
|
@ -238,6 +255,8 @@ const Settings: NextPage = () => {
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const server = e.target.value;
|
|
|
|
|
|
|
|
|
|
invidiousServerResponse.mutate(server);
|
|
|
|
|
|
|
|
|
|
setSetting("invidiousServer", server);
|
|
|
|
|
}}
|
|
|
|
|
MenuProps={{ sx: { maxHeight: 300 } }}
|
|
|
|
@ -256,7 +275,89 @@ const Settings: NextPage = () => {
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</Box>
|
|
|
|
|
</Setting>
|
|
|
|
|
|
|
|
|
|
<Setting
|
|
|
|
|
title="Data Storage Location"
|
|
|
|
|
description="Where your personal data will be stored"
|
|
|
|
|
>
|
|
|
|
|
<FormControl sx={{ minWidth: 200 }}>
|
|
|
|
|
<InputLabel id="location-select-label">Location</InputLabel>
|
|
|
|
|
<Select
|
|
|
|
|
labelId="location-select-label"
|
|
|
|
|
id="location-select"
|
|
|
|
|
value={settings.storageType}
|
|
|
|
|
label="Location"
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const location = e.target.value;
|
|
|
|
|
|
|
|
|
|
setSetting("storageType", location);
|
|
|
|
|
}}
|
|
|
|
|
MenuProps={{ sx: { maxHeight: 300 } }}
|
|
|
|
|
>
|
|
|
|
|
{[
|
|
|
|
|
{ name: "Locally", value: StorageType.Local },
|
|
|
|
|
{
|
|
|
|
|
name: "Invidious server using auth",
|
|
|
|
|
value: StorageType.Invidious
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: `A custom ${process.env.NEXT_PUBLIC_APP_NAME} auth server`,
|
|
|
|
|
value: StorageType.RemoteServer
|
|
|
|
|
}
|
|
|
|
|
].map((location, i) => (
|
|
|
|
|
<MenuItem key={i} value={location.value}>
|
|
|
|
|
{location.name}
|
|
|
|
|
</MenuItem>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
</Setting>
|
|
|
|
|
|
|
|
|
|
{settings.storageType != StorageType.Local && (
|
|
|
|
|
<>
|
|
|
|
|
{settings.storageType == "invidious" && allowsRegistrations && (
|
|
|
|
|
<Setting
|
|
|
|
|
title="Username"
|
|
|
|
|
description="The username for your Invidious account"
|
|
|
|
|
>
|
|
|
|
|
<TextField label="Username" color="primary" />
|
|
|
|
|
</Setting>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{settings.storageType == StorageType.RemoteServer && (
|
|
|
|
|
<Setting
|
|
|
|
|
title="Server address"
|
|
|
|
|
description={`The address for your ${process.env.NEXT_PUBLIC_APP_NAME} auth server`}
|
|
|
|
|
>
|
|
|
|
|
<TextField label="Server adress" color="primary" />
|
|
|
|
|
</Setting>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Setting
|
|
|
|
|
title={
|
|
|
|
|
settings.storageType == StorageType.Invidious
|
|
|
|
|
? "Password"
|
|
|
|
|
: "Passphrase"
|
|
|
|
|
}
|
|
|
|
|
description={
|
|
|
|
|
settings.storageType == StorageType.Invidious
|
|
|
|
|
? "The password for your invidious account"
|
|
|
|
|
: "The passphrase for your account"
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<TextField
|
|
|
|
|
type="password"
|
|
|
|
|
label={
|
|
|
|
|
settings.storageType == StorageType.Invidious
|
|
|
|
|
? "Password"
|
|
|
|
|
: "Passphrase"
|
|
|
|
|
}
|
|
|
|
|
color="primary"
|
|
|
|
|
/>
|
|
|
|
|
</Setting>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Container>
|
|
|
|
|
</Layout>
|
|
|
|
|
</>
|
|
|
|
|