mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 22:46:05 +00: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
|
@ -22,6 +22,7 @@
|
|||
"hls.js": "^1.0.7",
|
||||
"i18next": "^22.4.5",
|
||||
"immer": "^10.0.2",
|
||||
"iso-639-1": "^3.1.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"ofetch": "^1.0.0",
|
||||
|
|
|
@ -65,6 +65,9 @@ dependencies:
|
|||
immer:
|
||||
specifier: ^10.0.2
|
||||
version: 10.0.2
|
||||
iso-639-1:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
lodash.isequal:
|
||||
specifier: ^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 { 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";
|
||||
|
||||
export type ProviderMetric = {
|
||||
|
|
|
@ -5,24 +5,24 @@ export interface FlagIconProps {
|
|||
countryCode?: string;
|
||||
}
|
||||
|
||||
export function FlagIcon(props: FlagIconProps) {
|
||||
// Country code overrides
|
||||
const countryOverrides: Record<string, string> = {
|
||||
en: "gb",
|
||||
cs: "cz",
|
||||
el: "gr",
|
||||
fa: "ir",
|
||||
ko: "kr",
|
||||
he: "il",
|
||||
ze: "cn",
|
||||
ar: "sa",
|
||||
ja: "jp",
|
||||
bs: "ba",
|
||||
vi: "vn",
|
||||
zh: "cn",
|
||||
sl: "si",
|
||||
};
|
||||
// Country code overrides
|
||||
const countryOverrides: Record<string, string> = {
|
||||
en: "gb",
|
||||
cs: "cz",
|
||||
el: "gr",
|
||||
fa: "ir",
|
||||
ko: "kr",
|
||||
he: "il",
|
||||
ze: "cn",
|
||||
ar: "sa",
|
||||
ja: "jp",
|
||||
bs: "ba",
|
||||
vi: "vn",
|
||||
zh: "cn",
|
||||
sl: "si",
|
||||
};
|
||||
|
||||
export function FlagIcon(props: FlagIconProps) {
|
||||
let countryCode =
|
||||
(props.countryCode || "")?.split("-").pop()?.toLowerCase() || "";
|
||||
if (countryOverrides[countryCode])
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import classNames from "classnames";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||
import { UserAvatar } from "@/components/Avatar";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { useAuth } from "@/hooks/auth/useAuth";
|
||||
import { conf } from "@/setup/config";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
@ -81,6 +82,7 @@ function CircleDropdownLink(props: { icon: Icons; href: string }) {
|
|||
}
|
||||
|
||||
export function LinksDropdown(props: { children: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const deviceName = useAuthStore((s) => s.account?.deviceName);
|
||||
const seed = useAuthStore((s) => s.account?.seed);
|
||||
|
@ -130,18 +132,18 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||
</DropdownLink>
|
||||
) : (
|
||||
<DropdownLink href="/login" icon={Icons.RISING_STAR} highlight>
|
||||
Sync to cloud
|
||||
{t("navigation.menu.register")}
|
||||
</DropdownLink>
|
||||
)}
|
||||
<Divider />
|
||||
<DropdownLink href="/settings" icon={Icons.SETTINGS}>
|
||||
Settings
|
||||
{t("navigation.menu.settings")}
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/faq" icon={Icons.EPISODES}>
|
||||
About us
|
||||
{t("navigation.menu.about")}
|
||||
</DropdownLink>
|
||||
<DropdownLink href="/faq" icon={Icons.FILM}>
|
||||
HELP MEEE
|
||||
{t("navigation.menu.support")}
|
||||
</DropdownLink>
|
||||
{deviceName ? (
|
||||
<DropdownLink
|
||||
|
@ -149,7 +151,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) {
|
|||
icon={Icons.LOGOUT}
|
||||
onClick={logout}
|
||||
>
|
||||
Log out
|
||||
{t("navigation.menu.logout")}
|
||||
</DropdownLink>
|
||||
) : null}
|
||||
<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 { ButtonControl } from "./ButtonControl";
|
||||
|
||||
export interface EditButtonProps {
|
||||
editing: boolean;
|
||||
onEdit?: (editing: boolean) => void;
|
||||
|
@ -20,7 +18,8 @@ export function EditButton(props: EditButtonProps) {
|
|||
}, [props]);
|
||||
|
||||
return (
|
||||
<ButtonControl
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
|
@ -33,6 +32,6 @@ export function EditButton(props: EditButtonProps) {
|
|||
<Icon icon={Icons.EDIT} />
|
||||
)}
|
||||
</span>
|
||||
</ButtonControl>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import classNames from "classnames";
|
|||
|
||||
import { Icon, Icons } from "../Icon";
|
||||
|
||||
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"];
|
||||
export const initialColor = colors[0];
|
||||
|
||||
export function ColorPicker(props: {
|
||||
label: string;
|
||||
value: string;
|
||||
onInput: (v: string) => void;
|
||||
}) {
|
||||
// Migrate this to another file later
|
||||
const colors = ["#2E65CF", "#7652DD", "#CF2E68", "#C2CF2E", "#2ECFA8"];
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{props.label ? (
|
||||
|
|
|
@ -2,20 +2,20 @@ import classNames from "classnames";
|
|||
|
||||
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: {
|
||||
label: string;
|
||||
value: UserIcons;
|
||||
onInput: (v: UserIcons) => void;
|
||||
}) {
|
||||
// Migrate this to another file later
|
||||
const icons = [
|
||||
UserIcons.USER,
|
||||
UserIcons.BOOKMARK,
|
||||
UserIcons.CLOCK,
|
||||
UserIcons.EYE_SLASH,
|
||||
UserIcons.SEARCH,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{props.label ? (
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCopyToClipboard, useMountedState } from "react-use";
|
||||
|
||||
import { Icon, Icons } from "./Icon";
|
||||
import { Icon, Icons } from "../Icon";
|
||||
|
||||
export function PassphraseDisplay(props: { mnemonic: string }) {
|
||||
const { t } = useTranslation();
|
||||
const individualWords = props.mnemonic.split(" ");
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
|
@ -33,7 +35,7 @@ export function PassphraseDisplay(props: { mnemonic: string }) {
|
|||
icon={hasCopied ? Icons.CHECKMARK : Icons.COPY}
|
||||
className={hasCopied ? "text-xs" : ""}
|
||||
/>
|
||||
<span className="text-sm">Copy</span>
|
||||
<span className="text-sm">{t("actions.copy")}</span>
|
||||
</button>
|
||||
</div>
|
||||
<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 { Icon, Icons } from "./Icon";
|
||||
import { TextInputControl } from "./text-inputs/TextInputControl";
|
||||
import { Icon, Icons } from "../Icon";
|
||||
import { TextInputControl } from "../text-inputs/TextInputControl";
|
||||
|
||||
export interface SearchBarProps {
|
||||
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 { useHistory } from "react-router-dom";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
|
@ -6,16 +7,18 @@ import { WideContainer } from "@/components/layout/WideContainer";
|
|||
import { conf } from "@/setup/config";
|
||||
|
||||
function FooterLink(props: {
|
||||
href: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
icon: Icons;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={props.href}
|
||||
href={props.href ?? "#"}
|
||||
target="_blank"
|
||||
className="tabbable rounded py-2 px-3 inline-flex items-center space-x-3 transition-colors duration-200 hover:text-type-emphasis"
|
||||
rel="noreferrer"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon icon={props.icon} className="text-2xl" />
|
||||
<span className="font-medium">{props.children}</span>
|
||||
|
@ -25,8 +28,10 @@ function FooterLink(props: {
|
|||
|
||||
function Dmca() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<FooterLink icon={Icons.DRAGON} href="https://youtu.be/-WOonkg_ZCo">
|
||||
<FooterLink icon={Icons.DRAGON} onClick={() => history.push("/dmca")}>
|
||||
{t("footer.links.dmca")}
|
||||
</FooterLink>
|
||||
);
|
||||
|
|
|
@ -37,7 +37,7 @@ function MediaCardContent({
|
|||
|
||||
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());
|
||||
|
||||
return (
|
||||
|
@ -82,7 +82,7 @@ function MediaCardContent({
|
|||
closable ? "" : "group-hover:text-white",
|
||||
].join(" ")}
|
||||
>
|
||||
{t("seasons.seasonAndEpisode", {
|
||||
{t("media.episodeDisplay", {
|
||||
season: series.season || 1,
|
||||
episode: series.episode,
|
||||
})}
|
||||
|
|
|
@ -3,7 +3,7 @@ import FocusTrap from "focus-trap-react";
|
|||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import {
|
||||
useInternalOverlayRouter,
|
||||
useRouterAnchorUpdate,
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import classNames from "classnames";
|
||||
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 { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { useOverlayStore } from "@/stores/overlay/store";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from "classnames";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useInternalOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
|
||||
|
@ -10,6 +11,7 @@ interface MobilePositionProps {
|
|||
|
||||
export function OverlayMobilePosition(props: MobilePositionProps) {
|
||||
const router = useInternalOverlayRouter("hello world :)");
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -26,7 +28,7 @@ export function OverlayMobilePosition(props: MobilePositionProps) {
|
|||
type="button"
|
||||
onClick={() => router.close()}
|
||||
>
|
||||
Close
|
||||
{t("overlays.close")}
|
||||
</button>
|
||||
{/* 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" />
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback } from "react";
|
|||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
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 { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { useEmpheralVolumeStore } from "@/stores/volume";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { OverlayPage } from "@/components/overlays/OverlayPage";
|
||||
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 }) {
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function BottomControls(props: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from "classnames";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
|
||||
export function CenterMobileControls(props: {
|
||||
children: React.ReactNode;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
parseSubtitles,
|
||||
sanitize,
|
||||
} from "@/components/player/utils/captions";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { useBannerSize } from "@/stores/banner";
|
||||
import { BannerLocation } from "@/stores/banner/BannerLocation";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from "classnames";
|
|||
import { ReactNode } from "react";
|
||||
|
||||
import { StatusCircle } from "@/components/player/internals/StatusCircle";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
|
||||
export interface ScrapeItemProps {
|
||||
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
||||
|
|
|
@ -2,7 +2,7 @@ import { a, to, useSpring } from "@react-spring/web";
|
|||
import classNames from "classnames";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
|
||||
export interface StatusCircle {
|
||||
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"/>
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { isChromecastAvailable } from "@/setup/chromecast";
|
||||
|
||||
|
@ -13,93 +13,3 @@ export function useChromecastAvailable() {
|
|||
|
||||
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 "./stores/__old/imports";
|
||||
import "@/setup/ga";
|
||||
import "@/setup/index.css";
|
||||
import "@/assets/css/index.css";
|
||||
|
||||
import React, { Suspense, useCallback } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BrowserRouter, HashRouter } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { useAuthRestore } from "@/hooks/auth/useAuthRestore";
|
||||
|
@ -44,8 +45,15 @@ registerSW({
|
|||
});
|
||||
|
||||
function LoadingScreen(props: { type: "user" | "lazy" }) {
|
||||
const mapping = {
|
||||
user: "screens.loadingUser",
|
||||
lazy: "screens.loadingApp",
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
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;
|
||||
showResetButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
|
||||
const resetBackend = useCallback(() => {
|
||||
setBackendUrl(null);
|
||||
|
@ -70,7 +79,7 @@ function ErrorScreen(props: {
|
|||
{props.showResetButton ? (
|
||||
<div className="mt-6">
|
||||
<Button theme="secondary" onClick={resetBackend}>
|
||||
Reset back-end
|
||||
{t("screens.loadingUserError.reset")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -82,14 +91,17 @@ function AuthWrapper() {
|
|||
const status = useAuthRestore();
|
||||
const backendUrl = conf().BACKEND_URL;
|
||||
const userBackendUrl = useBackendUrl();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (status.loading) return <LoadingScreen type="user" />;
|
||||
if (status.error)
|
||||
return (
|
||||
<ErrorScreen showResetButton={backendUrl !== userBackendUrl}>
|
||||
{backendUrl !== userBackendUrl
|
||||
? "Failed to fetch user data. Try resetting the backend URL"
|
||||
: "Failed to fetch user data."}
|
||||
{t(
|
||||
backendUrl !== userBackendUrl
|
||||
? "screens.loadingUserError.textWithReset"
|
||||
: "screens.loadingUserError.text"
|
||||
)}
|
||||
</ErrorScreen>
|
||||
);
|
||||
return <App />;
|
||||
|
@ -100,10 +112,11 @@ function MigrationRunner() {
|
|||
i18n.changeLanguage(useLanguageStore.getState().language);
|
||||
await initializeOldStores();
|
||||
}, []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (status.loading) return <MigrationPart />;
|
||||
if (status.error)
|
||||
return <ErrorScreen>Failed to migrate your data.</ErrorScreen>;
|
||||
return <ErrorScreen>{t("screens.migration.failed")}</ErrorScreen>;
|
||||
return <AuthWrapper />;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { getSessions, updateSession } from "@/backend/accounts/sessions";
|
||||
import { updateSettings } from "@/backend/accounts/settings";
|
||||
import { editUser } from "@/backend/accounts/user";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { UserIcons } from "@/components/UserIcon";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
|
||||
// mostly empty view, add whatever you need
|
||||
export default function TestView() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Dropdown } from "@/components/Dropdown";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Dropdown } from "@/components/form/Dropdown";
|
||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||
import { Title } from "@/components/text/Title";
|
||||
import { TextInputControl } from "@/components/text-inputs/TextInputControl";
|
||||
|
|
|
@ -2,10 +2,9 @@ import { useState } from "react";
|
|||
import { useAsyncFn } from "react-use";
|
||||
|
||||
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 { Box } from "@/components/layout/Box";
|
||||
import { Spinner } from "@/components/layout/Spinner";
|
||||
import { Divider } from "@/components/utils/Divider";
|
||||
import { Heading2 } from "@/components/utils/Text";
|
||||
import { conf } from "@/setup/config";
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useAsyncFn } from "react-use";
|
|||
|
||||
import { getMediaDetails } from "@/backend/metadata/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 { Box } from "@/components/layout/Box";
|
||||
import { Spinner } from "@/components/layout/Spinner";
|
||||
|
|
|
@ -3,10 +3,9 @@ import { useMemo, useState } from "react";
|
|||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { mwFetch } from "@/backend/helpers/fetch";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Box } from "@/components/layout/Box";
|
||||
import { Spinner } from "@/components/layout/Spinner";
|
||||
import { Divider } from "@/components/utils/Divider";
|
||||
import { Heading2 } from "@/components/utils/Text";
|
||||
import { conf } from "@/setup/config";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { ColorPicker } from "@/components/form/ColorPicker";
|
||||
import { IconPicker } from "@/components/form/IconPicker";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useState } from "react";
|
|||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { verifyValidMnemonic } from "@/backend/accounts/crypto";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { BrandPill } from "@/components/layout/BrandPill";
|
||||
import {
|
||||
LargeCard,
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { useMemo } from "react";
|
||||
|
||||
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 {
|
||||
LargeCard,
|
||||
LargeCardButtons,
|
||||
LargeCardText,
|
||||
} from "@/components/layout/LargeCard";
|
||||
import { PassphraseDisplay } from "@/components/PassphraseDisplay";
|
||||
|
||||
interface PassphraseGeneratePartProps {
|
||||
onNext?: (mnemonic: string) => void;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useHistory } from "react-router-dom";
|
|||
import { useAsync } from "react-use";
|
||||
|
||||
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 {
|
||||
LargeCard,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useGoogleReCaptcha } from "react-google-recaptcha-v3";
|
|||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { updateSettings } from "@/backend/accounts/settings";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
LargeCard,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useRef, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
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 { IconPill } from "@/components/layout/IconPill";
|
||||
import { Title } from "@/components/text/Title";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { IconPill } from "@/components/layout/IconPill";
|
||||
import { Navigation } from "@/components/layout/Navigation";
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import Sticky from "react-sticky-el";
|
||||
|
||||
import { SearchBarInput } from "@/components/form/SearchBar";
|
||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||
import { SearchBarInput } from "@/components/SearchBar";
|
||||
import { HeroTitle } from "@/components/text/HeroTitle";
|
||||
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { AsyncReturnType } from "type-fest";
|
|||
import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta";
|
||||
import { decodeTMDBId } from "@/backend/metadata/tmdb";
|
||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { IconPill } from "@/components/layout/IconPill";
|
||||
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 { IconPill } from "@/components/layout/IconPill";
|
||||
import { Paragraph } from "@/components/text/Paragraph";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { IconPill } from "@/components/layout/IconPill";
|
||||
import { Paragraph } from "@/components/text/Paragraph";
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
scrapePartsToProviderMetric,
|
||||
useReportProviders,
|
||||
} from "@/backend/helpers/report";
|
||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||
import {
|
||||
ScrapeCard,
|
||||
ScrapeItem,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAsyncFn } from "react-use";
|
||||
|
||||
import { deleteUser } from "@/backend/accounts/user";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
||||
import { Modal, ModalCard, useModal } from "@/components/overlays/Modal";
|
||||
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Avatar } from "@/components/Avatar";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
} from "@/components/player/atoms/settings/CaptionSettingsView";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { CaptionCue } from "@/components/player/Player";
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import { SubtitleStyling } from "@/stores/subtitles";
|
||||
|
||||
export function CaptionPreview(props: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Toggle } from "@/components/buttons/Toggle";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useAsyncFn } from "react-use";
|
|||
import { SessionResponse } from "@/backend/accounts/auth";
|
||||
import { base64ToBuffer, decryptData } from "@/backend/accounts/crypto";
|
||||
import { removeSession } from "@/backend/accounts/sessions";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import { SettingsCard } from "@/components/layout/SettingsCard";
|
||||
import { SecondaryLabel } from "@/components/text/SecondaryLabel";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Dropdown } from "@/components/Dropdown";
|
||||
import { FlagIcon } from "@/components/FlagIcon";
|
||||
import { Dropdown } from "@/components/form/Dropdown";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { appLanguageOptions } from "@/setup/i18n";
|
||||
import { sortLangCodes } from "@/utils/sortLangCodes";
|
||||
|
@ -8,14 +8,14 @@ export function LocalePart(props: {
|
|||
language: string;
|
||||
setLanguage: (l: string) => void;
|
||||
}) {
|
||||
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.id));
|
||||
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.code));
|
||||
|
||||
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) => ({
|
||||
id: opt.id,
|
||||
name: `${opt.englishName} — ${opt.nativeName}`,
|
||||
leftIcon: <FlagIcon countryCode={opt.id} />,
|
||||
id: opt.code,
|
||||
name: `${opt.name} — ${opt.nativeName}`,
|
||||
leftIcon: <FlagIcon countryCode={opt.code} />,
|
||||
}));
|
||||
|
||||
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 { IconPicker } from "@/components/form/IconPicker";
|
||||
import { Modal, ModalCard } from "@/components/overlays/Modal";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useHistory } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { SolidSettingsCard } from "@/components/layout/SettingsCard";
|
||||
import { Heading3 } from "@/components/utils/Text";
|
||||
|
||||
|
|
|
@ -1,70 +1,27 @@
|
|||
import i18n from "i18next";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
import { locales } from "@/assets/languages";
|
||||
|
||||
// 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 langCodes = Object.keys(locales);
|
||||
const resources = Object.fromEntries(
|
||||
Object.entries(locales).map((entry) => [entry[0], { translation: entry[1] }])
|
||||
);
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: "en",
|
||||
resources,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
});
|
||||
|
||||
const locales = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
it: {
|
||||
translation: it,
|
||||
},
|
||||
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",
|
||||
resources: locales,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
});
|
||||
|
||||
export const appLanguageOptions = captionLanguages.filter((x) => {
|
||||
return Object.keys(locales).includes(x.id);
|
||||
export const appLanguageOptions = langCodes.map((lang) => {
|
||||
const [langObj] = ISO6391.getLanguages([lang]);
|
||||
if (!langObj)
|
||||
throw new Error(`Language with code ${lang} cannot be found in database`);
|
||||
return langObj;
|
||||
});
|
||||
|
||||
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 {
|
||||
color: string;
|
||||
/**
|
||||
|
@ -18,7 +16,7 @@ export interface CaptionSettingsV1 {
|
|||
}
|
||||
|
||||
export interface CaptionSettings {
|
||||
language: LangCode;
|
||||
language: string;
|
||||
/**
|
||||
* Range is [-10, 10]s
|
||||
*/
|
||||
|
@ -26,11 +24,11 @@ export interface CaptionSettings {
|
|||
style: CaptionStyleSettings;
|
||||
}
|
||||
export interface MWSettingsDataV1 {
|
||||
language: LangCode;
|
||||
language: string;
|
||||
captionSettings: CaptionSettingsV1;
|
||||
}
|
||||
|
||||
export interface MWSettingsData {
|
||||
language: LangCode;
|
||||
language: string;
|
||||
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 { mediaItemTypeToMediaType } from "@/backend/metadata/tmdb";
|
||||
import { MWMediaMeta, MWMediaType } from "@/backend/metadata/types/mw";
|
||||
import { compareTitle } from "@/utils/titleMatch";
|
||||
import { compareTitle } from "@/stores/__old/utils";
|
||||
|
||||
import { WatchedStoreData, WatchedStoreItem } from "../types";
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ export function BannerLocation(props: { location?: string }) {
|
|||
<div>
|
||||
{!isOnline ? (
|
||||
<Banner id="offline" type="error">
|
||||
{t("errors.offline")}
|
||||
{t("navigation.banner.offline")}
|
||||
</Banner>
|
||||
) : null}
|
||||
</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