mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
Remove unused files/functions + localize everything except player and pages + reorganize files + fix lint warnings
This commit is contained in:
parent
50b625c604
commit
0ef492f58b
77 changed files with 234 additions and 1966 deletions
|
@ -22,6 +22,7 @@
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"i18next": "^22.4.5",
|
"i18next": "^22.4.5",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
|
"iso-639-1": "^3.1.0",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.0.0",
|
||||||
|
|
|
@ -65,6 +65,9 @@ dependencies:
|
||||||
immer:
|
immer:
|
||||||
specifier: ^10.0.2
|
specifier: ^10.0.2
|
||||||
version: 10.0.2
|
version: 10.0.2
|
||||||
|
iso-639-1:
|
||||||
|
specifier: ^3.1.0
|
||||||
|
version: 3.1.0
|
||||||
lodash.isequal:
|
lodash.isequal:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0
|
version: 4.5.0
|
||||||
|
|
5
src/assets/languages.ts
Normal file
5
src/assets/languages.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import en from "@/assets/locales/en.json";
|
||||||
|
|
||||||
|
export const locales = {
|
||||||
|
en,
|
||||||
|
};
|
59
src/assets/locales/en.json
Normal file
59
src/assets/locales/en.json
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"global": {
|
||||||
|
"name": "movie-web"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"types": {
|
||||||
|
"movie": "Movie",
|
||||||
|
"show": "Show"
|
||||||
|
},
|
||||||
|
"episodeDisplay": "S{{season}} E{{episode}}"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"mediaList": {
|
||||||
|
"stopEditing": "Stop editing"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overlays": {
|
||||||
|
"close": "Close"
|
||||||
|
},
|
||||||
|
"screens": {
|
||||||
|
"loadingUser": "Loading your profile",
|
||||||
|
"loadingApp": "Loading application",
|
||||||
|
"loadingUserError": {
|
||||||
|
"text": "",
|
||||||
|
"textWithReset": "",
|
||||||
|
"reset": "Reset custom server"
|
||||||
|
},
|
||||||
|
"migration": {
|
||||||
|
"failed": "Failed to migrate your data."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"banner": {
|
||||||
|
"offline": "Check your internet connection"
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"register": "Sync to cloud",
|
||||||
|
"settings": "Settings",
|
||||||
|
"about": "About us",
|
||||||
|
"support": "Support",
|
||||||
|
"logout": "Log out"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"copy": "Copy"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"tagline": "Watch your favorite shows and movies with this open source streaming app.",
|
||||||
|
"links": {
|
||||||
|
"github": "GitHub",
|
||||||
|
"dmca": "DMCA",
|
||||||
|
"discord": "Discord"
|
||||||
|
},
|
||||||
|
"legal": {
|
||||||
|
"disclaimer": "Disclaimer",
|
||||||
|
"disclaimerText": "movie-web does not host any files, it merely links to 3rd party services. Legal issues should be taken up with the file hosts and providers. movie-web is not responsible for any media files shown by the video providers."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import { useCallback } from "react";
|
||||||
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
||||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
|
// for anybody who cares - these are anonymous metrics.
|
||||||
|
// They are just used for figuring out if providers are broken or not
|
||||||
const metricsEndpoint = "https://backend.movie-web.app/metrics/providers";
|
const metricsEndpoint = "https://backend.movie-web.app/metrics/providers";
|
||||||
|
|
||||||
export type ProviderMetric = {
|
export type ProviderMetric = {
|
||||||
|
|
|
@ -5,9 +5,8 @@ export interface FlagIconProps {
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlagIcon(props: FlagIconProps) {
|
// Country code overrides
|
||||||
// Country code overrides
|
const countryOverrides: Record<string, string> = {
|
||||||
const countryOverrides: Record<string, string> = {
|
|
||||||
en: "gb",
|
en: "gb",
|
||||||
cs: "cz",
|
cs: "cz",
|
||||||
el: "gr",
|
el: "gr",
|
||||||
|
@ -21,8 +20,9 @@ export function FlagIcon(props: FlagIconProps) {
|
||||||
vi: "vn",
|
vi: "vn",
|
||||||
zh: "cn",
|
zh: "cn",
|
||||||
sl: "si",
|
sl: "si",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function FlagIcon(props: FlagIconProps) {
|
||||||
let countryCode =
|
let countryCode =
|
||||||
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
||||||
if (countryOverrides[countryCode])
|
if (countryOverrides[countryCode])
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||||
import { UserAvatar } from "@/components/Avatar";
|
import { UserAvatar } from "@/components/Avatar";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { useAuth } from "@/hooks/auth/useAuth";
|
import { useAuth } from "@/hooks/auth/useAuth";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
@ -81,6 +82,7 @@ function CircleDropdownLink(props: { icon: Icons; href: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinksDropdown(props: { children: React.ReactNode }) {
|
export function LinksDropdown(props: { children: React.ReactNode }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const deviceName = useAuthStore((s) => s.account?.deviceName);
|
const deviceName = useAuthStore((s) => s.account?.deviceName);
|
||||||
const seed = useAuthStore((s) => s.account?.seed);
|
const seed = useAuthStore((s) => s.account?.seed);
|
||||||
|
@ -130,18 +132,18 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
) : (
|
) : (
|
||||||
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight>
|
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight>
|
||||||
Sync to cloud
|
{t("navigation.menu.register")}
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
<DropdownLink href="/settings" icon={Icons.SETTINGS}>
|
<DropdownLink href="/settings" icon={Icons.SETTINGS}>
|
||||||
Settings
|
{t("navigation.menu.settings")}
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
<DropdownLink href="/faq" icon={Icons.EPISODES}>
|
<DropdownLink href="/faq" icon={Icons.EPISODES}>
|
||||||
About us
|
{t("navigation.menu.about")}
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
<DropdownLink href="/faq" icon={Icons.FILM}>
|
<DropdownLink href="/faq" icon={Icons.FILM}>
|
||||||
HELP MEEE
|
{t("navigation.menu.support")}
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
{deviceName ? (
|
{deviceName ? (
|
||||||
<DropdownLink
|
<DropdownLink
|
||||||
|
@ -149,7 +151,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
||||||
icon={Icons.LOGOUT}
|
icon={Icons.LOGOUT}
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
>
|
>
|
||||||
Log out
|
{t("navigation.menu.logout")}
|
||||||
</DropdownLink>
|
</DropdownLink>
|
||||||
) : null}
|
) : null}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Helmet } from "react-helmet-async";
|
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
|
||||||
|
|
||||||
export function Overlay(props: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<body data-no-scroll />
|
|
||||||
</Helmet>
|
|
||||||
<div className="fixed inset-0 z-[99999]">
|
|
||||||
<Transition
|
|
||||||
animation="fade"
|
|
||||||
className="absolute inset-0 bg-[rgba(8,6,18,0.85)]"
|
|
||||||
isChild
|
|
||||||
/>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { ChangeEventHandler, useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
export type SliderProps = {
|
|
||||||
label?: string;
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
step: number;
|
|
||||||
value?: number;
|
|
||||||
valueDisplay?: string;
|
|
||||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Slider(props: SliderProps) {
|
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const e = ref.current as HTMLInputElement;
|
|
||||||
e.style.setProperty("--value", e.value);
|
|
||||||
e.style.setProperty("--min", e.min === "" ? "0" : e.min);
|
|
||||||
e.style.setProperty("--max", e.max === "" ? "100" : e.max);
|
|
||||||
e.addEventListener("input", () => e.style.setProperty("--value", e.value));
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-6 flex flex-row gap-4">
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
{props.label ? (
|
|
||||||
<label className="font-bold">{props.label}</label>
|
|
||||||
) : null}
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
ref={ref}
|
|
||||||
className="styled-slider slider-progress mt-[20px]"
|
|
||||||
onChange={props.onChange}
|
|
||||||
value={props.value}
|
|
||||||
max={props.max}
|
|
||||||
min={props.min}
|
|
||||||
step={props.step}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
|
||||||
<div className="text-center font-bold text-white">
|
|
||||||
{props.valueDisplay ?? props.value}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
export interface ButtonControlProps {
|
|
||||||
onClick?: () => void;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ButtonControl({
|
|
||||||
onClick,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: ButtonControlProps) {
|
|
||||||
return (
|
|
||||||
<button onClick={onClick} className={className} type="button">
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,8 +4,6 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
import { ButtonControl } from "./ButtonControl";
|
|
||||||
|
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
onEdit?: (editing: boolean) => void;
|
onEdit?: (editing: boolean) => void;
|
||||||
|
@ -20,7 +18,8 @@ export function EditButton(props: EditButtonProps) {
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonControl
|
<button
|
||||||
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105"
|
className="flex h-12 items-center overflow-hidden rounded-full bg-background-secondary px-4 py-2 text-white transition-[background-color,transform] hover:bg-background-secondaryHover active:scale-105"
|
||||||
>
|
>
|
||||||
|
@ -33,6 +32,6 @@ export function EditButton(props: EditButtonProps) {
|
||||||
<Icon icon={Icons.EDIT} />
|
<Icon icon={Icons.EDIT} />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</ButtonControl>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,14 @@ import classNames from "classnames";
|
||||||
|
|
||||||
import { Icon, Icons } from "../Icon";
|
import { Icon, Icons } from "../Icon";
|
||||||
|
|
||||||
|
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"];
|
||||||
|
export const initialColor = colors[0];
|
||||||
|
|
||||||
export function ColorPicker(props: {
|
export function ColorPicker(props: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
onInput: (v: string) => void;
|
onInput: (v: string) => void;
|
||||||
}) {
|
}) {
|
||||||
// Migrate this to another file later
|
|
||||||
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"];
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{props.label ? (
|
{props.label ? (
|
||||||
|
|
|
@ -2,20 +2,20 @@ import classNames from "classnames";
|
||||||
|
|
||||||
import { UserIcon, UserIcons } from "../UserIcon";
|
import { UserIcon, UserIcons } from "../UserIcon";
|
||||||
|
|
||||||
|
const icons = [
|
||||||
|
UserIcons.USER,
|
||||||
|
UserIcons.BOOKMARK,
|
||||||
|
UserIcons.CLOCK,
|
||||||
|
UserIcons.EYE_SLASH,
|
||||||
|
UserIcons.SEARCH,
|
||||||
|
];
|
||||||
|
export const initialIcon = icons[0];
|
||||||
|
|
||||||
export function IconPicker(props: {
|
export function IconPicker(props: {
|
||||||
label: string;
|
label: string;
|
||||||
value: UserIcons;
|
value: UserIcons;
|
||||||
onInput: (v: UserIcons) => void;
|
onInput: (v: UserIcons) => void;
|
||||||
}) {
|
}) {
|
||||||
// Migrate this to another file later
|
|
||||||
const icons = [
|
|
||||||
UserIcons.USER,
|
|
||||||
UserIcons.BOOKMARK,
|
|
||||||
UserIcons.CLOCK,
|
|
||||||
UserIcons.EYE_SLASH,
|
|
||||||
UserIcons.SEARCH,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{props.label ? (
|
{props.label ? (
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCopyToClipboard, useMountedState } from "react-use";
|
import { useCopyToClipboard, useMountedState } from "react-use";
|
||||||
|
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "../Icon";
|
||||||
|
|
||||||
export function PassphraseDisplay(props: { mnemonic: string }) {
|
export function PassphraseDisplay(props: { mnemonic: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const individualWords = props.mnemonic.split(" ");
|
const individualWords = props.mnemonic.split(" ");
|
||||||
|
|
||||||
const [, copy] = useCopyToClipboard();
|
const [, copy] = useCopyToClipboard();
|
||||||
|
@ -33,7 +35,7 @@ export function PassphraseDisplay(props: { mnemonic: string }) {
|
||||||
icon={hasCopied ? Icons.CHECKMARK : Icons.COPY}
|
icon={hasCopied ? Icons.CHECKMARK : Icons.COPY}
|
||||||
className={hasCopied ? "text-xs" : ""}
|
className={hasCopied ? "text-xs" : ""}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Copy</span>
|
<span className="text-sm">{t("actions.copy")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 py-4 grid grid-cols-4 gap-2">
|
<div className="px-4 py-4 grid grid-cols-4 gap-2">
|
|
@ -3,8 +3,8 @@ import { useState } from "react";
|
||||||
|
|
||||||
import { Flare } from "@/components/utils/Flare";
|
import { Flare } from "@/components/utils/Flare";
|
||||||
|
|
||||||
import { Icon, Icons } from "./Icon";
|
import { Icon, Icons } from "../Icon";
|
||||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
import { TextInputControl } from "../text-inputs/TextInputControl";
|
||||||
|
|
||||||
export interface SearchBarProps {
|
export interface SearchBarProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
|
@ -1,114 +0,0 @@
|
||||||
import React, { createRef, useEffect, useState } from "react";
|
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
|
|
||||||
import { useFade } from "@/hooks/useFade";
|
|
||||||
|
|
||||||
interface BackdropProps {
|
|
||||||
onClick?: (e: MouseEvent) => void;
|
|
||||||
onBackdropHide?: () => void;
|
|
||||||
active?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useBackdrop(): [
|
|
||||||
(state: boolean) => void,
|
|
||||||
BackdropProps,
|
|
||||||
{ style: any }
|
|
||||||
] {
|
|
||||||
const [backdrop, setBackdropState] = useState(false);
|
|
||||||
const [isHighlighted, setisHighlighted] = useState(false);
|
|
||||||
|
|
||||||
const setBackdrop = (state: boolean) => {
|
|
||||||
setBackdropState(state);
|
|
||||||
if (state) setisHighlighted(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const backdropProps: BackdropProps = {
|
|
||||||
active: backdrop,
|
|
||||||
onBackdropHide() {
|
|
||||||
setisHighlighted(false);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightedProps = {
|
|
||||||
style: isHighlighted
|
|
||||||
? {
|
|
||||||
zIndex: "1000",
|
|
||||||
position: "relative",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
return [setBackdrop, backdropProps, highlightedProps];
|
|
||||||
}
|
|
||||||
|
|
||||||
function Backdrop(props: BackdropProps) {
|
|
||||||
const clickEvent = props.onClick || (() => {});
|
|
||||||
const animationEvent = props.onBackdropHide || (() => {});
|
|
||||||
const [isVisible, setVisible, fadeProps] = useFade();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setVisible(!!props.active);
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [props.active, setVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isVisible) animationEvent();
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
}, [isVisible]);
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`pointer-events-auto fixed left-0 right-0 top-0 h-screen w-screen bg-black bg-opacity-50 opacity-100 transition-opacity ${
|
|
||||||
!isVisible ? "opacity-0" : ""
|
|
||||||
}`}
|
|
||||||
{...fadeProps}
|
|
||||||
onClick={(e) => clickEvent(e.nativeEvent)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BackdropContainer(
|
|
||||||
props: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
} & BackdropProps
|
|
||||||
) {
|
|
||||||
const root = createRef<HTMLDivElement>();
|
|
||||||
const copy = createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let frame = -1;
|
|
||||||
function poll() {
|
|
||||||
if (root.current && copy.current) {
|
|
||||||
const rect = root.current.getBoundingClientRect();
|
|
||||||
copy.current.style.top = `${rect.top}px`;
|
|
||||||
copy.current.style.left = `${rect.left}px`;
|
|
||||||
copy.current.style.width = `${rect.width}px`;
|
|
||||||
copy.current.style.height = `${rect.height}px`;
|
|
||||||
}
|
|
||||||
frame = window.requestAnimationFrame(poll);
|
|
||||||
}
|
|
||||||
poll();
|
|
||||||
return () => {
|
|
||||||
window.cancelAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
// we dont want this to run only on mount, dont care about ref updates
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [root, copy]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={root}>
|
|
||||||
{createPortal(
|
|
||||||
<div className="pointer-events-none fixed left-0 top-0 z-[999]">
|
|
||||||
<Backdrop active={props.active} {...props} />
|
|
||||||
<div ref={copy} className="pointer-events-auto absolute">
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
<div className="invisible">{props.children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { BrandPill } from "@/components/layout/BrandPill";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
|
@ -6,16 +7,18 @@ import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
function FooterLink(props: {
|
function FooterLink(props: {
|
||||||
href: string;
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon: Icons;
|
icon: Icons;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={props.href}
|
href={props.href ?? "#"}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="tabbable rounded py-2 px-3 inline-flex items-center space-x-3 transition-colors duration-200 hover:text-type-emphasis"
|
className="tabbable rounded py-2 px-3 inline-flex items-center space-x-3 transition-colors duration-200 hover:text-type-emphasis"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<Icon icon={props.icon} className="text-2xl" />
|
<Icon icon={props.icon} className="text-2xl" />
|
||||||
<span className="font-medium">{props.children}</span>
|
<span className="font-medium">{props.children}</span>
|
||||||
|
@ -25,8 +28,10 @@ function FooterLink(props: {
|
||||||
|
|
||||||
function Dmca() {
|
function Dmca() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FooterLink icon={Icons.DRAGON} href="https://youtu.be/-WOonkg_ZCo">
|
<FooterLink icon={Icons.DRAGON} onClick={() => history.push("/dmca")}>
|
||||||
{t("footer.links.dmca")}
|
{t("footer.links.dmca")}
|
||||||
</FooterLink>
|
</FooterLink>
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,7 +37,7 @@ function MediaCardContent({
|
||||||
|
|
||||||
const canLink = linkable && !closable;
|
const canLink = linkable && !closable;
|
||||||
|
|
||||||
const dotListContent = [t(`media.${media.type}`)];
|
const dotListContent = [t(`media.types.${media.type}`)];
|
||||||
if (media.year) dotListContent.push(media.year.toFixed());
|
if (media.year) dotListContent.push(media.year.toFixed());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -82,7 +82,7 @@ function MediaCardContent({
|
||||||
closable ? "" : "group-hover:text-white",
|
closable ? "" : "group-hover:text-white",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{t("seasons.seasonAndEpisode", {
|
{t("media.episodeDisplay", {
|
||||||
season: series.season || 1,
|
season: series.season || 1,
|
||||||
episode: series.episode,
|
episode: series.episode,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import FocusTrap from "focus-trap-react";
|
||||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import {
|
import {
|
||||||
useInternalOverlayRouter,
|
useInternalOverlayRouter,
|
||||||
useRouterAnchorUpdate,
|
useRouterAnchorUpdate,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode, useEffect, useMemo } from "react";
|
import { ReactNode, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { Transition, TransitionAnimations } from "@/components/Transition";
|
import {
|
||||||
|
Transition,
|
||||||
|
TransitionAnimations,
|
||||||
|
} from "@/components/utils/Transition";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { useOverlayStore } from "@/stores/overlay/store";
|
import { useOverlayStore } from "@/stores/overlay/store";
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ interface MobilePositionProps {
|
||||||
|
|
||||||
export function OverlayMobilePosition(props: MobilePositionProps) {
|
export function OverlayMobilePosition(props: MobilePositionProps) {
|
||||||
const router = useInternalOverlayRouter("hello world :)");
|
const router = useInternalOverlayRouter("hello world :)");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -26,7 +28,7 @@ export function OverlayMobilePosition(props: MobilePositionProps) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.close()}
|
onClick={() => router.close()}
|
||||||
>
|
>
|
||||||
Close
|
{t("overlays.close")}
|
||||||
</button>
|
</button>
|
||||||
{/* Gradient to hide the progress */}
|
{/* Gradient to hide the progress */}
|
||||||
<div className="pointer-events-none absolute z-0 bottom-0 left-0 w-full h-32 bg-gradient-to-t from-black to-transparent" />
|
<div className="pointer-events-none absolute z-0 bottom-0 left-0 w-full h-32 bg-gradient-to-t from-black to-transparent" />
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback } from "react";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import { Flare } from "@/components/utils/Flare";
|
import { Flare } from "@/components/utils/Flare";
|
||||||
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { useEmpheralVolumeStore } from "@/stores/volume";
|
import { useEmpheralVolumeStore } from "@/stores/volume";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
|
|
||||||
export function BlackOverlay(props: { show?: boolean }) {
|
export function BlackOverlay(props: { show?: boolean }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function BottomControls(props: {
|
export function BottomControls(props: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
|
|
||||||
export function CenterMobileControls(props: {
|
export function CenterMobileControls(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
parseSubtitles,
|
parseSubtitles,
|
||||||
sanitize,
|
sanitize,
|
||||||
} from "@/components/player/utils/captions";
|
} from "@/components/player/utils/captions";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
|
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { useBannerSize } from "@/stores/banner";
|
import { useBannerSize } from "@/stores/banner";
|
||||||
import { BannerLocation } from "@/stores/banner/BannerLocation";
|
import { BannerLocation } from "@/stores/banner/BannerLocation";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import classNames from "classnames";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
import { StatusCircle } from "@/components/player/internals/StatusCircle";
|
import { StatusCircle } from "@/components/player/internals/StatusCircle";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
|
|
||||||
export interface ScrapeItemProps {
|
export interface ScrapeItemProps {
|
||||||
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { a, to, useSpring } from "@react-spring/web";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
|
|
||||||
export interface StatusCircle {
|
export interface StatusCircle {
|
||||||
type: "loading" | "success" | "error" | "noresult" | "waiting";
|
type: "loading" | "success" | "error" | "noresult" | "waiting";
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { ReactNode } from "react";
|
|
||||||
import { Link as LinkRouter } from "react-router-dom";
|
|
||||||
|
|
||||||
interface ILinkPropsBase {
|
|
||||||
children?: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILinkPropsExternal extends ILinkPropsBase {
|
|
||||||
url: string;
|
|
||||||
newTab?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILinkPropsInternal extends ILinkPropsBase {
|
|
||||||
to: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LinkProps = ILinkPropsExternal | ILinkPropsInternal | ILinkPropsBase;
|
|
||||||
|
|
||||||
export function Link(props: LinkProps) {
|
|
||||||
const isExternal = !!(props as ILinkPropsExternal).url;
|
|
||||||
const isInternal = !!(props as ILinkPropsInternal).to;
|
|
||||||
const content = (
|
|
||||||
<span className="cursor-pointer font-bold text-type-link hover:text-type-linkHover">
|
|
||||||
{props.children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isExternal)
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
target={(props as ILinkPropsExternal).newTab ? "_blank" : undefined}
|
|
||||||
rel="noreferrer"
|
|
||||||
href={(props as ILinkPropsExternal).url}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
if (isInternal)
|
|
||||||
return (
|
|
||||||
<LinkRouter to={(props as ILinkPropsInternal).to}>{content}</LinkRouter>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<span onClick={() => props.onClick && props.onClick()}>{content}</span>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
/// <reference types="chromecast-caf-sender"/>
|
/// <reference types="chromecast-caf-sender"/>
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { isChromecastAvailable } from "@/setup/chromecast";
|
import { isChromecastAvailable } from "@/setup/chromecast";
|
||||||
|
|
||||||
|
@ -13,93 +13,3 @@ export function useChromecastAvailable() {
|
||||||
|
|
||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChromecast() {
|
|
||||||
const available = useChromecastAvailable();
|
|
||||||
const instance = useRef<cast.framework.CastContext | null>(null);
|
|
||||||
const remotePlayerController =
|
|
||||||
useRef<cast.framework.RemotePlayerController | null>(null);
|
|
||||||
|
|
||||||
function startCast() {
|
|
||||||
const movieMeta = new chrome.cast.media.MovieMediaMetadata();
|
|
||||||
movieMeta.title = "Big Buck Bunny";
|
|
||||||
|
|
||||||
const mediaInfo = new chrome.cast.media.MediaInfo("hello", "video/mp4");
|
|
||||||
(mediaInfo as any).contentUrl =
|
|
||||||
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
|
|
||||||
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
|
|
||||||
mediaInfo.metadata = movieMeta;
|
|
||||||
|
|
||||||
const request = new chrome.cast.media.LoadRequest(mediaInfo);
|
|
||||||
request.autoplay = true;
|
|
||||||
|
|
||||||
const session = instance.current?.getCurrentSession();
|
|
||||||
if (!session) return;
|
|
||||||
|
|
||||||
session.loadMedia(request).catch((e: any) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopCast() {
|
|
||||||
const session = instance.current?.getCurrentSession();
|
|
||||||
if (!session) return;
|
|
||||||
|
|
||||||
const controller = remotePlayerController.current;
|
|
||||||
if (!controller) return;
|
|
||||||
controller.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!available) return;
|
|
||||||
|
|
||||||
// setup instance if not already
|
|
||||||
if (!instance.current) {
|
|
||||||
const ins = cast.framework.CastContext.getInstance();
|
|
||||||
ins.setOptions({
|
|
||||||
receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
|
||||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
|
||||||
});
|
|
||||||
instance.current = ins;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup player if not already
|
|
||||||
if (!remotePlayerController.current) {
|
|
||||||
const player = new cast.framework.RemotePlayer();
|
|
||||||
const controller = new cast.framework.RemotePlayerController(player);
|
|
||||||
remotePlayerController.current = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup event listener
|
|
||||||
function listenToEvents(e: cast.framework.RemotePlayerChangedEvent) {
|
|
||||||
console.debug("chromecast event", e);
|
|
||||||
}
|
|
||||||
function connectionChanged(e: cast.framework.RemotePlayerChangedEvent) {
|
|
||||||
console.info("chromecast event connection changed", e);
|
|
||||||
}
|
|
||||||
remotePlayerController.current.addEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED,
|
|
||||||
listenToEvents
|
|
||||||
);
|
|
||||||
remotePlayerController.current.addEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
|
||||||
connectionChanged
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
remotePlayerController.current?.removeEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.PLAYER_STATE_CHANGED,
|
|
||||||
listenToEvents
|
|
||||||
);
|
|
||||||
remotePlayerController.current?.removeEventListener(
|
|
||||||
cast.framework.RemotePlayerEventType.IS_CONNECTED_CHANGED,
|
|
||||||
connectionChanged
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}, [available]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
startCast,
|
|
||||||
stopCast,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import "./useFade.css";
|
|
||||||
|
|
||||||
export const useFade = (
|
|
||||||
initial = false
|
|
||||||
): [boolean, React.Dispatch<React.SetStateAction<boolean>>, any] => {
|
|
||||||
const [show, setShow] = useState<boolean>(initial);
|
|
||||||
const [isVisible, setVisible] = useState<boolean>(show);
|
|
||||||
|
|
||||||
// Update visibility when show changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) setVisible(true);
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
// When the animation finishes, set visibility to false
|
|
||||||
const onAnimationEnd = () => {
|
|
||||||
if (!show) setVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const style = { animation: `${show ? "fadeIn" : "fadeOut"} .3s` };
|
|
||||||
|
|
||||||
// These props go on the fading DOM element
|
|
||||||
const fadeProps = {
|
|
||||||
style,
|
|
||||||
onAnimationEnd,
|
|
||||||
};
|
|
||||||
|
|
||||||
return [isVisible, setShow, fadeProps];
|
|
||||||
};
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { useLayoutEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function useFloatingRouter(initial = "/") {
|
|
||||||
const [route, setRoute] = useState<string[]>(
|
|
||||||
initial.split("/").filter((v) => v.length > 0)
|
|
||||||
);
|
|
||||||
const [previousRoute, setPreviousRoute] = useState(route);
|
|
||||||
const currentPage = route[route.length - 1] ?? "/";
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (previousRoute.length === route.length) return;
|
|
||||||
// when navigating backwards, we delay the updating by a bit so transitions can be applied correctly
|
|
||||||
setTimeout(() => {
|
|
||||||
setPreviousRoute(route);
|
|
||||||
}, 20);
|
|
||||||
}, [route, previousRoute]);
|
|
||||||
|
|
||||||
function navigate(path: string) {
|
|
||||||
const newRoute = path.split("/").filter((v) => v.length > 0);
|
|
||||||
if (newRoute.length > previousRoute.length) setPreviousRoute(newRoute);
|
|
||||||
setRoute(newRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isActive(page: string) {
|
|
||||||
if (page === "/") return true;
|
|
||||||
const index = previousRoute.indexOf(page);
|
|
||||||
if (index === -1) return false; // not active
|
|
||||||
if (index === previousRoute.length - 1) return false; // active but latest route so shouldnt be counted as active
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCurrentPage(page: string) {
|
|
||||||
return page === currentPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLoaded(page: string) {
|
|
||||||
if (page === "/") return true;
|
|
||||||
return route.includes(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pageProps(page: string) {
|
|
||||||
return {
|
|
||||||
show: isCurrentPage(page),
|
|
||||||
active: isActive(page),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
navigate("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
navigate,
|
|
||||||
reset,
|
|
||||||
isLoaded,
|
|
||||||
isCurrentPage,
|
|
||||||
pageProps,
|
|
||||||
isActive,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,17 +1,18 @@
|
||||||
import "core-js/stable";
|
import "core-js/stable";
|
||||||
import "./stores/__old/imports";
|
import "./stores/__old/imports";
|
||||||
import "@/setup/ga";
|
import "@/setup/ga";
|
||||||
import "@/setup/index.css";
|
import "@/assets/css/index.css";
|
||||||
|
|
||||||
import React, { Suspense, useCallback } from "react";
|
import React, { Suspense, useCallback } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import { registerSW } from "virtual:pwa-register";
|
import { registerSW } from "virtual:pwa-register";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { useAuthRestore } from "@/hooks/auth/useAuthRestore";
|
import { useAuthRestore } from "@/hooks/auth/useAuthRestore";
|
||||||
|
@ -44,8 +45,15 @@ registerSW({
|
||||||
});
|
});
|
||||||
|
|
||||||
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
||||||
|
const mapping = {
|
||||||
|
user: "screens.loadingUser",
|
||||||
|
lazy: "screens.loadingApp",
|
||||||
|
};
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<LargeTextPart iconSlot={<Loading />}>Loading {props.type}</LargeTextPart>
|
<LargeTextPart iconSlot={<Loading />}>
|
||||||
|
{t(mapping[props.type] ?? "unknown.translation")}
|
||||||
|
</LargeTextPart>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +61,7 @@ function ErrorScreen(props: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
showResetButton?: boolean;
|
showResetButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
|
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
|
||||||
const resetBackend = useCallback(() => {
|
const resetBackend = useCallback(() => {
|
||||||
setBackendUrl(null);
|
setBackendUrl(null);
|
||||||
|
@ -70,7 +79,7 @@ function ErrorScreen(props: {
|
||||||
{props.showResetButton ? (
|
{props.showResetButton ? (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Button theme="secondary" onClick={resetBackend}>
|
<Button theme="secondary" onClick={resetBackend}>
|
||||||
Reset back-end
|
{t("screens.loadingUserError.reset")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -82,14 +91,17 @@ function AuthWrapper() {
|
||||||
const status = useAuthRestore();
|
const status = useAuthRestore();
|
||||||
const backendUrl = conf().BACKEND_URL;
|
const backendUrl = conf().BACKEND_URL;
|
||||||
const userBackendUrl = useBackendUrl();
|
const userBackendUrl = useBackendUrl();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (status.loading) return <LoadingScreen type="user" />;
|
if (status.loading) return <LoadingScreen type="user" />;
|
||||||
if (status.error)
|
if (status.error)
|
||||||
return (
|
return (
|
||||||
<ErrorScreen showResetButton={backendUrl !== userBackendUrl}>
|
<ErrorScreen showResetButton={backendUrl !== userBackendUrl}>
|
||||||
{backendUrl !== userBackendUrl
|
{t(
|
||||||
? "Failed to fetch user data. Try resetting the backend URL"
|
backendUrl !== userBackendUrl
|
||||||
: "Failed to fetch user data."}
|
? "screens.loadingUserError.textWithReset"
|
||||||
|
: "screens.loadingUserError.text"
|
||||||
|
)}
|
||||||
</ErrorScreen>
|
</ErrorScreen>
|
||||||
);
|
);
|
||||||
return <App />;
|
return <App />;
|
||||||
|
@ -100,10 +112,11 @@ function MigrationRunner() {
|
||||||
i18n.changeLanguage(useLanguageStore.getState().language);
|
i18n.changeLanguage(useLanguageStore.getState().language);
|
||||||
await initializeOldStores();
|
await initializeOldStores();
|
||||||
}, []);
|
}, []);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (status.loading) return <MigrationPart />;
|
if (status.loading) return <MigrationPart />;
|
||||||
if (status.error)
|
if (status.error)
|
||||||
return <ErrorScreen>Failed to migrate your data.</ErrorScreen>;
|
return <ErrorScreen>{t("screens.migration.failed")}</ErrorScreen>;
|
||||||
return <AuthWrapper />;
|
return <AuthWrapper />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
import { getSessions, updateSession } from "@/backend/accounts/sessions";
|
import { getSessions, updateSession } from "@/backend/accounts/sessions";
|
||||||
import { updateSettings } from "@/backend/accounts/settings";
|
import { updateSettings } from "@/backend/accounts/settings";
|
||||||
import { editUser } from "@/backend/accounts/user";
|
import { editUser } from "@/backend/accounts/user";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { WideContainer } from "@/components/layout/WideContainer";
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { UserIcons } from "@/components/UserIcon";
|
import { UserIcons } from "@/components/UserIcon";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
|
|
||||||
// mostly empty view, add whatever you need
|
// mostly empty view, add whatever you need
|
||||||
export default function TestView() {
|
export default function TestView() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Dropdown } from "@/components/Dropdown";
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
||||||
|
|
|
@ -2,10 +2,9 @@ import { useState } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { MetaResponse, getBackendMeta } from "@/backend/accounts/meta";
|
import { MetaResponse, getBackendMeta } from "@/backend/accounts/meta";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Box } from "@/components/layout/Box";
|
import { Box } from "@/components/layout/Box";
|
||||||
import { Spinner } from "@/components/layout/Spinner";
|
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { Divider } from "@/components/utils/Divider";
|
||||||
import { Heading2 } from "@/components/utils/Text";
|
import { Heading2 } from "@/components/utils/Text";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { getMediaDetails } from "@/backend/metadata/tmdb";
|
import { getMediaDetails } from "@/backend/metadata/tmdb";
|
||||||
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Box } from "@/components/layout/Box";
|
import { Box } from "@/components/layout/Box";
|
||||||
import { Spinner } from "@/components/layout/Spinner";
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { useMemo, useState } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { mwFetch } from "@/backend/helpers/fetch";
|
import { mwFetch } from "@/backend/helpers/fetch";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Box } from "@/components/layout/Box";
|
import { Box } from "@/components/layout/Box";
|
||||||
import { Spinner } from "@/components/layout/Spinner";
|
|
||||||
import { Divider } from "@/components/utils/Divider";
|
import { Divider } from "@/components/utils/Divider";
|
||||||
import { Heading2 } from "@/components/utils/Text";
|
import { Heading2 } from "@/components/utils/Text";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useState } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { BrandPill } from "@/components/layout/BrandPill";
|
import { BrandPill } from "@/components/layout/BrandPill";
|
||||||
import {
|
import {
|
||||||
LargeCard,
|
LargeCard,
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { genMnemonic } from "@/backend/accounts/crypto";
|
import { genMnemonic } from "@/backend/accounts/crypto";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
|
import { PassphraseDisplay } from "@/components/form/PassphraseDisplay";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
LargeCard,
|
LargeCard,
|
||||||
LargeCardButtons,
|
LargeCardButtons,
|
||||||
LargeCardText,
|
LargeCardText,
|
||||||
} from "@/components/layout/LargeCard";
|
} from "@/components/layout/LargeCard";
|
||||||
import { PassphraseDisplay } from "@/components/PassphraseDisplay";
|
|
||||||
|
|
||||||
interface PassphraseGeneratePartProps {
|
interface PassphraseGeneratePartProps {
|
||||||
onNext?: (mnemonic: string) => void;
|
onNext?: (mnemonic: string) => void;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useHistory } from "react-router-dom";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
|
|
||||||
import { MetaResponse, getBackendMeta } from "@/backend/accounts/meta";
|
import { MetaResponse, getBackendMeta } from "@/backend/accounts/meta";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
LargeCard,
|
LargeCard,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { updateSettings } from "@/backend/accounts/settings";
|
import { updateSettings } from "@/backend/accounts/settings";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
LargeCard,
|
LargeCard,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { DisplayError } from "@/components/player/display/displayInterface";
|
import { DisplayError } from "@/components/player/display/displayInterface";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ButtonPlain } from "@/components/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";
|
||||||
import { Title } from "@/components/text/Title";
|
import { Title } from "@/components/text/Title";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Navigation } from "@/components/layout/Navigation";
|
import { Navigation } from "@/components/layout/Navigation";
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import Sticky from "react-sticky-el";
|
import Sticky from "react-sticky-el";
|
||||||
|
|
||||||
|
import { SearchBarInput } from "@/components/form/SearchBar";
|
||||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { SearchBarInput } from "@/components/SearchBar";
|
|
||||||
import { HeroTitle } from "@/components/text/HeroTitle";
|
import { HeroTitle } from "@/components/text/HeroTitle";
|
||||||
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
||||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { AsyncReturnType } from "type-fest";
|
||||||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
||||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Paragraph } from "@/components/text/Paragraph";
|
import { Paragraph } from "@/components/text/Paragraph";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Paragraph } from "@/components/text/Paragraph";
|
import { Paragraph } from "@/components/text/Paragraph";
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
scrapePartsToProviderMetric,
|
scrapePartsToProviderMetric,
|
||||||
useReportProviders,
|
useReportProviders,
|
||||||
} from "@/backend/helpers/report";
|
} from "@/backend/helpers/report";
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
|
||||||
import {
|
import {
|
||||||
ScrapeCard,
|
ScrapeCard,
|
||||||
ScrapeItem,
|
ScrapeItem,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { deleteUser } from "@/backend/accounts/user";
|
import { deleteUser } from "@/backend/accounts/user";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||||
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
|
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Avatar } from "@/components/Avatar";
|
import { Avatar } from "@/components/Avatar";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { useModal } from "@/components/overlays/Modal";
|
import { useModal } from "@/components/overlays/Modal";
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
} from "@/components/player/atoms/settings/CaptionSettingsView";
|
} from "@/components/player/atoms/settings/CaptionSettingsView";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { CaptionCue } from "@/components/player/Player";
|
import { CaptionCue } from "@/components/player/Player";
|
||||||
import { Transition } from "@/components/Transition";
|
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import { SubtitleStyling } from "@/stores/subtitles";
|
import { SubtitleStyling } from "@/stores/subtitles";
|
||||||
|
|
||||||
export function CaptionPreview(props: {
|
export function CaptionPreview(props: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Dispatch, SetStateAction, useCallback } from "react";
|
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useAsyncFn } from "react-use";
|
||||||
import { SessionResponse } from "@/backend/accounts/auth";
|
import { SessionResponse } from "@/backend/accounts/auth";
|
||||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||||
import { removeSession } from "@/backend/accounts/sessions";
|
import { removeSession } from "@/backend/accounts/sessions";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { SecondaryLabel } from "@/components/text/SecondaryLabel";
|
import { SecondaryLabel } from "@/components/text/SecondaryLabel";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Dropdown } from "@/components/Dropdown";
|
|
||||||
import { FlagIcon } from "@/components/FlagIcon";
|
import { FlagIcon } from "@/components/FlagIcon";
|
||||||
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Heading1 } from "@/components/utils/Text";
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
import { appLanguageOptions } from "@/setup/i18n";
|
import { appLanguageOptions } from "@/setup/i18n";
|
||||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
import { sortLangCodes } from "@/utils/sortLangCodes";
|
||||||
|
@ -8,14 +8,14 @@ export function LocalePart(props: {
|
||||||
language: string;
|
language: string;
|
||||||
setLanguage: (l: string) => void;
|
setLanguage: (l: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.id));
|
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.code));
|
||||||
|
|
||||||
const options = appLanguageOptions
|
const options = appLanguageOptions
|
||||||
.sort((a, b) => sorted.indexOf(a.id) - sorted.indexOf(b.id))
|
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
|
||||||
.map((opt) => ({
|
.map((opt) => ({
|
||||||
id: opt.id,
|
id: opt.code,
|
||||||
name: `${opt.englishName} — ${opt.nativeName}`,
|
name: `${opt.name} — ${opt.nativeName}`,
|
||||||
leftIcon: <FlagIcon countryCode={opt.id} />,
|
leftIcon: <FlagIcon countryCode={opt.code} />,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selected = options.find((t) => t.id === props.language);
|
const selected = options.find((t) => t.id === props.language);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button } from "@/components/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";
|
||||||
import { Modal, ModalCard } from "@/components/overlays/Modal";
|
import { Modal, ModalCard } from "@/components/overlays/Modal";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
||||||
import { Heading3 } from "@/components/utils/Text";
|
import { Heading3 } from "@/components/utils/Text";
|
||||||
|
|
||||||
|
|
|
@ -1,70 +1,27 @@
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
|
import ISO6391 from "iso-639-1";
|
||||||
import { initReactI18next } from "react-i18next";
|
import { initReactI18next } from "react-i18next";
|
||||||
|
|
||||||
// Languages
|
import { locales } from "@/assets/languages";
|
||||||
import { captionLanguages } from "./iso6391";
|
|
||||||
import cs from "./locales/cs/translation.json";
|
|
||||||
import de from "./locales/de/translation.json";
|
|
||||||
import en from "./locales/en/translation.json";
|
|
||||||
import fr from "./locales/fr/translation.json";
|
|
||||||
import it from "./locales/it/translation.json";
|
|
||||||
import nl from "./locales/nl/translation.json";
|
|
||||||
import pirate from "./locales/pirate/translation.json";
|
|
||||||
import pl from "./locales/pl/translation.json";
|
|
||||||
import tr from "./locales/tr/translation.json";
|
|
||||||
import vi from "./locales/vi/translation.json";
|
|
||||||
import zh from "./locales/zh/translation.json";
|
|
||||||
|
|
||||||
const locales = {
|
// Languages
|
||||||
en: {
|
const langCodes = Object.keys(locales);
|
||||||
translation: en,
|
const resources = Object.fromEntries(
|
||||||
},
|
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }])
|
||||||
it: {
|
);
|
||||||
translation: it,
|
i18n.use(initReactI18next).init({
|
||||||
},
|
|
||||||
nl: {
|
|
||||||
translation: nl,
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
translation: tr,
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
translation: fr,
|
|
||||||
},
|
|
||||||
de: {
|
|
||||||
translation: de,
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
translation: zh,
|
|
||||||
},
|
|
||||||
cs: {
|
|
||||||
translation: cs,
|
|
||||||
},
|
|
||||||
pirate: {
|
|
||||||
translation: pirate,
|
|
||||||
},
|
|
||||||
vi: {
|
|
||||||
translation: vi,
|
|
||||||
},
|
|
||||||
pl: {
|
|
||||||
translation: pl,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
i18n
|
|
||||||
// pass the i18n instance to react-i18next.
|
|
||||||
.use(initReactI18next)
|
|
||||||
// init i18next
|
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
|
||||||
.init({
|
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
resources: locales,
|
resources,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // not needed for react as it escapes by default
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const appLanguageOptions = captionLanguages.filter((x) => {
|
export const appLanguageOptions = langCodes.map((lang) => {
|
||||||
return Object.keys(locales).includes(x.id);
|
const [langObj] = ISO6391.getLanguages([lang]);
|
||||||
|
if (!langObj)
|
||||||
|
throw new Error(`Language with code ${lang} cannot be found in database`);
|
||||||
|
return langObj;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|
1338
src/setup/iso6391.ts
1338
src/setup/iso6391.ts
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,3 @@
|
||||||
import { LangCode } from "@/setup/iso6391";
|
|
||||||
|
|
||||||
export interface CaptionStyleSettings {
|
export interface CaptionStyleSettings {
|
||||||
color: string;
|
color: string;
|
||||||
/**
|
/**
|
||||||
|
@ -18,7 +16,7 @@ export interface CaptionSettingsV1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CaptionSettings {
|
export interface CaptionSettings {
|
||||||
language: LangCode;
|
language: string;
|
||||||
/**
|
/**
|
||||||
* Range is [-10, 10]s
|
* Range is [-10, 10]s
|
||||||
*/
|
*/
|
||||||
|
@ -26,11 +24,11 @@ export interface CaptionSettings {
|
||||||
style: CaptionStyleSettings;
|
style: CaptionStyleSettings;
|
||||||
}
|
}
|
||||||
export interface MWSettingsDataV1 {
|
export interface MWSettingsDataV1 {
|
||||||
language: LangCode;
|
language: string;
|
||||||
captionSettings: CaptionSettingsV1;
|
captionSettings: CaptionSettingsV1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MWSettingsData {
|
export interface MWSettingsData {
|
||||||
language: LangCode;
|
language: string;
|
||||||
captionSettings: CaptionSettings;
|
captionSettings: CaptionSettings;
|
||||||
}
|
}
|
||||||
|
|
11
src/stores/__old/utils.ts
Normal file
11
src/stores/__old/utils.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
function normalizeTitle(title: string): string {
|
||||||
|
return title
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/['":]/g, "")
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareTitle(a: string, b: string): boolean {
|
||||||
|
return normalizeTitle(a) === normalizeTitle(b);
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||||
import { searchForMedia } from "@/backend/metadata/search";
|
import { searchForMedia } from "@/backend/metadata/search";
|
||||||
import { mediaItemTypeToMediaType } from "@/backend/metadata/tmdb";
|
import { mediaItemTypeToMediaType } from "@/backend/metadata/tmdb";
|
||||||
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types/mw";
|
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { compareTitle } from "@/utils/titleMatch";
|
import { compareTitle } from "@/stores/__old/utils";
|
||||||
|
|
||||||
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ export function BannerLocation(props: { location?: string }) {
|
||||||
<div>
|
<div>
|
||||||
{!isOnline ? (
|
{!isOnline ? (
|
||||||
<Banner id="offline" type="error">
|
<Banner id="offline" type="error">
|
||||||
{t("errors.offline")}
|
{t("navigation.banner.offline")}
|
||||||
</Banner>
|
</Banner>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
export function normalizeTitle(title: string): string {
|
|
||||||
return title
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/['":]/g, "")
|
|
||||||
.replace(/[^a-zA-Z0-9]+/g, "_");
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { normalizeTitle } from "./normalizeTitle";
|
|
||||||
|
|
||||||
export function compareTitle(a: string, b: string): boolean {
|
|
||||||
return normalizeTitle(a) === normalizeTitle(b);
|
|
||||||
}
|
|
Loading…
Reference in a new issue