mirror of
https://github.com/movie-web/movie-web.git
synced 2025-02-03 05:02:51 +01:00
More localisation
Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
parent
d20fc4bf82
commit
75933e7080
@ -1,4 +1,60 @@
|
|||||||
{
|
{
|
||||||
|
"auth": {
|
||||||
|
"deviceNameLabel": "Device name",
|
||||||
|
"deviceNamePlaceholder": "Muad'Dib's Nintendo Switch",
|
||||||
|
"register": {
|
||||||
|
"information": {
|
||||||
|
"title": "Account information",
|
||||||
|
"color1": "First color",
|
||||||
|
"color2": "Second color",
|
||||||
|
"icon": "User icon",
|
||||||
|
"header": "Enter a name for your device and choose a user icon and colours"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Login to your account",
|
||||||
|
"description": "Oh, you're asking for the key to my top-secret lair, also known as The Fortress of Wordsmithery, accessed only by reciting the sacred incantation of the 12-word passphrase!",
|
||||||
|
"validationError": "Invalid or incomplete passphrase",
|
||||||
|
"submit": "Login",
|
||||||
|
"passphraseLabel": "12-Word Passphrase",
|
||||||
|
"passphrasePlaceholder": "Passphrase"
|
||||||
|
},
|
||||||
|
"generate": {
|
||||||
|
"title": "Your passphrase",
|
||||||
|
"description": "If you lose this, you're a silly goose and will be posted on the wall of shame™️"
|
||||||
|
},
|
||||||
|
"trust": {
|
||||||
|
"title": "Do you trust this host?",
|
||||||
|
"host": "Do you trust <0>{{hostname}}</0>?",
|
||||||
|
"failed": {
|
||||||
|
"title": "Failed to reach backend",
|
||||||
|
"text": "Did you configure it correctly?"
|
||||||
|
},
|
||||||
|
"yes": "Trust",
|
||||||
|
"no": "Go back"
|
||||||
|
},
|
||||||
|
"verify": {
|
||||||
|
"title": "Enter your passphrase",
|
||||||
|
"description": "If you've already lost it, how will you ever be able to take care of a child?",
|
||||||
|
"invalidData": "Data is not valid",
|
||||||
|
"noMatch": "Passphrase doesn't match",
|
||||||
|
"recaptchaFailed": "ReCaptcha validation failed",
|
||||||
|
"passphraseLabel": "Your passphrase",
|
||||||
|
"register": "Register"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"details": "Error details",
|
||||||
|
"reloadPage": "Reload the page",
|
||||||
|
"badge": "It broke",
|
||||||
|
"title": "That's an error boss"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"badge": "Not found",
|
||||||
|
"title": "Page could not be found",
|
||||||
|
"message": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||||
|
"goHome": "Go home"
|
||||||
|
},
|
||||||
"global": {
|
"global": {
|
||||||
"name": "movie-web"
|
"name": "movie-web"
|
||||||
},
|
},
|
||||||
@ -9,9 +65,57 @@
|
|||||||
},
|
},
|
||||||
"episodeDisplay": "S{{season}} E{{episode}}"
|
"episodeDisplay": "S{{season}} E{{episode}}"
|
||||||
},
|
},
|
||||||
|
"player": {
|
||||||
|
"scraping": {
|
||||||
|
"notFound": {
|
||||||
|
"badge": "Not found",
|
||||||
|
"title": "Goo goo gaa gaa",
|
||||||
|
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||||
|
"homeButton": "Go home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playbackError": {
|
||||||
|
"badge": "Not found",
|
||||||
|
"title": "Goo goo gaa gaa",
|
||||||
|
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||||
|
"homeButton": "Go home"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"notFound": {
|
||||||
|
"badge": "Not found",
|
||||||
|
"title": "This media doesnt exist",
|
||||||
|
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||||
|
"homeButton": "Go home"
|
||||||
|
},
|
||||||
|
"failed": {
|
||||||
|
"badge": "Failed",
|
||||||
|
"title": "Failed to load meta data",
|
||||||
|
"text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖",
|
||||||
|
"homeButton": "Go home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"mediaList": {
|
"mediaList": {
|
||||||
"stopEditing": "Stop editing"
|
"stopEditing": "Stop editing"
|
||||||
|
},
|
||||||
|
"titles": {
|
||||||
|
"morning": ["Morning title"],
|
||||||
|
"day": ["Day title"],
|
||||||
|
"night": ["Night title"]
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"sectionTitle": "Search results",
|
||||||
|
"allResults": "That's all we have!",
|
||||||
|
"noResults": "We couldn't find anything!",
|
||||||
|
"failed": "Failed to find media, try again!",
|
||||||
|
"placeholder": "What do you want to watch?"
|
||||||
|
},
|
||||||
|
"continueWatching": {
|
||||||
|
"sectionTitle": "Continue Watching"
|
||||||
|
},
|
||||||
|
"bookmarks": {
|
||||||
|
"sectionTitle": "Bookmarks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overlays": {
|
"overlays": {
|
||||||
@ -21,16 +125,17 @@
|
|||||||
"loadingUser": "Loading your profile",
|
"loadingUser": "Loading your profile",
|
||||||
"loadingApp": "Loading application",
|
"loadingApp": "Loading application",
|
||||||
"loadingUserError": {
|
"loadingUserError": {
|
||||||
"text": "",
|
"text": "Failed to load your profile",
|
||||||
"textWithReset": "",
|
"textWithReset": "Failed to load your profile from your custom server, want to reset back to default?",
|
||||||
"reset": "Reset custom server"
|
"reset": "Reset custom server"
|
||||||
},
|
},
|
||||||
"migration": {
|
"migration": {
|
||||||
"failed": "Failed to migrate your data."
|
"failed": "Failed to migrate your data.",
|
||||||
|
"inProgress": "Please hold, we are migrating your data. This shouldn't take long."
|
||||||
},
|
},
|
||||||
"dmca": {
|
"dmca": {
|
||||||
"title": "",
|
"title": "DMCA",
|
||||||
"text": ""
|
"text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Millennium Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reporting infringements on this platform, thereby adhering to legal protocols for takedown requests, which, like, you know, it's all about, like, maintaining the integrity of intellectual property, and, um, making sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where you have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, you know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where you're, like, balancing, um, the rights of the, you know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, you know, you're, like, seeking justice in the digital wilderness, and, uh, striving for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, you know?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
@ -46,7 +151,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"copy": "Copy"
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"next": "Next"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"unsaved": "You have unsaved changes",
|
"unsaved": "You have unsaved changes",
|
||||||
@ -96,6 +203,23 @@
|
|||||||
"failed": "Failed to load sessions",
|
"failed": "Failed to load sessions",
|
||||||
"deviceNameLabel": "Device name",
|
"deviceNameLabel": "Device name",
|
||||||
"removeDevice": "Remove"
|
"removeDevice": "Remove"
|
||||||
|
},
|
||||||
|
"accountDetails": {
|
||||||
|
"editProfile": "Edit",
|
||||||
|
"deviceNameLabel": "Device name",
|
||||||
|
"deviceNamePlaceholder": "Fremen tablet",
|
||||||
|
"logoutButton": "Log out"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"title": "Actions",
|
||||||
|
"delete": {
|
||||||
|
"title": "Delete account",
|
||||||
|
"text": "This action is irreversible. All data will be deleted and nothing can be recovered.",
|
||||||
|
"button": "Delete account",
|
||||||
|
"confirmTitle": "Are you sure?",
|
||||||
|
"confirmDescription": "Are you sure you want to delete your account? All your data will be lost!",
|
||||||
|
"confirmButton": "Delete account"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
@ -104,7 +228,11 @@
|
|||||||
"languageDescription": "Language applied to the entire application."
|
"languageDescription": "Language applied to the entire application."
|
||||||
},
|
},
|
||||||
"captions": {
|
"captions": {
|
||||||
"title": "Captions"
|
"title": "Captions",
|
||||||
|
"previewQuote": "I must not fear. Fear is the mind-killer.",
|
||||||
|
"backgroundLabel": "Background opacity",
|
||||||
|
"textSizeLabel": "Text size",
|
||||||
|
"colorLabel": "Color"
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
"title": "Connections",
|
"title": "Connections",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { NotFoundPart } from "@/pages/parts/errors/ErrorWrapperPart";
|
import { NotFoundPart } from "@/pages/parts/errors/NotFoundPart";
|
||||||
|
|
||||||
export function NotFoundPage() {
|
export function NotFoundPage() {
|
||||||
return <NotFoundPart />;
|
return <NotFoundPart />;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
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";
|
||||||
@ -30,6 +31,7 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
|||||||
const [colorA, setColorA] = useState("#2E65CF");
|
const [colorA, setColorA] = useState("#2E65CF");
|
||||||
const [colorB, setColorB] = useState("#2E65CF");
|
const [colorB, setColorB] = useState("#2E65CF");
|
||||||
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
||||||
|
const { t } = useTranslation();
|
||||||
// TODO validate device and account before next step
|
// TODO validate device and account before next step
|
||||||
|
|
||||||
const nextStep = useCallback(() => {
|
const nextStep = useCallback(() => {
|
||||||
@ -47,24 +49,36 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
|||||||
<LargeCard>
|
<LargeCard>
|
||||||
<LargeCardText
|
<LargeCardText
|
||||||
icon={<Icon icon={Icons.USER} />}
|
icon={<Icon icon={Icons.USER} />}
|
||||||
title="Account information"
|
title={t("auth.register.information.title") ?? undefined}
|
||||||
>
|
>
|
||||||
Set up your account.... OR ELSE!
|
{t("auth.register.information.header")}
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<AuthInputBox
|
<AuthInputBox
|
||||||
label="Device name"
|
label={t("auth.deviceNameLabel") ?? undefined}
|
||||||
value={device}
|
value={device}
|
||||||
onChange={setDevice}
|
onChange={setDevice}
|
||||||
placeholder="Muad'Dib's Nintendo Switch"
|
placeholder={t("auth.deviceNamePlaceholder") ?? undefined}
|
||||||
|
/>
|
||||||
|
<ColorPicker
|
||||||
|
label={t("auth.register.information.color1")}
|
||||||
|
value={colorA}
|
||||||
|
onInput={setColorA}
|
||||||
|
/>
|
||||||
|
<ColorPicker
|
||||||
|
label={t("auth.register.information.color2")}
|
||||||
|
value={colorB}
|
||||||
|
onInput={setColorB}
|
||||||
|
/>
|
||||||
|
<IconPicker
|
||||||
|
label={t("auth.register.information.icon")}
|
||||||
|
value={userIcon}
|
||||||
|
onInput={setUserIcon}
|
||||||
/>
|
/>
|
||||||
<ColorPicker label="First color" value={colorA} onInput={setColorA} />
|
|
||||||
<ColorPicker label="Second color" value={colorB} onInput={setColorB} />
|
|
||||||
<IconPicker label="User icon" value={userIcon} onInput={setUserIcon} />
|
|
||||||
</div>
|
</div>
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => nextStep()}>
|
<Button theme="purple" onClick={() => nextStep()}>
|
||||||
Next
|
{t("actions.next")}
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
</LargeCard>
|
</LargeCard>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||||
@ -24,12 +25,13 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||||||
const { login, restore, importData } = useAuth();
|
const { login, restore, importData } = useAuth();
|
||||||
const progressItems = useProgressStore((store) => store.items);
|
const progressItems = useProgressStore((store) => store.items);
|
||||||
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
|
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [result, execute] = useAsyncFn(
|
const [result, execute] = useAsyncFn(
|
||||||
async (inputMnemonic: string, inputdevice: string) => {
|
async (inputMnemonic: string, inputdevice: string) => {
|
||||||
// TODO verify valid device input
|
// TODO verify valid device input
|
||||||
if (!verifyValidMnemonic(inputMnemonic))
|
if (!verifyValidMnemonic(inputMnemonic))
|
||||||
throw new Error("Invalid or incomplete passphrase");
|
throw new Error(t("auth.login.validationError") ?? undefined);
|
||||||
|
|
||||||
const account = await login({
|
const account = await login({
|
||||||
mnemonic: inputMnemonic,
|
mnemonic: inputMnemonic,
|
||||||
@ -49,25 +51,23 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LargeCard top={<BrandPill backgroundClass="bg-[#161527]" />}>
|
<LargeCard top={<BrandPill backgroundClass="bg-[#161527]" />}>
|
||||||
<LargeCardText title="Login to your account">
|
<LargeCardText title={t("auth.login.title")}>
|
||||||
Oh, you're asking for the key to my top-secret lair, also known as
|
{t("auth.login.description")}
|
||||||
The Fortress of Wordsmithery, accessed only by reciting the sacred
|
|
||||||
incantation of the 12-word passphrase!
|
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<AuthInputBox
|
<AuthInputBox
|
||||||
label="12-Word Passphrase"
|
label={t("auth.login.passphraseLabel") ?? undefined}
|
||||||
value={mnemonic}
|
value={mnemonic}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
name="username"
|
name="username"
|
||||||
onChange={setMnemonic}
|
onChange={setMnemonic}
|
||||||
placeholder="Passphrase"
|
placeholder={t("auth.login.passphrasePlaceholder") ?? undefined}
|
||||||
/>
|
/>
|
||||||
<AuthInputBox
|
<AuthInputBox
|
||||||
label="Device name"
|
label={t("auth.deviceNameLabel") ?? undefined}
|
||||||
value={device}
|
value={device}
|
||||||
onChange={setDevice}
|
onChange={setDevice}
|
||||||
placeholder="Device"
|
placeholder={t("auth.deviceNamePlaceholder") ?? undefined}
|
||||||
/>
|
/>
|
||||||
{result.error && !result.loading ? (
|
{result.error && !result.loading ? (
|
||||||
<p className="text-authentication-errorText">
|
<p className="text-authentication-errorText">
|
||||||
@ -82,7 +82,7 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||||||
loading={result.loading}
|
loading={result.loading}
|
||||||
onClick={() => execute(mnemonic, device)}
|
onClick={() => execute(mnemonic, device)}
|
||||||
>
|
>
|
||||||
LET ME IN!
|
{t("auth.login.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
</LargeCard>
|
</LargeCard>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { genMnemonic } from "@/backend/accounts/crypto";
|
import { genMnemonic } from "@/backend/accounts/crypto";
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
@ -16,18 +17,21 @@ interface PassphraseGeneratePartProps {
|
|||||||
|
|
||||||
export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
|
export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
|
||||||
const mnemonic = useMemo(() => genMnemonic(), []);
|
const mnemonic = useMemo(() => genMnemonic(), []);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LargeCard>
|
<LargeCard>
|
||||||
<LargeCardText title="Your passphrase" icon={<Icon icon={Icons.USER} />}>
|
<LargeCardText
|
||||||
If you lose this, you're a silly goose and will be posted on the
|
title={t("auth.generate.title")}
|
||||||
wall of shame™️
|
icon={<Icon icon={Icons.USER} />}
|
||||||
|
>
|
||||||
|
{t("auth.generate.description")}
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<PassphraseDisplay mnemonic={mnemonic} />
|
<PassphraseDisplay mnemonic={mnemonic} />
|
||||||
|
|
||||||
<LargeCardButtons>
|
<LargeCardButtons>
|
||||||
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
|
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
|
||||||
NEXT!
|
{t("actions.next")}
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
</LargeCard>
|
</LargeCard>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
@ -21,18 +22,18 @@ interface TrustBackendPartProps {
|
|||||||
export function TrustBackendPart(props: TrustBackendPartProps) {
|
export function TrustBackendPart(props: TrustBackendPartProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const backendUrl = useBackendUrl();
|
const backendUrl = useBackendUrl();
|
||||||
const backendHostname = useMemo(
|
const hostname = useMemo(() => new URL(backendUrl).hostname, [backendUrl]);
|
||||||
() => new URL(backendUrl).hostname,
|
|
||||||
[backendUrl]
|
|
||||||
);
|
|
||||||
const result = useAsync(() => {
|
const result = useAsync(() => {
|
||||||
return getBackendMeta(conf().BACKEND_URL);
|
return getBackendMeta(conf().BACKEND_URL);
|
||||||
}, [backendUrl]);
|
}, [backendUrl]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
let cardContent = (
|
let cardContent = (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-white font-bold text-lg">Failed to reach backend</h3>
|
<h3 className="text-white font-bold text-lg">
|
||||||
<p>Did you configure it correctly?</p>
|
{t("auth.trust.failed.title")}
|
||||||
|
</h3>
|
||||||
|
<p>{t("auth.trust.failed.text")}</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
if (result.loading) cardContent = <Loading />;
|
if (result.loading) cardContent = <Loading />;
|
||||||
@ -47,10 +48,12 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
|
|||||||
return (
|
return (
|
||||||
<LargeCard>
|
<LargeCard>
|
||||||
<LargeCardText
|
<LargeCardText
|
||||||
title="Do you trust this host?"
|
title={t("auth.trust.title")}
|
||||||
icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />}
|
icon={<Icon icon={Icons.CIRCLE_EXCLAMATION} />}
|
||||||
>
|
>
|
||||||
Do you trust <span className="text-white">{backendHostname}</span>?
|
<Trans i18nKey="auth.trust.host">
|
||||||
|
<span className="text-white">{{ hostname }}</span>
|
||||||
|
</Trans>
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
|
|
||||||
<div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8">
|
<div className="border border-authentication-border rounded-xl px-4 py-8 flex flex-col items-center space-y-2 my-8">
|
||||||
@ -61,10 +64,10 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
|
|||||||
theme="purple"
|
theme="purple"
|
||||||
onClick={() => result.value && props.onNext?.(result.value)}
|
onClick={() => result.value && props.onNext?.(result.value)}
|
||||||
>
|
>
|
||||||
I pledge my life to the United States
|
{t("auth.trust.yes")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button theme="secondary" onClick={() => history.push("/")}>
|
<Button theme="secondary" onClick={() => history.push("/")}>
|
||||||
I WILL NEVER SUCCUMB!
|
{t("auth.trust.no")}
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
</LargeCard>
|
</LargeCard>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { updateSettings } from "@/backend/accounts/settings";
|
import { updateSettings } from "@/backend/accounts/settings";
|
||||||
@ -40,24 +41,26 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||||||
const applicationTheme = useThemeStore((store) => store.theme);
|
const applicationTheme = useThemeStore((store) => store.theme);
|
||||||
|
|
||||||
const backendUrl = useBackendUrl();
|
const backendUrl = useBackendUrl();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||||
|
|
||||||
const [result, execute] = useAsyncFn(
|
const [result, execute] = useAsyncFn(
|
||||||
async (inputMnemonic: string) => {
|
async (inputMnemonic: string) => {
|
||||||
if (!props.mnemonic || !props.userData)
|
if (!props.mnemonic || !props.userData)
|
||||||
throw new Error("Data is not valid");
|
throw new Error(t("auth.verify.invalidData") ?? undefined);
|
||||||
|
|
||||||
let recaptchaToken: string | undefined;
|
let recaptchaToken: string | undefined;
|
||||||
if (props.hasCaptcha) {
|
if (props.hasCaptcha) {
|
||||||
recaptchaToken = executeRecaptcha
|
recaptchaToken = executeRecaptcha
|
||||||
? await executeRecaptcha()
|
? await executeRecaptcha()
|
||||||
: undefined;
|
: undefined;
|
||||||
if (!recaptchaToken) throw new Error("ReCaptcha validation failed");
|
if (!recaptchaToken)
|
||||||
|
throw new Error(t("auth.verify.recaptchaFailed") ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputMnemonic !== props.mnemonic)
|
if (inputMnemonic !== props.mnemonic)
|
||||||
throw new Error("Passphrase doesn't match");
|
throw new Error(t("auth.verify.noMatch") ?? undefined);
|
||||||
|
|
||||||
const account = await register({
|
const account = await register({
|
||||||
mnemonic: inputMnemonic,
|
mnemonic: inputMnemonic,
|
||||||
@ -85,13 +88,12 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||||||
<form>
|
<form>
|
||||||
<LargeCardText
|
<LargeCardText
|
||||||
icon={<Icon icon={Icons.CIRCLE_CHECK} />}
|
icon={<Icon icon={Icons.CIRCLE_CHECK} />}
|
||||||
title="Enter your passphrase"
|
title={t("auth.verify.title")}
|
||||||
>
|
>
|
||||||
If you've already lost it, how will you ever be able to take care
|
{t("auth.verify.description")}
|
||||||
of a child?
|
|
||||||
</LargeCardText>
|
</LargeCardText>
|
||||||
<AuthInputBox
|
<AuthInputBox
|
||||||
label="Your passphrase"
|
label={t("auth.verify.passphraseLabel") ?? undefined}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
name="username"
|
name="username"
|
||||||
value={mnemonic}
|
value={mnemonic}
|
||||||
@ -108,7 +110,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||||||
loading={result.loading}
|
loading={result.loading}
|
||||||
onClick={() => execute(mnemonic)}
|
onClick={() => execute(mnemonic)}
|
||||||
>
|
>
|
||||||
Register
|
{t("auth.verify.register")}
|
||||||
</Button>
|
</Button>
|
||||||
</LargeCardButtons>
|
</LargeCardButtons>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
@ -10,6 +11,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
|||||||
const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>(
|
const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
typeof props.error === "string" ? props.error : props.error.message;
|
typeof props.error === "string" ? props.error : props.error.message;
|
||||||
@ -32,7 +34,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
|||||||
// I didn't put a <Transition> here because it'd fade out, then jump height weirdly
|
// I didn't put a <Transition> here because it'd fade out, then jump height weirdly
|
||||||
<div className="w-full bg-errors-card p-6 rounded-lg">
|
<div className="w-full bg-errors-card p-6 rounded-lg">
|
||||||
<div className="flex justify-between items-center pb-2 border-b border-errors-border">
|
<div className="flex justify-between items-center pb-2 border-b border-errors-border">
|
||||||
<span className="text-white font-medium">Error details</span>
|
<span className="text-white font-medium">{t("errors.details")}</span>
|
||||||
<div className="flex justify-center items-center gap-3">
|
<div className="flex justify-center items-center gap-3">
|
||||||
<Button
|
<Button
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
@ -42,12 +44,12 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
|||||||
{hasCopied ? (
|
{hasCopied ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon={Icons.CHECKMARK} className="text-xs mr-3" />
|
<Icon icon={Icons.CHECKMARK} className="text-xs mr-3" />
|
||||||
Copied
|
{t("actions.copied")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
|
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
|
||||||
Copy
|
{t("actions.copy")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ButtonPlain } from "@/components/buttons/Button";
|
import { ButtonPlain } from "@/components/buttons/Button";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { IconPill } from "@/components/layout/IconPill";
|
import { IconPill } from "@/components/layout/IconPill";
|
||||||
@ -10,20 +12,22 @@ export function ErrorPart(props: { error: any; errorInfo: any }) {
|
|||||||
error: props.error,
|
error: props.error,
|
||||||
errorInfo: props.errorInfo,
|
errorInfo: props.errorInfo,
|
||||||
});
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-1 flex-col">
|
<div className="relative flex flex-1 flex-col">
|
||||||
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.EYE_SLASH}>It broke</IconPill>
|
<IconPill icon={Icons.EYE_SLASH}>{t("errors.badge")}</IconPill>
|
||||||
<Title>Failed to load meta data</Title>
|
<Title>{t("errors.title")}</Title>
|
||||||
<Paragraph>{data}</Paragraph>
|
<Paragraph>{data}</Paragraph>
|
||||||
<ButtonPlain
|
<ButtonPlain
|
||||||
theme="purple"
|
theme="purple"
|
||||||
className="mt-6 md:px-12 p-2.5"
|
className="mt-6 md:px-12 p-2.5"
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
>
|
>
|
||||||
Reload the page
|
{t("errors.reloadPage")}
|
||||||
</ButtonPlain>
|
</ButtonPlain>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
</ErrorLayout>
|
</ErrorLayout>
|
||||||
|
@ -15,29 +15,22 @@ export function NotFoundPart() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex flex-1 flex-col">
|
<div className="relative flex flex-1 flex-col">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{t("notFound.genericTitle")}</title>
|
<title>{t("notFound.badge")}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.EYE_SLASH}>
|
<IconPill icon={Icons.EYE_SLASH}>{t("notFound.badge")}</IconPill>
|
||||||
{t("notFound.genericTitle")}
|
<Title>{t("notFound.title")}</Title>
|
||||||
</IconPill>
|
<Paragraph>{t("notFound.message")}</Paragraph>
|
||||||
<Title>Failed to load meta data</Title>
|
|
||||||
<Paragraph>
|
|
||||||
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
|
||||||
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
|
||||||
Please don't be angwy, wittle movie-web ish twying so hard.
|
|
||||||
Can you find it in your heart to forgive? UwU 💖
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
<Button
|
||||||
href="/"
|
href="/"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
padding="md:px-12 p-2.5"
|
padding="md:px-12 p-2.5"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Go home
|
{t("notFound.goHome")}
|
||||||
</Button>
|
</Button>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
</ErrorLayout>
|
</ErrorLayout>
|
@ -46,7 +46,7 @@ export function BookmarksPart() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title={t("search.bookmarks") || "Bookmarks"}
|
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||||
icon={Icons.BOOKMARK}
|
icon={Icons.BOOKMARK}
|
||||||
>
|
>
|
||||||
<EditButton editing={editing} onEdit={setEditing} />
|
<EditButton editing={editing} onEdit={setEditing} />
|
||||||
|
@ -31,7 +31,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||||||
if (hour < 12) time = "morning";
|
if (hour < 12) time = "morning";
|
||||||
else if (hour < 19) time = "day";
|
else if (hour < 19) time = "day";
|
||||||
|
|
||||||
const title = t(`search.title.${time}`);
|
const title = t(`home.titles.${time}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThinContainer>
|
<ThinContainer>
|
||||||
@ -51,9 +51,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
value={search}
|
value={search}
|
||||||
onUnFocus={setSearchUnFocus}
|
onUnFocus={setSearchUnFocus}
|
||||||
placeholder={
|
placeholder={t("home.search.placeholder")}
|
||||||
t("search.placeholder") || "What do you want to watch?"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Sticky>
|
</Sticky>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,7 +44,7 @@ export function WatchingPart() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title={t("search.continueWatching") || "Continue Watching"}
|
title={t("home.continueWatching.sectionTitle")}
|
||||||
icon={Icons.CLOCK}
|
icon={Icons.CLOCK}
|
||||||
>
|
>
|
||||||
<EditButton editing={editing} onEdit={setEditing} />
|
<EditButton editing={editing} onEdit={setEditing} />
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BrandPill } from "@/components/layout/BrandPill";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
||||||
|
|
||||||
export function MigrationPart() {
|
export function MigrationPart() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center h-screen text-center font-medium">
|
<div className="flex flex-col justify-center items-center h-screen text-center font-medium">
|
||||||
{/* Overlaid elements */}
|
{/* Overlaid elements */}
|
||||||
@ -14,8 +17,7 @@ export function MigrationPart() {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<Loading />
|
<Loading />
|
||||||
<p className="max-w-[19rem] mt-3 mb-12 text-type-secondary">
|
<p className="max-w-[19rem] mt-3 mb-12 text-type-secondary">
|
||||||
Please hold, we are migrating your data. This shouldn't take long.
|
{t("screens.migration.inProgress")}
|
||||||
Also, fuck you.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import type { AsyncReturnType } from "type-fest";
|
import type { AsyncReturnType } from "type-fest";
|
||||||
@ -18,6 +19,7 @@ export interface MetaPartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MetaPart(props: MetaPartProps) {
|
export function MetaPart(props: MetaPartProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const params = useParams<{
|
const params = useParams<{
|
||||||
media: string;
|
media: string;
|
||||||
episode?: string;
|
episode?: string;
|
||||||
@ -70,21 +72,18 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.WAND}>Failed to load</IconPill>
|
<IconPill icon={Icons.WAND}>
|
||||||
<Title>Failed to load meta data</Title>
|
{t("player.metadata.failed.badge")}
|
||||||
<Paragraph>
|
</IconPill>
|
||||||
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
<Title>{t("player.metadata.failed.title")}</Title>
|
||||||
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
<Paragraph>{t("player.metadata.failed.text")}</Paragraph>
|
||||||
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
|
||||||
you find it in your heart to forgive? UwU 💖
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
<Button
|
||||||
href="/"
|
href="/"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
padding="md:px-12 p-2.5"
|
padding="md:px-12 p-2.5"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Go home
|
{t("player.metadata.failed.homeButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
</ErrorLayout>
|
</ErrorLayout>
|
||||||
@ -95,21 +94,18 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
<IconPill icon={Icons.WAND}>
|
||||||
<Title>This media doesnt exist</Title>
|
{t("player.metadata.notFound.badge")}
|
||||||
<Paragraph>
|
</IconPill>
|
||||||
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
<Title>{t("player.metadata.notFound.title")}</Title>
|
||||||
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
<Paragraph>{t("player.metadata.notFound.text")}</Paragraph>
|
||||||
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
|
||||||
you find it in your heart to forgive? UwU 💖
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
<Button
|
||||||
href="/"
|
href="/"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
padding="md:px-12 p-2.5"
|
padding="md:px-12 p-2.5"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Go home
|
{t("player.metadata.notFound.homeButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
</ErrorLayout>
|
</ErrorLayout>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
import { IconPill } from "@/components/layout/IconPill";
|
import { IconPill } from "@/components/layout/IconPill";
|
||||||
@ -9,26 +11,22 @@ import { usePlayerStore } from "@/stores/player/store";
|
|||||||
import { ErrorCard } from "../errors/ErrorCard";
|
import { ErrorCard } from "../errors/ErrorCard";
|
||||||
|
|
||||||
export function PlaybackErrorPart() {
|
export function PlaybackErrorPart() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const playbackError = usePlayerStore((s) => s.interface.error);
|
const playbackError = usePlayerStore((s) => s.interface.error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
<IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
|
||||||
<Title>Goo goo gaa gaa</Title>
|
<Title>{t("player.playbackError.title")}</Title>
|
||||||
<Paragraph>
|
<Paragraph>{t("player.playbackError.text")}</Paragraph>
|
||||||
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
|
||||||
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
|
||||||
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
|
||||||
you find it in your heart to forgive? UwU 💖
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
<Button
|
||||||
href="/"
|
href="/"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
padding="md:px-12 p-2.5"
|
padding="md:px-12 p-2.5"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Go home
|
{t("player.playbackError.homeButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
<ErrorContainer maxWidth="max-w-[45rem]">
|
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icons } from "@/components/Icon";
|
import { Icons } from "@/components/Icon";
|
||||||
@ -18,6 +19,7 @@ export interface ScrapeErrorPartProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const error = useMemo(() => {
|
const error = useMemo(() => {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const amountError = Object.values(data.sources).filter(
|
const amountError = Object.values(data.sources).filter(
|
||||||
@ -36,21 +38,18 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
|||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
<IconPill icon={Icons.WAND}>
|
||||||
<Title>Goo goo gaa gaa</Title>
|
{t("player.scraping.notFound.badge")}
|
||||||
<Paragraph>
|
</IconPill>
|
||||||
Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost
|
<Title>{t("player.scraping.notFound.title")}</Title>
|
||||||
bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`)
|
<Paragraph>{t("player.scraping.notFound.text")}</Paragraph>
|
||||||
Please don't be angwy, wittle movie-web ish twying so hard. Can
|
|
||||||
you find it in your heart to forgive? UwU 💖
|
|
||||||
</Paragraph>
|
|
||||||
<Button
|
<Button
|
||||||
href="/"
|
href="/"
|
||||||
theme="purple"
|
theme="purple"
|
||||||
padding="md:px-12 p-2.5"
|
padding="md:px-12 p-2.5"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
>
|
>
|
||||||
Go home
|
{t("player.scraping.notFound.homeButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</ErrorContainer>
|
</ErrorContainer>
|
||||||
<ErrorContainer maxWidth="max-w-[45rem]">
|
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||||
|
@ -30,9 +30,9 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
|||||||
{!props.failed ? (
|
{!props.failed ? (
|
||||||
<div>
|
<div>
|
||||||
{(props.results ?? 0) > 0 ? (
|
{(props.results ?? 0) > 0 ? (
|
||||||
<p>{t("search.allResults")}</p>
|
<p>{t("home.search.allResults")}</p>
|
||||||
) : (
|
) : (
|
||||||
<p>{t("search.noResults")}</p>
|
<p>{t("home.search.noResults")}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -40,7 +40,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
|||||||
{/* Error result */}
|
{/* Error result */}
|
||||||
{props.failed ? (
|
{props.failed ? (
|
||||||
<div>
|
<div>
|
||||||
<p>{t("search.allFailed")}</p>
|
<p>{t("home.search.failed")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -72,7 +72,7 @@ export function SearchListPart({ searchQuery }: { searchQuery: string }) {
|
|||||||
{results.length > 0 ? (
|
{results.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
title={t("search.headingTitle") || "Search results"}
|
title={t("home.search.sectionTitle")}
|
||||||
icon={Icons.SEARCH}
|
icon={Icons.SEARCH}
|
||||||
/>
|
/>
|
||||||
<MediaGrid>
|
<MediaGrid>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { deleteUser } from "@/backend/accounts/user";
|
import { deleteUser } from "@/backend/accounts/user";
|
||||||
@ -10,6 +11,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
|||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
export function AccountActionsPart() {
|
export function AccountActionsPart() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const url = useBackendUrl();
|
const url = useBackendUrl();
|
||||||
const account = useAuthStore((s) => s.account);
|
const account = useAuthStore((s) => s.account);
|
||||||
const { logout } = useAuthData();
|
const { logout } = useAuthData();
|
||||||
@ -26,16 +28,15 @@ export function AccountActionsPart() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading2 border>Actions</Heading2>
|
<Heading2 border>{t("settings.account.actions.title")}</Heading2>
|
||||||
<SolidSettingsCard
|
<SolidSettingsCard
|
||||||
paddingClass="px-6 py-12"
|
paddingClass="px-6 py-12"
|
||||||
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12"
|
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Heading3>Delete account</Heading3>
|
<Heading3>{t("settings.account.actions.delete.title")}</Heading3>
|
||||||
<p className="text-type-text">
|
<p className="text-type-text">
|
||||||
This action is irreversible. All data will be deleted and nothing
|
{t("settings.account.actions.delete.text")}
|
||||||
can be recovered.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start lg:justify-end items-center">
|
<div className="flex justify-start lg:justify-end items-center">
|
||||||
@ -44,23 +45,24 @@ export function AccountActionsPart() {
|
|||||||
loading={deleteResult.loading}
|
loading={deleteResult.loading}
|
||||||
onClick={deleteModal.show}
|
onClick={deleteModal.show}
|
||||||
>
|
>
|
||||||
Delete account
|
{t("settings.account.actions.delete.button")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</SolidSettingsCard>
|
</SolidSettingsCard>
|
||||||
<Modal id={deleteModal.id}>
|
<Modal id={deleteModal.id}>
|
||||||
<ModalCard>
|
<ModalCard>
|
||||||
<Heading2 className="!mt-0">Are you sure?</Heading2>
|
<Heading2 className="!mt-0">
|
||||||
|
{t("settings.account.actions.delete.confirmTitle")}
|
||||||
|
</Heading2>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
Are you sure you want to delete your account? All your data will be
|
{t("settings.account.actions.delete.confirmDescription")}
|
||||||
lost!
|
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Button
|
<Button
|
||||||
theme="danger"
|
theme="danger"
|
||||||
loading={deleteResult.loading}
|
loading={deleteResult.loading}
|
||||||
onClick={deleteExec}
|
onClick={deleteExec}
|
||||||
>
|
>
|
||||||
Delete account
|
{t("settings.account.actions.delete.confirmButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalCard>
|
</ModalCard>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Avatar } from "@/components/Avatar";
|
import { Avatar } from "@/components/Avatar";
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
@ -18,6 +20,7 @@ export function AccountEditPart(props: {
|
|||||||
userIcon: UserIcons;
|
userIcon: UserIcons;
|
||||||
setUserIcon: (s: UserIcons) => void;
|
setUserIcon: (s: UserIcons) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const profileEditModal = useModal("profile-edit");
|
const profileEditModal = useModal("profile-edit");
|
||||||
|
|
||||||
@ -50,7 +53,7 @@ export function AccountEditPart(props: {
|
|||||||
onClick={profileEditModal.show}
|
onClick={profileEditModal.show}
|
||||||
>
|
>
|
||||||
<Icon icon={Icons.EDIT} />
|
<Icon icon={Icons.EDIT} />
|
||||||
Edit
|
{t("settings.account.accountDetails.editProfile")}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -58,14 +61,20 @@ export function AccountEditPart(props: {
|
|||||||
<div>
|
<div>
|
||||||
<div className="space-y-8 max-w-xs">
|
<div className="space-y-8 max-w-xs">
|
||||||
<AuthInputBox
|
<AuthInputBox
|
||||||
label="Device name"
|
label={
|
||||||
placeholder="Fremen tablet"
|
t("settings.account.accountDetails.deviceNameLabel") ??
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
t("settings.account.accountDetails.deviceNamePlaceholder") ??
|
||||||
|
undefined
|
||||||
|
}
|
||||||
value={props.deviceName}
|
value={props.deviceName}
|
||||||
onChange={(value) => props.setDeviceName(value)}
|
onChange={(value) => props.setDeviceName(value)}
|
||||||
/>
|
/>
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button theme="danger" onClick={logout}>
|
<Button theme="danger" onClick={logout}>
|
||||||
Log out
|
{t("settings.account.accountDetails.logoutButton")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
@ -19,6 +20,7 @@ export function CaptionPreview(props: {
|
|||||||
styling: SubtitleStyling;
|
styling: SubtitleStyling;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
@ -50,7 +52,7 @@ export function CaptionPreview(props: {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CaptionCue
|
<CaptionCue
|
||||||
text="I must not fear. Fear is the mind-killer."
|
text={t("settings.captions.previewQuote") ?? undefined}
|
||||||
styling={props.styling}
|
styling={props.styling}
|
||||||
overrideCasing={false}
|
overrideCasing={false}
|
||||||
/>
|
/>
|
||||||
@ -66,15 +68,16 @@ export function CaptionsPart(props: {
|
|||||||
styling: SubtitleStyling;
|
styling: SubtitleStyling;
|
||||||
setStyling: (s: SubtitleStyling) => void;
|
setStyling: (s: SubtitleStyling) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [fullscreenPreview, setFullscreenPreview] = useState(false);
|
const [fullscreenPreview, setFullscreenPreview] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Heading1 border>Captions</Heading1>
|
<Heading1 border>{t("settings.captions.title")}</Heading1>
|
||||||
<div className="grid md:grid-cols-[1fr,356px] gap-8">
|
<div className="grid md:grid-cols-[1fr,356px] gap-8">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<CaptionSetting
|
<CaptionSetting
|
||||||
label="Background opacity"
|
label={t("settings.captions.backgroundLabel")}
|
||||||
max={100}
|
max={100}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(v) =>
|
onChange={(v) =>
|
||||||
@ -84,7 +87,7 @@ export function CaptionsPart(props: {
|
|||||||
textTransformer={(s) => `${s}%`}
|
textTransformer={(s) => `${s}%`}
|
||||||
/>
|
/>
|
||||||
<CaptionSetting
|
<CaptionSetting
|
||||||
label="Text size"
|
label={t("settings.captions.textSizeLabel")}
|
||||||
max={200}
|
max={200}
|
||||||
min={1}
|
min={1}
|
||||||
textTransformer={(s) => `${s}%`}
|
textTransformer={(s) => `${s}%`}
|
||||||
@ -94,7 +97,9 @@ export function CaptionsPart(props: {
|
|||||||
value={props.styling.size * 100}
|
value={props.styling.size * 100}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Menu.FieldTitle>Color</Menu.FieldTitle>
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.captions.colorLabel")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
{colors.map((v) => (
|
{colors.map((v) => (
|
||||||
<ColorOption
|
<ColorOption
|
||||||
|
Loading…
x
Reference in New Issue
Block a user