diff --git a/.env b/.env deleted file mode 100644 index c459b80a..00000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -# this is to prevent warnings in webpack builds -GENERATE_SOURCEMAP=false - -# uncomment and add the following line to `.env.local` if you are running behind a proxy or on a subdirectory -# PUBLIC_URL=https://your-domain.com/directory-here diff --git a/README.md b/README.md index 3b696bf3..5b3e7d01 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

movie-web

-GitHub Workflow Status +GitHub Workflow Status GitHub license GitHub forks GitHub stars
diff --git a/index.html b/index.html new file mode 100644 index 00000000..822f74f3 --- /dev/null +++ b/index.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + movie-web + + + +

+ + + diff --git a/package.json b/package.json index 0e789eb5..4807d235 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "2.0.5", + "version": "2.1.0", "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": { @@ -8,18 +8,22 @@ "crypto-js": "^4.1.1", "fuse.js": "^6.4.6", "hls.js": "^1.0.7", + "i18next": "^22.4.5", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-http-backend": "^2.1.0", "json5": "^2.2.0", "nanoid": "^4.0.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-i18next": "^12.1.1", "react-router-dom": "^5.2.0", - "react-scripts": "5.0.1", "srt-webvtt": "^2.0.0", "unpacker": "^1.0.1" }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src", "lint:strict": "eslint --ext .tsx,.ts --max-warnings 0 src" }, @@ -44,7 +48,8 @@ "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.13.0", "@typescript-eslint/parser": "^5.13.0", - "autoprefixer": "^10.4.2", + "@vitejs/plugin-react-swc": "^3.0.0", + "autoprefixer": "^10.4.13", "eslint": "^8.10.0", "eslint-config-airbnb": "19.0.4", "eslint-config-prettier": "^8.5.0", @@ -53,11 +58,12 @@ "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.3.0", - "postcss": "^8.4.6", + "postcss": "^8.4.20", "prettier": "^2.5.1", "prettier-plugin-tailwindcss": "^0.1.7", - "tailwind-scrollbar": "^1.3.1", - "tailwindcss": "^3.0.20", - "typescript": "^4.6.4" + "tailwind-scrollbar": "^2.0.1", + "tailwindcss": "^3.2.4", + "typescript": "^4.6.4", + "vite": "^4.0.1" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 9705cefa..00000000 --- a/public/index.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - movie-web - - - -
- - diff --git a/public/locales/en-GB/translation.json b/public/locales/en-GB/translation.json new file mode 100644 index 00000000..cbc2eba3 --- /dev/null +++ b/public/locales/en-GB/translation.json @@ -0,0 +1,46 @@ +{ + "global": { + "name": "movie-web" + }, + "search": { + "loading": "Fetching your favourite shows...", + "providersFailed": "{{fails}}/{{total}} providers failed!", + "allResults": "That's all we have!", + "noResults": "We couldn't find anything!", + "allFailed": "All providers have failed!", + "headingTitle": "Search results", + "headingLink": "Back to home", + "bookmarks": "Bookmarks", + "continueWatching": "Continue Watching", + "tagline": "Because watching legally is boring", + "title": "What do you want to watch?", + "placeholder": "What do you want to watch?" + }, + "media": { + "invalidUrl": "Your URL may be invalid", + "arrowText": "Go back" + }, + "notFound": { + "backArrow": "Back to home", + "media": { + "title": "Couldn't find that media", + "description": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL" + }, + "provider": { + "title": "This provider has been disabled", + "description": "We had issues with the provider or it was too unstable to use, so we had to disable it." + }, + "page": { + "title": "Couldn't find that page", + "description": "We looked everywhere: under the bins, in the closet, behind the proxy but ultimately couldn't find the page you are looking for." + } + }, + "searchBar": { + "movie": "Movie", + "series": "Series", + "Search": "Search" + }, + "errorBoundary": { + "text": "The app encountered an error and wasn't able to recover, please report it to the" + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index e67c09b8..b427c62e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,8 @@ -import { MWMediaType } from "providers"; import { Redirect, Route, Switch } from "react-router-dom"; -import { BookmarkContextProvider } from "state/bookmark"; -import { WatchedContextProvider } from "state/watched"; -import { NotFoundPage } from "views/notfound/NotFoundView"; +import { MWMediaType } from "@/providers"; +import { BookmarkContextProvider } from "@/state/bookmark"; +import { WatchedContextProvider } from "@/state/watched"; +import { NotFoundPage } from "@/views/notfound/NotFoundView"; import "./index.css"; import { MediaView } from "./views/MediaView"; import { SearchView } from "./views/SearchView"; diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index 32c89cb8..9ebef561 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -1,7 +1,7 @@ -import { Icon, Icons } from "components/Icon"; import React, { Fragment } from "react"; import { Listbox, Transition } from "@headlessui/react"; +import { Icon, Icons } from "@/components/Icon"; export interface OptionItem { id: string; @@ -14,48 +14,46 @@ interface DropdownProps { options: Array; } -export const Dropdown = React.forwardRef( - (props: DropdownProps) => ( -
- - {({ open }) => ( - <> - - {props.selectedItem.name} - - - - - - - {props.options.map((opt) => ( - - `relative cursor-default select-none py-2 pl-10 pr-4 ${ - active ? "bg-denim-400 text-bink-700" : "text-white" - }` - } - key={opt.id} - value={opt} - > - {opt.name} - - ))} - - - - )} - -
- ) -); +export function Dropdown(props: DropdownProps) { +
+ + {({ open }) => ( + <> + + {props.selectedItem.name} + + + + + + + {props.options.map((opt) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? "bg-denim-400 text-bink-700" : "text-white" + }` + } + key={opt.id} + value={opt} + > + {opt.name} + + ))} + + + + )} + +
; +} diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 9caef7da..6a5c3ad4 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { MWMediaType, MWQuery } from "providers"; +import { MWMediaType, MWQuery } from "@/providers"; +import { useTranslation } from "react-i18next"; import { DropdownButton } from "./buttons/DropdownButton"; import { Icons } from "./Icon"; import { TextInputControl } from "./text-inputs/TextInputControl"; @@ -13,6 +14,8 @@ export interface SearchBarProps { } export function SearchBarInput(props: SearchBarProps) { + const { t } = useTranslation(); + const [dropdownOpen, setDropdownOpen] = useState(false); function setSearch(value: string) { props.onChange( @@ -52,12 +55,12 @@ export function SearchBarInput(props: SearchBarProps) { options={[ { id: MWMediaType.MOVIE, - name: "Movie", + name: t('searchBar.movie'), icon: Icons.FILM, }, { id: MWMediaType.SERIES, - name: "Series", + name: t('searchBar.series'), icon: Icons.CLAPPER_BOARD, }, // { @@ -68,7 +71,7 @@ export function SearchBarInput(props: SearchBarProps) { ]} onClick={() => setDropdownOpen((old) => !old)} > - {props.buttonText || "Search"} + {props.buttonText || t('searchBar.search')} ); diff --git a/src/components/buttons/DropdownButton.tsx b/src/components/buttons/DropdownButton.tsx index 0dbfcb71..5c1a12c5 100644 --- a/src/components/buttons/DropdownButton.tsx +++ b/src/components/buttons/DropdownButton.tsx @@ -1,12 +1,12 @@ -import { Icon, Icons } from "components/Icon"; import React, { MouseEventHandler, SyntheticEvent, useEffect, useState, } from "react"; +import { Icon, Icons } from "@/components/Icon"; -import { Backdrop, useBackdrop } from "components/layout/Backdrop"; +import { Backdrop, useBackdrop } from "@/components/layout/Backdrop"; import { ButtonControlProps, ButtonControl } from "./ButtonControl"; export interface OptionItem { @@ -33,7 +33,7 @@ export interface OptionProps { function Option({ option, onClick, tabIndex }: OptionProps) { return (
@@ -95,7 +95,7 @@ export const DropdownButton = React.forwardRef< > {selectedItem.name} @@ -105,7 +105,7 @@ export const DropdownButton = React.forwardRef< />
{props.children} diff --git a/src/components/buttons/IconPatch.tsx b/src/components/buttons/IconPatch.tsx index 1a80061c..f14a3f56 100644 --- a/src/components/buttons/IconPatch.tsx +++ b/src/components/buttons/IconPatch.tsx @@ -1,4 +1,4 @@ -import { Icon, Icons } from "components/Icon"; +import { Icon, Icons } from "@/components/Icon"; export interface IconPatchProps { active?: boolean; @@ -12,11 +12,11 @@ export function IconPatch(props: IconPatchProps) { return (
diff --git a/src/components/layout/Backdrop.tsx b/src/components/layout/Backdrop.tsx index cc5b68f2..65d3a81d 100644 --- a/src/components/layout/Backdrop.tsx +++ b/src/components/layout/Backdrop.tsx @@ -1,5 +1,5 @@ -import { useFade } from "hooks/useFade"; import { useEffect, useState } from "react"; +import { useFade } from "@/hooks/useFade"; interface BackdropProps { onClick?: (e: MouseEvent) => void; diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index b2b8eabd..3df0be76 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -1,16 +1,18 @@ -import { Icon, Icons } from "components/Icon"; +import { Icon, Icons } from "@/components/Icon"; +import { useTranslation } from "react-i18next"; export function BrandPill(props: { clickable?: boolean }) { + const { t } = useTranslation(); + return (
- movie-web + {t('global.name')}
); } diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx index 3bd20e88..75aab1e1 100644 --- a/src/components/layout/ErrorBoundary.tsx +++ b/src/components/layout/ErrorBoundary.tsx @@ -1,9 +1,9 @@ -import { IconPatch } from "components/buttons/IconPatch"; -import { Icons } from "components/Icon"; -import { Link } from "components/text/Link"; -import { Title } from "components/text/Title"; -import { DISCORD_LINK, GITHUB_LINK } from "mw_constants"; import { Component } from "react"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { Link } from "@/components/text/Link"; +import { Title } from "@/components/text/Title"; +import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants"; interface ErrorBoundaryState { hasError: boolean; diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index dfc8891f..0a255395 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -1,8 +1,8 @@ -import { IconPatch } from "components/buttons/IconPatch"; -import { Icons } from "components/Icon"; -import { DISCORD_LINK, GITHUB_LINK } from "mw_constants"; import { ReactNode } from "react"; import { Link } from "react-router-dom"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { DISCORD_LINK, GITHUB_LINK } from "@/mw_constants"; import { BrandPill } from "./BrandPill"; export interface NavigationProps { @@ -11,8 +11,8 @@ export interface NavigationProps { export function Navigation(props: NavigationProps) { return ( -
-
+
+
@@ -20,7 +20,11 @@ export function Navigation(props: NavigationProps) {
{props.children}
-
+
-
+
{!props.error ? ( <> -
-
-
+
+
+
) : (
-

Failed to load seasons and episodes

+

{t('seasons.failed')}

)}
@@ -42,6 +45,8 @@ export function LoadingSeasons(props: { error?: boolean }) { } export function Seasons(props: SeasonsProps) { + const { t } = useTranslation(); + const [searchSeasons, loading, error, success] = useLoading( (portableMedia: MWPortableMedia) => getSeasonDataFromMedia(portableMedia) ); @@ -70,7 +75,7 @@ export function Seasons(props: SeasonsProps) { const mapSeason = (season: MWMediaSeason) => ({ id: season.id, - name: season.title || `Season ${season.sort}`, + name: season.title || `${t('seasons.season')} ${season.sort}`, }); const options = seasons.seasons.map(mapSeason); diff --git a/src/components/layout/SectionHeading.tsx b/src/components/layout/SectionHeading.tsx index e341496b..fd89d47a 100644 --- a/src/components/layout/SectionHeading.tsx +++ b/src/components/layout/SectionHeading.tsx @@ -1,6 +1,6 @@ -import { Icon, Icons } from "components/Icon"; -import { ArrowLink } from "components/text/ArrowLink"; import { ReactNode } from "react"; +import { Icon, Icons } from "@/components/Icon"; +import { ArrowLink } from "@/components/text/ArrowLink"; interface SectionHeadingProps { icon?: Icons; @@ -15,7 +15,7 @@ export function SectionHeading(props: SectionHeadingProps) { return (
-

+

{props.icon ? ( diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 1e8ceb02..2171f7cb 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,13 +1,13 @@ +import { Link } from "react-router-dom"; import { convertMediaToPortable, getProviderFromId, MWMediaMeta, MWMediaType, -} from "providers"; -import { Link } from "react-router-dom"; -import { Icon, Icons } from "components/Icon"; -import { serializePortableMedia } from "hooks/usePortableMedia"; -import { DotList } from "components/text/DotList"; +} from "@/providers"; +import { Icon, Icons } from "@/components/Icon"; +import { serializePortableMedia } from "@/hooks/usePortableMedia"; +import { DotList } from "@/components/text/DotList"; export interface MediaCardProps { media: MWMediaMeta; @@ -30,7 +30,7 @@ function MediaCardContent({ return (

@@ -38,12 +38,12 @@ function MediaCardContent({ {watchedPercentage > 0 ? (
-
+
) : null} @@ -54,7 +54,7 @@ function MediaCardContent({

{media.title} {series && media.seasonId && media.episodeId ? ( - + S{media.seasonId} E{media.episodeId} ) : null} diff --git a/src/components/media/VideoPlayer.tsx b/src/components/media/VideoPlayer.tsx index 92ea4d1c..0009922e 100644 --- a/src/components/media/VideoPlayer.tsx +++ b/src/components/media/VideoPlayer.tsx @@ -1,9 +1,9 @@ -import { IconPatch } from "components/buttons/IconPatch"; -import { Icons } from "components/Icon"; -import { Loading } from "components/layout/Loading"; -import { MWMediaCaption, MWMediaStream } from "providers"; import { ReactElement, useEffect, useRef, useState } from "react"; import Hls from "hls.js"; +import { IconPatch } from "@/components/buttons/IconPatch"; +import { Icons } from "@/components/Icon"; +import { Loading } from "@/components/layout/Loading"; +import { MWMediaCaption, MWMediaStream } from "@/providers"; export interface VideoPlayerProps { source: MWMediaStream; @@ -14,7 +14,7 @@ export interface VideoPlayerProps { export function SkeletonVideoPlayer(props: { error?: boolean }) { return ( -
+
{props.error ? (
@@ -44,8 +44,7 @@ export function VideoPlayer(props: VideoPlayerProps) { // hls support if (mustUseHls) { - if (!videoRef.current) - return; + if (!videoRef.current) return; if (!Hls.isSupported()) { setLoading(false); @@ -55,7 +54,7 @@ export function VideoPlayer(props: VideoPlayerProps) { const hls = new Hls(); - if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { + if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { videoRef.current.src = props.source.url; return; } @@ -81,8 +80,7 @@ export function VideoPlayer(props: VideoPlayerProps) { <> {skeletonUi}