mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
More localisation
Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
parent
d20fc4bf82
commit
75933e7080
21 changed files with 291 additions and 132 deletions
|
@ -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": {
|
||||
"name": "movie-web"
|
||||
},
|
||||
|
@ -9,9 +65,57 @@
|
|||
},
|
||||
"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": {
|
||||
"mediaList": {
|
||||
"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": {
|
||||
|
@ -21,16 +125,17 @@
|
|||
"loadingUser": "Loading your profile",
|
||||
"loadingApp": "Loading application",
|
||||
"loadingUserError": {
|
||||
"text": "",
|
||||
"textWithReset": "",
|
||||
"text": "Failed to load your profile",
|
||||
"textWithReset": "Failed to load your profile from your custom server, want to reset back to default?",
|
||||
"reset": "Reset custom server"
|
||||
},
|
||||
"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": {
|
||||
"title": "",
|
||||
"text": ""
|
||||
"title": "DMCA",
|
||||
"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": {
|
||||
|
@ -46,7 +151,9 @@
|
|||
}
|
||||
},
|
||||
"actions": {
|
||||
"copy": "Copy"
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"next": "Next"
|
||||
},
|
||||
"settings": {
|
||||
"unsaved": "You have unsaved changes",
|
||||
|
@ -96,6 +203,23 @@
|
|||
"failed": "Failed to load sessions",
|
||||
"deviceNameLabel": "Device name",
|
||||
"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": {
|
||||
|
@ -104,7 +228,11 @@
|
|||
"languageDescription": "Language applied to the entire application."
|
||||
},
|
||||
"captions": {
|
||||
"title": "Captions"
|
||||
"title": "Captions",
|
||||
"previewQuote": "I must not fear. Fear is the mind-killer.",
|
||||
"backgroundLabel": "Background opacity",
|
||||
"textSizeLabel": "Text size",
|
||||
"colorLabel": "Color"
|
||||
},
|
||||
"connections": {
|
||||
"title": "Connections",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { NotFoundPart } from "@/pages/parts/errors/ErrorWrapperPart";
|
||||
import { NotFoundPart } from "@/pages/parts/errors/NotFoundPart";
|
||||
|
||||
export function NotFoundPage() {
|
||||
return <NotFoundPart />;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||
|
@ -30,6 +31,7 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
|||
const [colorA, setColorA] = useState("#2E65CF");
|
||||
const [colorB, setColorB] = useState("#2E65CF");
|
||||
const [userIcon, setUserIcon] = useState<UserIcons>(UserIcons.USER);
|
||||
const { t } = useTranslation();
|
||||
// TODO validate device and account before next step
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
|
@ -47,24 +49,36 @@ export function AccountCreatePart(props: AccountCreatePartProps) {
|
|||
<LargeCard>
|
||||
<LargeCardText
|
||||
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>
|
||||
<div className="space-y-6">
|
||||
<AuthInputBox
|
||||
label="Device name"
|
||||
label={t("auth.deviceNameLabel") ?? undefined}
|
||||
value={device}
|
||||
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>
|
||||
<LargeCardButtons>
|
||||
<Button theme="purple" onClick={() => nextStep()}>
|
||||
Next
|
||||
{t("actions.next")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||
|
@ -24,12 +25,13 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||
const { login, restore, importData } = useAuth();
|
||||
const progressItems = useProgressStore((store) => store.items);
|
||||
const bookmarkItems = useBookmarkStore((store) => store.bookmarks);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [result, execute] = useAsyncFn(
|
||||
async (inputMnemonic: string, inputdevice: string) => {
|
||||
// TODO verify valid device input
|
||||
if (!verifyValidMnemonic(inputMnemonic))
|
||||
throw new Error("Invalid or incomplete passphrase");
|
||||
throw new Error(t("auth.login.validationError") ?? undefined);
|
||||
|
||||
const account = await login({
|
||||
mnemonic: inputMnemonic,
|
||||
|
@ -49,25 +51,23 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||
|
||||
return (
|
||||
<LargeCard top={<BrandPill backgroundClass="bg-[#161527]" />}>
|
||||
<LargeCardText title="Login to your account">
|
||||
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!
|
||||
<LargeCardText title={t("auth.login.title")}>
|
||||
{t("auth.login.description")}
|
||||
</LargeCardText>
|
||||
<div className="space-y-4">
|
||||
<AuthInputBox
|
||||
label="12-Word Passphrase"
|
||||
label={t("auth.login.passphraseLabel") ?? undefined}
|
||||
value={mnemonic}
|
||||
autoComplete="username"
|
||||
name="username"
|
||||
onChange={setMnemonic}
|
||||
placeholder="Passphrase"
|
||||
placeholder={t("auth.login.passphrasePlaceholder") ?? undefined}
|
||||
/>
|
||||
<AuthInputBox
|
||||
label="Device name"
|
||||
label={t("auth.deviceNameLabel") ?? undefined}
|
||||
value={device}
|
||||
onChange={setDevice}
|
||||
placeholder="Device"
|
||||
placeholder={t("auth.deviceNamePlaceholder") ?? undefined}
|
||||
/>
|
||||
{result.error && !result.loading ? (
|
||||
<p className="text-authentication-errorText">
|
||||
|
@ -82,7 +82,7 @@ export function LoginFormPart(props: LoginFormPartProps) {
|
|||
loading={result.loading}
|
||||
onClick={() => execute(mnemonic, device)}
|
||||
>
|
||||
LET ME IN!
|
||||
{t("auth.login.submit")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { genMnemonic } from "@/backend/accounts/crypto";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
|
@ -16,18 +17,21 @@ interface PassphraseGeneratePartProps {
|
|||
|
||||
export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) {
|
||||
const mnemonic = useMemo(() => genMnemonic(), []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<LargeCard>
|
||||
<LargeCardText title="Your passphrase" icon={<Icon icon={Icons.USER} />}>
|
||||
If you lose this, you're a silly goose and will be posted on the
|
||||
wall of shame™️
|
||||
<LargeCardText
|
||||
title={t("auth.generate.title")}
|
||||
icon={<Icon icon={Icons.USER} />}
|
||||
>
|
||||
{t("auth.generate.description")}
|
||||
</LargeCardText>
|
||||
<PassphraseDisplay mnemonic={mnemonic} />
|
||||
|
||||
<LargeCardButtons>
|
||||
<Button theme="purple" onClick={() => props.onNext?.(mnemonic)}>
|
||||
NEXT!
|
||||
{t("actions.next")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
|
@ -21,18 +22,18 @@ interface TrustBackendPartProps {
|
|||
export function TrustBackendPart(props: TrustBackendPartProps) {
|
||||
const history = useHistory();
|
||||
const backendUrl = useBackendUrl();
|
||||
const backendHostname = useMemo(
|
||||
() => new URL(backendUrl).hostname,
|
||||
[backendUrl]
|
||||
);
|
||||
const hostname = useMemo(() => new URL(backendUrl).hostname, [backendUrl]);
|
||||
const result = useAsync(() => {
|
||||
return getBackendMeta(conf().BACKEND_URL);
|
||||
}, [backendUrl]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let cardContent = (
|
||||
<>
|
||||
<h3 className="text-white font-bold text-lg">Failed to reach backend</h3>
|
||||
<p>Did you configure it correctly?</p>
|
||||
<h3 className="text-white font-bold text-lg">
|
||||
{t("auth.trust.failed.title")}
|
||||
</h3>
|
||||
<p>{t("auth.trust.failed.text")}</p>
|
||||
</>
|
||||
);
|
||||
if (result.loading) cardContent = <Loading />;
|
||||
|
@ -47,10 +48,12 @@ export function TrustBackendPart(props: TrustBackendPartProps) {
|
|||
return (
|
||||
<LargeCard>
|
||||
<LargeCardText
|
||||
title="Do you trust this host?"
|
||||
title={t("auth.trust.title")}
|
||||
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>
|
||||
|
||||
<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"
|
||||
onClick={() => result.value && props.onNext?.(result.value)}
|
||||
>
|
||||
I pledge my life to the United States
|
||||
{t("auth.trust.yes")}
|
||||
</Button>
|
||||
<Button theme="secondary" onClick={() => history.push("/")}>
|
||||
I WILL NEVER SUCCUMB!
|
||||
{t("auth.trust.no")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</LargeCard>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { updateSettings } from "@/backend/accounts/settings";
|
||||
|
@ -40,24 +41,26 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||
const applicationTheme = useThemeStore((store) => store.theme);
|
||||
|
||||
const backendUrl = useBackendUrl();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { executeRecaptcha } = useGoogleReCaptcha();
|
||||
|
||||
const [result, execute] = useAsyncFn(
|
||||
async (inputMnemonic: string) => {
|
||||
if (!props.mnemonic || !props.userData)
|
||||
throw new Error("Data is not valid");
|
||||
throw new Error(t("auth.verify.invalidData") ?? undefined);
|
||||
|
||||
let recaptchaToken: string | undefined;
|
||||
if (props.hasCaptcha) {
|
||||
recaptchaToken = executeRecaptcha
|
||||
? await executeRecaptcha()
|
||||
: undefined;
|
||||
if (!recaptchaToken) throw new Error("ReCaptcha validation failed");
|
||||
if (!recaptchaToken)
|
||||
throw new Error(t("auth.verify.recaptchaFailed") ?? undefined);
|
||||
}
|
||||
|
||||
if (inputMnemonic !== props.mnemonic)
|
||||
throw new Error("Passphrase doesn't match");
|
||||
throw new Error(t("auth.verify.noMatch") ?? undefined);
|
||||
|
||||
const account = await register({
|
||||
mnemonic: inputMnemonic,
|
||||
|
@ -85,13 +88,12 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||
<form>
|
||||
<LargeCardText
|
||||
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
|
||||
of a child?
|
||||
{t("auth.verify.description")}
|
||||
</LargeCardText>
|
||||
<AuthInputBox
|
||||
label="Your passphrase"
|
||||
label={t("auth.verify.passphraseLabel") ?? undefined}
|
||||
autoComplete="username"
|
||||
name="username"
|
||||
value={mnemonic}
|
||||
|
@ -108,7 +110,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
|
|||
loading={result.loading}
|
||||
onClick={() => execute(mnemonic)}
|
||||
>
|
||||
Register
|
||||
{t("auth.verify.register")}
|
||||
</Button>
|
||||
</LargeCardButtons>
|
||||
</form>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
@ -10,6 +11,7 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
|||
const hasCopiedUnsetDebounce = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorMessage =
|
||||
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
|
||||
<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">
|
||||
<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">
|
||||
<Button
|
||||
theme="secondary"
|
||||
|
@ -42,12 +44,12 @@ export function ErrorCard(props: { error: DisplayError | string }) {
|
|||
{hasCopied ? (
|
||||
<>
|
||||
<Icon icon={Icons.CHECKMARK} className="text-xs mr-3" />
|
||||
Copied
|
||||
{t("actions.copied")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon icon={Icons.COPY} className="text-2xl mr-3" />
|
||||
Copy
|
||||
{t("actions.copy")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ButtonPlain } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { IconPill } from "@/components/layout/IconPill";
|
||||
|
@ -10,20 +12,22 @@ export function ErrorPart(props: { error: any; errorInfo: any }) {
|
|||
error: props.error,
|
||||
errorInfo: props.errorInfo,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.EYE_SLASH}>It broke</IconPill>
|
||||
<Title>Failed to load meta data</Title>
|
||||
<IconPill icon={Icons.EYE_SLASH}>{t("errors.badge")}</IconPill>
|
||||
<Title>{t("errors.title")}</Title>
|
||||
<Paragraph>{data}</Paragraph>
|
||||
<ButtonPlain
|
||||
theme="purple"
|
||||
className="mt-6 md:px-12 p-2.5"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Reload the page
|
||||
{t("errors.reloadPage")}
|
||||
</ButtonPlain>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
||||
|
|
|
@ -15,29 +15,22 @@ export function NotFoundPart() {
|
|||
return (
|
||||
<div className="relative flex flex-1 flex-col">
|
||||
<Helmet>
|
||||
<title>{t("notFound.genericTitle")}</title>
|
||||
<title>{t("notFound.badge")}</title>
|
||||
</Helmet>
|
||||
<Navigation />
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center p-5 text-center">
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.EYE_SLASH}>
|
||||
{t("notFound.genericTitle")}
|
||||
</IconPill>
|
||||
<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>
|
||||
<IconPill icon={Icons.EYE_SLASH}>{t("notFound.badge")}</IconPill>
|
||||
<Title>{t("notFound.title")}</Title>
|
||||
<Paragraph>{t("notFound.message")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
Go home
|
||||
{t("notFound.goHome")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
|
@ -46,7 +46,7 @@ export function BookmarksPart() {
|
|||
return (
|
||||
<div>
|
||||
<SectionHeading
|
||||
title={t("search.bookmarks") || "Bookmarks"}
|
||||
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||
icon={Icons.BOOKMARK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
|
|
|
@ -31,7 +31,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||
if (hour < 12) time = "morning";
|
||||
else if (hour < 19) time = "day";
|
||||
|
||||
const title = t(`search.title.${time}`);
|
||||
const title = t(`home.titles.${time}`);
|
||||
|
||||
return (
|
||||
<ThinContainer>
|
||||
|
@ -51,9 +51,7 @@ export function HeroPart({ setIsSticky, searchParams }: HeroPartProps) {
|
|||
onChange={setSearch}
|
||||
value={search}
|
||||
onUnFocus={setSearchUnFocus}
|
||||
placeholder={
|
||||
t("search.placeholder") || "What do you want to watch?"
|
||||
}
|
||||
placeholder={t("home.search.placeholder")}
|
||||
/>
|
||||
</Sticky>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,7 @@ export function WatchingPart() {
|
|||
return (
|
||||
<div>
|
||||
<SectionHeading
|
||||
title={t("search.continueWatching") || "Continue Watching"}
|
||||
title={t("home.continueWatching.sectionTitle")}
|
||||
icon={Icons.CLOCK}
|
||||
>
|
||||
<EditButton editing={editing} onEdit={setEditing} />
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
|
||||
|
||||
export function MigrationPart() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center h-screen text-center font-medium">
|
||||
{/* Overlaid elements */}
|
||||
|
@ -14,8 +17,7 @@ export function MigrationPart() {
|
|||
{/* Content */}
|
||||
<Loading />
|
||||
<p className="max-w-[19rem] mt-3 mb-12 text-type-secondary">
|
||||
Please hold, we are migrating your data. This shouldn't take long.
|
||||
Also, fuck you.
|
||||
{t("screens.migration.inProgress")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
import type { AsyncReturnType } from "type-fest";
|
||||
|
@ -18,6 +19,7 @@ export interface MetaPartProps {
|
|||
}
|
||||
|
||||
export function MetaPart(props: MetaPartProps) {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams<{
|
||||
media: string;
|
||||
episode?: string;
|
||||
|
@ -70,21 +72,18 @@ export function MetaPart(props: MetaPartProps) {
|
|||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>Failed to load</IconPill>
|
||||
<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>
|
||||
<IconPill icon={Icons.WAND}>
|
||||
{t("player.metadata.failed.badge")}
|
||||
</IconPill>
|
||||
<Title>{t("player.metadata.failed.title")}</Title>
|
||||
<Paragraph>{t("player.metadata.failed.text")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
Go home
|
||||
{t("player.metadata.failed.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
||||
|
@ -95,21 +94,18 @@ export function MetaPart(props: MetaPartProps) {
|
|||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||
<Title>This media doesnt exist</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>
|
||||
<IconPill icon={Icons.WAND}>
|
||||
{t("player.metadata.notFound.badge")}
|
||||
</IconPill>
|
||||
<Title>{t("player.metadata.notFound.title")}</Title>
|
||||
<Paragraph>{t("player.metadata.notFound.text")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
Go home
|
||||
{t("player.metadata.notFound.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { IconPill } from "@/components/layout/IconPill";
|
||||
|
@ -9,26 +11,22 @@ import { usePlayerStore } from "@/stores/player/store";
|
|||
import { ErrorCard } from "../errors/ErrorCard";
|
||||
|
||||
export function PlaybackErrorPart() {
|
||||
const { t } = useTranslation();
|
||||
const playbackError = usePlayerStore((s) => s.interface.error);
|
||||
|
||||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||
<Title>Goo goo gaa gaa</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>
|
||||
<IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
|
||||
<Title>{t("player.playbackError.title")}</Title>
|
||||
<Paragraph>{t("player.playbackError.text")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
Go home
|
||||
{t("player.playbackError.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
|
@ -18,6 +19,7 @@ export interface ScrapeErrorPartProps {
|
|||
}
|
||||
|
||||
export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
||||
const { t } = useTranslation();
|
||||
const error = useMemo(() => {
|
||||
const data = props.data;
|
||||
const amountError = Object.values(data.sources).filter(
|
||||
|
@ -36,21 +38,18 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
|||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>Not found</IconPill>
|
||||
<Title>Goo goo gaa gaa</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>
|
||||
<IconPill icon={Icons.WAND}>
|
||||
{t("player.scraping.notFound.badge")}
|
||||
</IconPill>
|
||||
<Title>{t("player.scraping.notFound.title")}</Title>
|
||||
<Paragraph>{t("player.scraping.notFound.text")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
Go home
|
||||
{t("player.scraping.notFound.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
<ErrorContainer maxWidth="max-w-[45rem]">
|
||||
|
|
|
@ -30,9 +30,9 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
|||
{!props.failed ? (
|
||||
<div>
|
||||
{(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>
|
||||
) : null}
|
||||
|
@ -40,7 +40,7 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
|
|||
{/* Error result */}
|
||||
{props.failed ? (
|
||||
<div>
|
||||
<p>{t("search.allFailed")}</p>
|
||||
<p>{t("home.search.failed")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -72,7 +72,7 @@ export function SearchListPart({ searchQuery }: { searchQuery: string }) {
|
|||
{results.length > 0 ? (
|
||||
<div>
|
||||
<SectionHeading
|
||||
title={t("search.headingTitle") || "Search results"}
|
||||
title={t("home.search.sectionTitle")}
|
||||
icon={Icons.SEARCH}
|
||||
/>
|
||||
<MediaGrid>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { deleteUser } from "@/backend/accounts/user";
|
||||
|
@ -10,6 +11,7 @@ import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
|
|||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export function AccountActionsPart() {
|
||||
const { t } = useTranslation();
|
||||
const url = useBackendUrl();
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const { logout } = useAuthData();
|
||||
|
@ -26,16 +28,15 @@ export function AccountActionsPart() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Heading2 border>Actions</Heading2>
|
||||
<Heading2 border>{t("settings.account.actions.title")}</Heading2>
|
||||
<SolidSettingsCard
|
||||
paddingClass="px-6 py-12"
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12"
|
||||
>
|
||||
<div>
|
||||
<Heading3>Delete account</Heading3>
|
||||
<Heading3>{t("settings.account.actions.delete.title")}</Heading3>
|
||||
<p className="text-type-text">
|
||||
This action is irreversible. All data will be deleted and nothing
|
||||
can be recovered.
|
||||
{t("settings.account.actions.delete.text")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-start lg:justify-end items-center">
|
||||
|
@ -44,23 +45,24 @@ export function AccountActionsPart() {
|
|||
loading={deleteResult.loading}
|
||||
onClick={deleteModal.show}
|
||||
>
|
||||
Delete account
|
||||
{t("settings.account.actions.delete.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</SolidSettingsCard>
|
||||
<Modal id={deleteModal.id}>
|
||||
<ModalCard>
|
||||
<Heading2 className="!mt-0">Are you sure?</Heading2>
|
||||
<Heading2 className="!mt-0">
|
||||
{t("settings.account.actions.delete.confirmTitle")}
|
||||
</Heading2>
|
||||
<Paragraph>
|
||||
Are you sure you want to delete your account? All your data will be
|
||||
lost!
|
||||
{t("settings.account.actions.delete.confirmDescription")}
|
||||
</Paragraph>
|
||||
<Button
|
||||
theme="danger"
|
||||
loading={deleteResult.loading}
|
||||
onClick={deleteExec}
|
||||
>
|
||||
Delete account
|
||||
{t("settings.account.actions.delete.confirmButton")}
|
||||
</Button>
|
||||
</ModalCard>
|
||||
</Modal>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Avatar } from "@/components/Avatar";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
@ -18,6 +20,7 @@ export function AccountEditPart(props: {
|
|||
userIcon: UserIcons;
|
||||
setUserIcon: (s: UserIcons) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuth();
|
||||
const profileEditModal = useModal("profile-edit");
|
||||
|
||||
|
@ -50,7 +53,7 @@ export function AccountEditPart(props: {
|
|||
onClick={profileEditModal.show}
|
||||
>
|
||||
<Icon icon={Icons.EDIT} />
|
||||
Edit
|
||||
{t("settings.account.accountDetails.editProfile")}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
@ -58,14 +61,20 @@ export function AccountEditPart(props: {
|
|||
<div>
|
||||
<div className="space-y-8 max-w-xs">
|
||||
<AuthInputBox
|
||||
label="Device name"
|
||||
placeholder="Fremen tablet"
|
||||
label={
|
||||
t("settings.account.accountDetails.deviceNameLabel") ??
|
||||
undefined
|
||||
}
|
||||
placeholder={
|
||||
t("settings.account.accountDetails.deviceNamePlaceholder") ??
|
||||
undefined
|
||||
}
|
||||
value={props.deviceName}
|
||||
onChange={(value) => props.setDeviceName(value)}
|
||||
/>
|
||||
<div className="flex space-x-3">
|
||||
<Button theme="danger" onClick={logout}>
|
||||
Log out
|
||||
{t("settings.account.accountDetails.logoutButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
|
@ -19,6 +20,7 @@ export function CaptionPreview(props: {
|
|||
styling: SubtitleStyling;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -50,7 +52,7 @@ export function CaptionPreview(props: {
|
|||
}
|
||||
>
|
||||
<CaptionCue
|
||||
text="I must not fear. Fear is the mind-killer."
|
||||
text={t("settings.captions.previewQuote") ?? undefined}
|
||||
styling={props.styling}
|
||||
overrideCasing={false}
|
||||
/>
|
||||
|
@ -66,15 +68,16 @@ export function CaptionsPart(props: {
|
|||
styling: SubtitleStyling;
|
||||
setStyling: (s: SubtitleStyling) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [fullscreenPreview, setFullscreenPreview] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Heading1 border>Captions</Heading1>
|
||||
<Heading1 border>{t("settings.captions.title")}</Heading1>
|
||||
<div className="grid md:grid-cols-[1fr,356px] gap-8">
|
||||
<div className="space-y-6">
|
||||
<CaptionSetting
|
||||
label="Background opacity"
|
||||
label={t("settings.captions.backgroundLabel")}
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={(v) =>
|
||||
|
@ -84,7 +87,7 @@ export function CaptionsPart(props: {
|
|||
textTransformer={(s) => `${s}%`}
|
||||
/>
|
||||
<CaptionSetting
|
||||
label="Text size"
|
||||
label={t("settings.captions.textSizeLabel")}
|
||||
max={200}
|
||||
min={1}
|
||||
textTransformer={(s) => `${s}%`}
|
||||
|
@ -94,7 +97,9 @@ export function CaptionsPart(props: {
|
|||
value={props.styling.size * 100}
|
||||
/>
|
||||
<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">
|
||||
{colors.map((v) => (
|
||||
<ColorOption
|
||||
|
|
Loading…
Reference in a new issue