Remove unused files/functions + localize everything except player and pages + reorganize files + fix lint warnings

This commit is contained in:
mrjvs 2023-11-26 16:04:23 +01:00
parent 50b625c604
commit 0ef492f58b
77 changed files with 234 additions and 1966 deletions

View File

@ -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",

View File

@ -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
View File

@ -0,0 +1,5 @@
import en from "@/assets/locales/en.json";
export const locales = {
en,
};

View 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."
}
}
}

View File

@ -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 = {

View File

@ -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])

View File

@ -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 />

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 ? (

View File

@ -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 ? (

View File

@ -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">

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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,
})}

View File

@ -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,

View File

@ -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";

View File

@ -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" />

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -1,4 +1,4 @@
import { Transition } from "@/components/Transition";
import { Transition } from "@/components/utils/Transition";
export function BlackOverlay(props: { show?: boolean }) {
return (

View File

@ -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: {

View File

@ -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;

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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>
);
}

View File

@ -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,
};
}

View File

@ -1,17 +0,0 @@
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -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];
};

View File

@ -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,
};
}

View File

@ -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 />;
}

View File

@ -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";

View File

@ -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() {

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -7,7 +7,6 @@ import {
scrapePartsToProviderMetric,
useReportProviders,
} from "@/backend/helpers/report";
import { usePlayer } from "@/components/player/hooks/usePlayer";
import {
ScrapeCard,
ScrapeItem,

View File

@ -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";

View File

@ -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";

View File

@ -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: {

View File

@ -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";

View File

@ -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";

View File

@ -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);

View File

@ -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";

View File

@ -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";

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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);
}

View File

@ -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";

View File

@ -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>

View File

@ -1,7 +0,0 @@
export function normalizeTitle(title: string): string {
return title
.trim()
.toLowerCase()
.replace(/['":]/g, "")
.replace(/[^a-zA-Z0-9]+/g, "_");
}

View File

@ -1,5 +0,0 @@
import { normalizeTitle } from "./normalizeTitle";
export function compareTitle(a: string, b: string): boolean {
return normalizeTitle(a) === normalizeTitle(b);
}