mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01:00
localize part of settings page
This commit is contained in:
parent
0ef492f58b
commit
5b71aae159
10 changed files with 195 additions and 130 deletions
|
@ -27,6 +27,10 @@
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"failed": "Failed to migrate your data."
|
"failed": "Failed to migrate your data."
|
||||||
|
},
|
||||||
|
"dmca": {
|
||||||
|
"title": "",
|
||||||
|
"text": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
|
@ -44,6 +48,79 @@
|
||||||
"actions": {
|
"actions": {
|
||||||
"copy": "Copy"
|
"copy": "Copy"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"unsaved": "You have unsaved changes",
|
||||||
|
"reset": "Reset",
|
||||||
|
"save": "Save",
|
||||||
|
"sidebar": {
|
||||||
|
"info": {
|
||||||
|
"title": "App information",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"backendUrl": "Backend URL",
|
||||||
|
"userId": "User ID",
|
||||||
|
"notLoggedIn": "Not logged in",
|
||||||
|
"appVersion": "App version",
|
||||||
|
"backendVersion": "Backend version",
|
||||||
|
"unknownVersion": "Unknown",
|
||||||
|
"secure": "Secure",
|
||||||
|
"insecure": "Insecure"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"title": "Appearance",
|
||||||
|
"activeTheme": "Active",
|
||||||
|
"themes": {
|
||||||
|
"default": "Default",
|
||||||
|
"blue": "Blue",
|
||||||
|
"teal": "Teal",
|
||||||
|
"red": "Red",
|
||||||
|
"gray": "Gray"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"title": "Account",
|
||||||
|
"register": {
|
||||||
|
"title": "Sync to the cloud",
|
||||||
|
"text": "Instantly share your watch progress between devices and keep them synced.",
|
||||||
|
"cta": "Get started"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Edit profile picture",
|
||||||
|
"firstColor": "First color",
|
||||||
|
"secondColor": "Second color",
|
||||||
|
"userIcon": "User icon",
|
||||||
|
"finish": "Finish editing"
|
||||||
|
},
|
||||||
|
"devices": {
|
||||||
|
"title": "Devices",
|
||||||
|
"failed": "Failed to load sessions",
|
||||||
|
"deviceNameLabel": "Device name",
|
||||||
|
"removeDevice": "Remove"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"title": "Locale",
|
||||||
|
"language": "Application language",
|
||||||
|
"languageDescription": "Language applied to the entire application."
|
||||||
|
},
|
||||||
|
"captions": {
|
||||||
|
"title": "Captions"
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"title": "Connections"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faq": {
|
||||||
|
"title": "About us",
|
||||||
|
"q1": {
|
||||||
|
"title": "1",
|
||||||
|
"body": "Body of 1"
|
||||||
|
},
|
||||||
|
"how": {
|
||||||
|
"title": "1",
|
||||||
|
"body": "Body of 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
|
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
|
||||||
"links": {
|
"links": {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* eslint-disable react/no-unescaped-entities */
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { Ol } from "@/components/utils/Ol";
|
import { Ol } from "@/components/utils/Ol";
|
||||||
|
@ -16,95 +16,18 @@ function Question(props: { title: string; children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AboutPage() {
|
export function AboutPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
<Heading1>About us</Heading1>
|
<Heading1>{t("faq.title")}</Heading1>
|
||||||
<Ol
|
<Ol
|
||||||
items={[
|
items={[
|
||||||
<Question title="What is Blue?">
|
<Question title={t("faq.q1.title")}>{t("faq.q1.body")}</Question>,
|
||||||
Blue, oh so blue, like the tranquil sky on a summer's day. It's
|
|
||||||
the color of calm and serenity, a gentle embrace for your senses.
|
|
||||||
When you think of blue, you think of the vast ocean stretching
|
|
||||||
endlessly, inviting you to dive deep into its azure depths.
|
|
||||||
</Question>,
|
|
||||||
<Question title="Huh?">
|
|
||||||
Blue is the color of dreams, where the world slows down, and you
|
|
||||||
can hear the whispers of the wind in the tall grass. It's a
|
|
||||||
symphony of peacefulness that resonates with your soul, like a
|
|
||||||
melody that lingers in your heart.
|
|
||||||
</Question>,
|
|
||||||
<Question title="What the hell are you talking about?">
|
|
||||||
Blue, like, it's totally, um, the essence of like, everything, you
|
|
||||||
know? It's like, you look at it, and it's like, it's there, but
|
|
||||||
it's also not there, and it's like, you're trying to grasp the
|
|
||||||
concept of blue, but it's like trying to catch a dream in a net
|
|
||||||
made of spaghetti, you know? It's like, it's the ultimate paradox,
|
|
||||||
and it's like, it's just blowing your mind, man, like, it's like
|
|
||||||
trying to find the meaning of life in a jar of peanut butter, but
|
|
||||||
the peanut butter is made of pure energy, man, and it's like,
|
|
||||||
whoa.
|
|
||||||
</Question>,
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Paragraph>
|
<Heading2>{t("faq.how.title")}</Heading2>
|
||||||
Blue, oh so blue, like the tranquil sky on a summer's day. It's the
|
<Paragraph>{t("faq.how.body")}</Paragraph>
|
||||||
color of calm and serenity, a gentle embrace for your senses. When you
|
|
||||||
think of blue, you think of the vast ocean stretching endlessly,
|
|
||||||
inviting you to dive deep into its azure depths. Blue is the color of
|
|
||||||
dreams, where the world slows down, and you can hear the whispers of
|
|
||||||
the wind in the tall grass. It's a symphony of peacefulness that
|
|
||||||
resonates with your soul, like a melody that lingers in your heart.
|
|
||||||
</Paragraph>
|
|
||||||
<Heading2>How does it work?</Heading2>
|
|
||||||
<Paragraph>
|
|
||||||
Blue, well, it's like this cosmic wavelength, man, and it's like the
|
|
||||||
universe is just vibin', you know? It's like, when you stare at the
|
|
||||||
blue, it's like you're staring at the secrets of the cosmos, like,
|
|
||||||
whoa, it's like a trippy trip to another dimension where time doesn't
|
|
||||||
even matter, and you're just floating in a sea of, like, blue, man.
|
|
||||||
And it's like, it's not just a color, it's a whole experience, like,
|
|
||||||
you're in this cosmic rollercoaster ride through the quantum soup of
|
|
||||||
existence, and you're just riding the blue wave, man.
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
Blue, like, it's totally, um, the essence of like, everything, you
|
|
||||||
know? It's like, you look at it, and it's like, it's there, but it's
|
|
||||||
also not there, and it's like, you're trying to grasp the concept of
|
|
||||||
blue, but it's like trying to catch a dream in a net made of
|
|
||||||
spaghetti, you know? It's like, it's the ultimate paradox, and it's
|
|
||||||
like, it's just blowing your mind, man, like, it's like trying to find
|
|
||||||
the meaning of life in a jar of peanut butter, but the peanut butter
|
|
||||||
is made of pure energy, man, and it's like, whoa.
|
|
||||||
</Paragraph>
|
|
||||||
<Heading2>Frequently asked questions</Heading2>
|
|
||||||
<Paragraph>
|
|
||||||
Blue, blue, b-b-b-bluuuuuueeeeeeeee, zippity zappity zoooooo, it's
|
|
||||||
like, you know, it's like, blue is like, um, you know, it's like, um,
|
|
||||||
like a thing, but it's also not a thing, and it's like, whoa, dude,
|
|
||||||
it's like, it's like trying to juggle invisible watermelons while
|
|
||||||
riding a unicycle made of rubber bands and ketchup, and it's like,
|
|
||||||
you're just floating in the cosmic jellyfish of existence, and the
|
|
||||||
jellyfish are like, playing the accordion, man, and it's like, the
|
|
||||||
accordion is made of, like, spaghetti and, like, um, interdimensional
|
|
||||||
cheese, and it's like, whoa, dude, like, whoa.
|
|
||||||
</Paragraph>
|
|
||||||
<Paragraph>
|
|
||||||
Bloo-bloo-bloo, bleepity-bloop, blibber-blabber, blarble-blurble, blue
|
|
||||||
is like, um, you know, flibberflabberfloober, like,
|
|
||||||
zoomity-zamity-zoom, and it's like, um, sproingity-sproing, like, uh,
|
|
||||||
gibber-gabber-gobblygook, you know, it's like, um,
|
|
||||||
jibber-jabber-jibberish, like, whatchamacallit, thingamajig,
|
|
||||||
doodad-doodad-dingdong, like, ploopity-ploop, um, blibbity-blam,
|
|
||||||
flibbity-floo, like, gobbledygook-gobbledygook,
|
|
||||||
whoopsy-daisy-dingleberry, and it's like, uh,
|
|
||||||
flibberflabberflooberzoomity-sproing, um, like,
|
|
||||||
blibber-gibber-jibber-jabber, thingamajig-whatchamacallit, like, you
|
|
||||||
know, thingamajig-doodad-doodledee, and it's like, um,
|
|
||||||
doodad-gobbledygook-doodley-doo, like,
|
|
||||||
ploopity-whoopsy-doodleberry-flibber, you know, it's like, uh, blue,
|
|
||||||
man, like, totally, um, blue.
|
|
||||||
</Paragraph>
|
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,14 +7,15 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
|
||||||
|
|
||||||
import { SubPageLayout } from "./layouts/SubPageLayout";
|
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
|
|
||||||
|
// TODO make email a constant
|
||||||
export function DmcaPage() {
|
export function DmcaPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
<Heading1>{t("dmca.title")}</Heading1>
|
<Heading1>{t("screens.dmca.title")}</Heading1>
|
||||||
<Paragraph>{t("dmca.description")}</Paragraph>
|
<Paragraph>{t("screens.dmca.text")}</Paragraph>
|
||||||
<Paragraph className="flex space-x-3 items-center">
|
<Paragraph className="flex space-x-3 items-center">
|
||||||
<Icon icon={Icons.MAIL} />
|
<Icon icon={Icons.MAIL} />
|
||||||
<span>dmca@movie-web.app</span>
|
<span>dmca@movie-web.app</span>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -96,6 +97,7 @@ export function AccountSettings(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const activeTheme = useThemeStore((s) => s.theme);
|
const activeTheme = useThemeStore((s) => s.theme);
|
||||||
const setTheme = useThemeStore((s) => s.setTheme);
|
const setTheme = useThemeStore((s) => s.setTheme);
|
||||||
|
|
||||||
|
@ -244,21 +246,21 @@ export function SettingsPage() {
|
||||||
state.changed ? "opacity-100" : "opacity-0"
|
state.changed ? "opacity-100" : "opacity-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className="text-type-danger">You have unsaved changes</p>
|
<p className="text-type-danger">{t("settings.unsaved")}</p>
|
||||||
<div className="space-x-3 w-full md:w-auto flex">
|
<div className="space-x-3 w-full md:w-auto flex">
|
||||||
<Button
|
<Button
|
||||||
className="w-full md:w-auto"
|
className="w-full md:w-auto"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
onClick={state.reset}
|
onClick={state.reset}
|
||||||
>
|
>
|
||||||
Reset
|
{t("settings.reset")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full md:w-auto"
|
className="w-full md:w-auto"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
onClick={saveChanges}
|
onClick={saveChanges}
|
||||||
>
|
>
|
||||||
Save
|
{t("settings.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { SessionResponse } from "@/backend/accounts/auth";
|
import { SessionResponse } from "@/backend/accounts/auth";
|
||||||
|
@ -18,6 +19,7 @@ export function Device(props: {
|
||||||
isCurrent?: boolean;
|
isCurrent?: boolean;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const url = useBackendUrl();
|
const url = useBackendUrl();
|
||||||
const token = useAuthStore((s) => s.account?.token);
|
const token = useAuthStore((s) => s.account?.token);
|
||||||
const [result, exec] = useAsyncFn(async () => {
|
const [result, exec] = useAsyncFn(async () => {
|
||||||
|
@ -32,12 +34,14 @@ export function Device(props: {
|
||||||
paddingClass="px-6 py-4"
|
paddingClass="px-6 py-4"
|
||||||
>
|
>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
<SecondaryLabel>Device name</SecondaryLabel>
|
<SecondaryLabel>
|
||||||
|
{t("settings.account.devices.deviceNameLabel")}
|
||||||
|
</SecondaryLabel>
|
||||||
<p className="text-white">{props.name}</p>
|
<p className="text-white">{props.name}</p>
|
||||||
</div>
|
</div>
|
||||||
{!props.isCurrent ? (
|
{!props.isCurrent ? (
|
||||||
<Button theme="danger" loading={result.loading} onClick={exec}>
|
<Button theme="danger" loading={result.loading} onClick={exec}>
|
||||||
Remove
|
{t("settings.account.devices.removeDevice")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
@ -50,6 +54,7 @@ export function DeviceListPart(props: {
|
||||||
sessions: SessionResponse[];
|
sessions: SessionResponse[];
|
||||||
onChange?: () => void;
|
onChange?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const seed = useAuthStore((s) => s.account?.seed);
|
const seed = useAuthStore((s) => s.account?.seed);
|
||||||
const sessions = props.sessions;
|
const sessions = props.sessions;
|
||||||
const currentSessionId = useAuthStore((s) => s.account?.sessionId);
|
const currentSessionId = useAuthStore((s) => s.account?.sessionId);
|
||||||
|
@ -75,10 +80,10 @@ export function DeviceListPart(props: {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading2 border className="mt-0 mb-9">
|
<Heading2 border className="mt-0 mb-9">
|
||||||
Devices
|
{t("settings.account.devices.title")}
|
||||||
</Heading2>
|
</Heading2>
|
||||||
{props.error ? (
|
{props.error ? (
|
||||||
<p>Failed to load sessions</p>
|
<p>{t("settings.account.devices.failed")}</p>
|
||||||
) : props.loading ? (
|
) : props.loading ? (
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { FlagIcon } from "@/components/FlagIcon";
|
import { FlagIcon } from "@/components/FlagIcon";
|
||||||
import { Dropdown } from "@/components/form/Dropdown";
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
@ -8,7 +10,8 @@ export function LocalePart(props: {
|
||||||
language: string;
|
language: string;
|
||||||
setLanguage: (l: string) => void;
|
setLanguage: (l: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.code));
|
const { t } = useTranslation();
|
||||||
|
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
|
||||||
|
|
||||||
const options = appLanguageOptions
|
const options = appLanguageOptions
|
||||||
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
||||||
|
@ -18,14 +21,16 @@ export function LocalePart(props: {
|
||||||
leftIcon: <FlagIcon countryCode={opt.code} />,
|
leftIcon: <FlagIcon countryCode={opt.code} />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selected = options.find((t) => t.id === props.language);
|
const selected = options.find((item) => item.id === props.language);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading1 border>Locale</Heading1>
|
<Heading1 border>{t("settings.locale.title")}</Heading1>
|
||||||
<p className="text-white font-bold mb-3">Application language</p>
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.locale.language")}
|
||||||
|
</p>
|
||||||
<p className="max-w-[20rem] font-medium">
|
<p className="max-w-[20rem] font-medium">
|
||||||
Language applied to the entire application.
|
{t("settings.locale.languageDescription")}
|
||||||
</p>
|
</p>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={options}
|
options={options}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||||
import { IconPicker } from "@/components/form/IconPicker";
|
import { IconPicker } from "@/components/form/IconPicker";
|
||||||
|
@ -17,30 +19,34 @@ export interface ProfileEditModalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileEditModal(props: ProfileEditModalProps) {
|
export function ProfileEditModal(props: ProfileEditModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal id={props.id}>
|
<Modal id={props.id}>
|
||||||
<ModalCard>
|
<ModalCard>
|
||||||
<Heading2 className="!mt-0">Edit profile picture</Heading2>
|
<Heading2 className="!mt-0">
|
||||||
|
{t("settings.account.profile.title")}
|
||||||
|
</Heading2>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="First color"
|
label={t("settings.account.profile.firstColor")}
|
||||||
value={props.colorA}
|
value={props.colorA}
|
||||||
onInput={props.setColorA}
|
onInput={props.setColorA}
|
||||||
/>
|
/>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
label="Second color"
|
label={t("settings.account.profile.secondColor")}
|
||||||
value={props.colorB}
|
value={props.colorB}
|
||||||
onInput={props.setColorB}
|
onInput={props.setColorB}
|
||||||
/>
|
/>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="User icon"
|
label={t("settings.account.profile.userIcon")}
|
||||||
value={props.userIcon}
|
value={props.userIcon}
|
||||||
onInput={props.setUserIcon}
|
onInput={props.setUserIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center mt-8">
|
<div className="flex justify-center mt-8">
|
||||||
<Button theme="purple" className="!px-20" onClick={props.close}>
|
<Button theme="purple" className="!px-20" onClick={props.close}>
|
||||||
Finish editing
|
{t("settings.account.profile.finish")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalCard>
|
</ModalCard>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
|
@ -6,6 +7,7 @@ import { Heading3 } from "@/components/utils/Text";
|
||||||
|
|
||||||
export function RegisterCalloutPart() {
|
export function RegisterCalloutPart() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -14,15 +16,14 @@ export function RegisterCalloutPart() {
|
||||||
className="grid grid-cols-2 gap-12 mt-5"
|
className="grid grid-cols-2 gap-12 mt-5"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Heading3>Sync to the cloud</Heading3>
|
<Heading3>{t("settings.account.register.title")}</Heading3>
|
||||||
<p className="text-type-text">
|
<p className="text-type-text">
|
||||||
Instantly share your watch progress between devices and keep them
|
{t("settings.account.register.text")}
|
||||||
synced.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end items-center">
|
<div className="flex justify-end items-center">
|
||||||
<Button theme="purple" onClick={() => history.push("/register")}>
|
<Button theme="purple" onClick={() => history.push("/register")}>
|
||||||
Get started
|
{t("settings.account.register.cta")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SolidSettingsCard>
|
</SolidSettingsCard>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import Sticky from "react-sticky-el";
|
import Sticky from "react-sticky-el";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
|
@ -14,16 +15,22 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
const rem = 16;
|
const rem = 16;
|
||||||
|
|
||||||
function SecureBadge(props: { url: string }) {
|
function SecureBadge(props: { url: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const secure = props.url.startsWith("https://");
|
const secure = props.url.startsWith("https://");
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
|
<div className="flex items-center gap-1 -mx-1 ml-3 px-1 rounded bg-largeCard-background font-bold">
|
||||||
<Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
|
<Icon icon={secure ? Icons.LOCK : Icons.UNLOCK} />
|
||||||
Secure
|
{t(
|
||||||
|
secure
|
||||||
|
? "settings.sidebar.info.secure"
|
||||||
|
: "settings.sidebar.info.insecure"
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarPart() {
|
export function SidebarPart() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { isMobile } = useIsMobile();
|
const { isMobile } = useIsMobile();
|
||||||
const { account } = useAuthStore();
|
const { account } = useAuthStore();
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
@ -31,11 +38,31 @@ export function SidebarPart() {
|
||||||
const [activeLink, setActiveLink] = useState("");
|
const [activeLink, setActiveLink] = useState("");
|
||||||
|
|
||||||
const settingLinks = [
|
const settingLinks = [
|
||||||
{ text: "Account", id: "settings-account", icon: Icons.USER },
|
{
|
||||||
{ text: "Locale", id: "settings-locale", icon: Icons.BOOKMARK },
|
textKey: "settings.account.title",
|
||||||
{ text: "Appearance", id: "settings-appearance", icon: Icons.GITHUB },
|
id: "settings-account",
|
||||||
{ text: "Captions", id: "settings-captions", icon: Icons.CAPTIONS },
|
icon: Icons.USER,
|
||||||
{ text: "Connections", id: "settings-connection", icon: Icons.LINK },
|
},
|
||||||
|
{
|
||||||
|
textKey: "settings.locale.title",
|
||||||
|
id: "settings-locale",
|
||||||
|
icon: Icons.BOOKMARK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textKey: "settings.appearance.title",
|
||||||
|
id: "settings-appearance",
|
||||||
|
icon: Icons.GITHUB,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textKey: "settings.captions.title",
|
||||||
|
id: "settings-captions",
|
||||||
|
icon: Icons.CAPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
textKey: "settings.connections.title",
|
||||||
|
id: "settings-connection",
|
||||||
|
icon: Icons.LINK,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const backendUrl = useBackendUrl();
|
const backendUrl = useBackendUrl();
|
||||||
|
@ -103,24 +130,29 @@ export function SidebarPart() {
|
||||||
onClick={() => scrollTo(v.id)}
|
onClick={() => scrollTo(v.id)}
|
||||||
key={v.id}
|
key={v.id}
|
||||||
>
|
>
|
||||||
{v.text}
|
{t(v.textKey)}
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<SidebarSection className="text-sm" title="App information">
|
<SidebarSection
|
||||||
|
className="text-sm"
|
||||||
|
title={t("settings.sidebar.info.title")}
|
||||||
|
>
|
||||||
<div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4">
|
<div className="px-3 py-3.5 rounded-lg bg-largeCard-background bg-opacity-50 grid grid-cols-2 gap-4">
|
||||||
{/* Hostname */}
|
{/* Hostname */}
|
||||||
<div className="col-span-2 space-y-1">
|
<div className="col-span-2 space-y-1">
|
||||||
<p className="text-type-dimmed font-medium">Hostname</p>
|
<p className="text-type-dimmed font-medium">
|
||||||
|
{t("settings.sidebar.info.hostname")}
|
||||||
|
</p>
|
||||||
<p className="text-white">{hostname}</p>
|
<p className="text-white">{hostname}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Backend URL */}
|
{/* Backend URL */}
|
||||||
<div className="col-span-2 space-y-1">
|
<div className="col-span-2 space-y-1">
|
||||||
<p className="text-type-dimmed font-medium flex items-center">
|
<p className="text-type-dimmed font-medium flex items-center">
|
||||||
Backend URL
|
{t("settings.sidebar.info.backendUrl")}
|
||||||
<SecureBadge url={backendUrl} />
|
<SecureBadge url={backendUrl} />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-white">
|
<p className="text-white">
|
||||||
|
@ -130,13 +162,19 @@ export function SidebarPart() {
|
||||||
|
|
||||||
{/* User ID */}
|
{/* User ID */}
|
||||||
<div className="col-span-2 space-y-1">
|
<div className="col-span-2 space-y-1">
|
||||||
<p className="text-type-dimmed font-medium">User ID</p>
|
<p className="text-type-dimmed font-medium">
|
||||||
<p className="text-white">{account?.userId ?? "Not logged in"}</p>
|
{t("settings.sidebar.info.userId")}
|
||||||
|
</p>
|
||||||
|
<p className="text-white">
|
||||||
|
{account?.userId ?? t("settings.sidebar.info.notLoggedIn")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App version */}
|
{/* App version */}
|
||||||
<div className="col-span-1 space-y-1">
|
<div className="col-span-1 space-y-1">
|
||||||
<p className="text-type-dimmed font-medium">App version</p>
|
<p className="text-type-dimmed font-medium">
|
||||||
|
{t("settings.sidebar.info.appVersion")}
|
||||||
|
</p>
|
||||||
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block">
|
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-block">
|
||||||
{conf().APP_VERSION}
|
{conf().APP_VERSION}
|
||||||
</p>
|
</p>
|
||||||
|
@ -144,7 +182,9 @@ export function SidebarPart() {
|
||||||
|
|
||||||
{/* Backend version */}
|
{/* Backend version */}
|
||||||
<div className="col-span-1 space-y-1">
|
<div className="col-span-1 space-y-1">
|
||||||
<p className="text-type-dimmed font-medium">Backend version</p>
|
<p className="text-type-dimmed font-medium">
|
||||||
|
{t("settings.sidebar.info.backendVersion")}
|
||||||
|
</p>
|
||||||
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1">
|
<p className="text-type-dimmed px-2 py-1 rounded bg-settings-sidebar-badge inline-flex items-center gap-1">
|
||||||
{backendMeta.error ? (
|
{backendMeta.error ? (
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -155,7 +195,8 @@ export function SidebarPart() {
|
||||||
{backendMeta.loading ? (
|
{backendMeta.loading ? (
|
||||||
<div className="h-4 w-12 bg-type-dimmed/20 rounded" />
|
<div className="h-4 w-12 bg-type-dimmed/20 rounded" />
|
||||||
) : (
|
) : (
|
||||||
backendMeta?.value?.version || "Unknown"
|
backendMeta?.value?.version ||
|
||||||
|
t("settings.sidebar.info.unknownVersion")
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
@ -6,19 +7,19 @@ import { Heading1 } from "@/components/utils/Text";
|
||||||
const availableThemes = [
|
const availableThemes = [
|
||||||
{
|
{
|
||||||
id: "blue",
|
id: "blue",
|
||||||
name: "Blue",
|
key: "settings.themes.blue",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "teal",
|
id: "teal",
|
||||||
name: "Teal",
|
key: "settings.themes.teal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "red",
|
id: "red",
|
||||||
name: "Red",
|
key: "settings.themes.red",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "gray",
|
id: "gray",
|
||||||
name: "Gray",
|
key: "settings.themes.gray",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ function ThemePreview(props: {
|
||||||
name: string;
|
name: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(props.selector, "cursor-pointer group tabbable")}
|
className={classNames(props.selector, "cursor-pointer group tabbable")}
|
||||||
|
@ -58,7 +61,6 @@ function ThemePreview(props: {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/* Mini movie-web. So Kawaiiiii! */}
|
{/* Mini movie-web. So Kawaiiiii! */}
|
||||||
{/* ^ can we keep this comment in forever please? - Jip */}
|
|
||||||
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden">
|
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-3/5 h-4/5 rounded-t-lg -mb-px bg-background-main overflow-hidden">
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
{/* Background color */}
|
{/* Background color */}
|
||||||
|
@ -106,7 +108,7 @@ function ThemePreview(props: {
|
||||||
props.active ? "opacity-100" : "opacity-0 pointer-events-none"
|
props.active ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Active
|
{t("settings.appearance.activeTheme")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,13 +119,15 @@ export function ThemePart(props: {
|
||||||
active: string | null;
|
active: string | null;
|
||||||
setTheme: (theme: string | null) => void;
|
setTheme: (theme: string | null) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading1 border>Appearance</Heading1>
|
<Heading1 border>{t("settings.appearance.title")}</Heading1>
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-6 max-w-[700px]">
|
||||||
{/* default theme */}
|
{/* default theme */}
|
||||||
<ThemePreview
|
<ThemePreview
|
||||||
name="Default"
|
name={t("settings.appearance.themes.default")}
|
||||||
selector="theme-default"
|
selector="theme-default"
|
||||||
active={props.active === null}
|
active={props.active === null}
|
||||||
onClick={() => props.setTheme(null)}
|
onClick={() => props.setTheme(null)}
|
||||||
|
@ -132,7 +136,7 @@ export function ThemePart(props: {
|
||||||
<ThemePreview
|
<ThemePreview
|
||||||
selector={`theme-${v.id}`}
|
selector={`theme-${v.id}`}
|
||||||
active={props.active === v.id}
|
active={props.active === v.id}
|
||||||
name={v.name}
|
name={t(v.key)}
|
||||||
key={v.id}
|
key={v.id}
|
||||||
onClick={() => props.setTheme(v.id)}
|
onClick={() => props.setTheme(v.id)}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in a new issue