diff --git a/.docs/content/2.self-hosting/3.client.md b/.docs/content/2.self-hosting/3.client.md index 14e025b1..d4cd12f4 100644 --- a/.docs/content/2.self-hosting/3.client.md +++ b/.docs/content/2.self-hosting/3.client.md @@ -1,7 +1,7 @@ # Setting up the client ## Vercel - Recommended -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmovie-web%2Fmovie-web&env=VITE_CORS_PROXY_URL,VITE_TMDB_READ_API_KEY) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fmovie-web%2Fmovie-web%2Ftree%2Fmaster&env=VITE_CORS_PROXY_URL,VITE_TMDB_READ_API_KEY) 1. Click the Deploy button. 1. Sign in using either a GitHub, GitLab, or Bitbucket. 1. Follow the instructions to create a repository for movie-web. diff --git a/.github/logo-dark.svg b/.github/logo-dark.svg new file mode 100644 index 00000000..e0258f10 --- /dev/null +++ b/.github/logo-dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.github/logo-light.svg b/.github/logo-light.svg new file mode 100644 index 00000000..dc5a242f --- /dev/null +++ b/.github/logo-light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/README.md b/README.md index fd21a07e..99a336ff 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,93 @@ -

movie-web

+

+

-GitHub Workflow Status -GitHub license -GitHub forks -GitHub stars -
-Discord Server + +
+ 🔵 discord 🟢 website

+

-movie-web is a web app for watching movies easily. Check it out at **[movie-web.app](https://movie-web.app)**. +# ⚡What is movie-web? + +movie-web is a web app for watching movies easily. Check it out at movie-web.app. This service works by displaying video files from third-party providers inside an intuitive and aesthetic user interface. -Features include: +# 🔥Features -- 🕑 Saving of your progress so you can come back to a video at any time! -- 🔖 Bookmarks to keep track of videos you would like to watch. -- 🎞️ Easy switching between seasons and episodes for a TV series; binge away! -- ✖️ Supports multiple types of content including movies, TV shows and Anime (coming soon™️) +- Automatic saving of progress - optionally synced to an account. +- Bookmark shows or movies, keep track of what you want to watch. +- Minimalistic interface that only shows whats required - no algorithm to consume you. -## Goals of movie-web +## 🍄 Philosophy -- No ads -- No BS: just a search bar and a video player -- No responsibility on the hoster, no databases or api's hosted by us, just a static site +This project is meant to be simple and easy to use. Keep features minimal but polished. +We do not want this project to be yet another bulky streaming site, instead it aims for minimalism. -## Self-hosting +On top of that, hosting should be as cheap and simple as possible. Just a static website with a proxy, with an optional backend if you want cross-device syncing. -A simple guide has been written to assist in hosting your own instance of movie-web. +Content is fetched from third parties and scraping is done fully done on the client. This means that the hoster has no files or media on their server. All files are streamed directly from the third parties. -Check it out here: [https://github.com/movie-web/movie-web/blob/dev/SELFHOSTING.md](https://github.com/movie-web/movie-web/blob/dev/SELFHOSTING.md) +## ⚠️ Limitations -## Running locally for development +- Due to being a static site, there can be no SSR +- To keep it cheap to host, amount of proxied requests need to be kept to a minimum +- Also to keep it cheap, no content must ever be streamed through the proxy. So only streams not protected by CORS headers. -To run this project locally for contributing or testing, run the following commands: -
note: must use pnpm to install packages and run NodeJS 16 (install with `npm i -g pnpm`)
+# 🧬 Running locally for development +To run locally, you must first clone the repository. After that run the following commands in the root of the repository: ```bash -git clone https://github.com/movie-web/movie-web -cd movie-web pnpm install pnpm run dev ``` -To build production files, simply run `pnpm run build`. +To build production files, run: +```bash +pnpm build +``` -You'll need to deploy a cloudflare service worker as well. Check the [selfhosting guide](https://github.com/movie-web/movie-web/blob/dev/SELFHOSTING.md) on how to run the service worker. Afterwards you can make a `.env` file and put in the URL. (see `example.env` for an example) +> [!TIP] +> You must use pnpm (`npm i -g pnpm`) and run NodeJS 20 -

Contributing - GitHub issues -GitHub pull requests

+# 🥔 Selfhosting -Check out [this project's issues](https://github.com/movie-web/movie-web/issues) for inspiration for contribution. Pull requests are always welcome. +A simple guide has been written to assist in hosting your own instance of movie-web. Check it out below -**All pull requests must be merged into the `dev` branch. it will then be deployed with the next version** +|[Selfhosting guide](https://docs.movie-web.app/self-hosting/self-hosting)| +|---| -## Credits + +# 🤝 Contributors This project would not be possible without our amazing contributors and the community. -GitHub contributors - -
-@JamesHawkinss for original concept. -
- -
-@JipFr for initial work on movie-cli. -
- -
-@mrjvs for leading the port to React, and for the beautiful design. -
- -
-@binaryoverload for help rewriting the application into React and making the README look ✨ pretty ✨. -
- -
-@lem6ns for helpfully implementing extra scrapers. -
+ + + + + + + + + + + + +
+
+ @JamesHawkinss +
+
+ @JipFr +
+
+ @mrjvs +
+
+ @binaryoverload +
+
+ @lem6ns +
diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 8951793d..78653fed 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1,53 +1,56 @@ { "auth": { "deviceNameLabel": "Device name", - "deviceNamePlaceholder": "Muad'Dib's Nintendo Switch", + "deviceNamePlaceholder": "Personal phone", "register": { "information": { "title": "Account information", - "color1": "First color", - "color2": "Second color", + "color1": "Profile color one", + "color2": "Profile color two", "icon": "User icon", - "header": "Enter a name for your device and choose a user icon and colours" + "header": "Enter a name for your device and pick colours and a user icon of your choosing", + "next": "Next" } }, "login": { "title": "Login to your account", - "description": "Oh, you're asking for the key to my top-secret lair, also known as The Fortress of Wordsmithery, accessed only by reciting the sacred incantation of the 12-word passphrase!", + "description": "Please enter your passphrase to login to your account", "validationError": "Invalid or incomplete passphrase", "submit": "Login", - "passphraseLabel": "12-Word Passphrase", + "passphraseLabel": "12-Word passphrase", "passphrasePlaceholder": "Passphrase" }, "generate": { "title": "Your passphrase", - "description": "If you lose this, you're a silly goose and will be posted on the wall of shame™️" + "next": "I have saved my passphrase", + "description": "Your passphase acts as your username and password. Make sure to keep it safe as you will need to enter it to login to your account" }, "trust": { - "title": "Do you trust this host?", - "host": "Do you trust <0>{{hostname}}?", + "title": "Do you trust this server?", + "host": "You are connecting to <0>{{hostname}} - please confirm you trust it before making an account", "failed": { - "title": "Failed to reach backend", + "title": "Failed to reach server", "text": "Did you configure it correctly?" }, - "yes": "Trust", + "yes": "I trust this server", "no": "Go back" }, "verify": { - "title": "Enter your passphrase", - "description": "If you've already lost it, how will you ever be able to take care of a child?", + "title": "Confirm your passphrase", + "description": "Please enter your passphrase from earlier to confirm you have saved it and to create your account", "invalidData": "Data is not valid", "noMatch": "Passphrase doesn't match", "recaptchaFailed": "ReCaptcha validation failed", - "passphraseLabel": "Your passphrase", - "register": "Register" + "passphraseLabel": "Your 12-word passphrase", + "register": "Create account" } }, "errors": { "details": "Error details", "reloadPage": "Reload the page", + "showError": "Show error details", "badge": "It broke", - "title": "That's an error boss" + "title": "We encountered an error!" }, "notFound": { "badge": "Not found", @@ -77,9 +80,10 @@ "scraping": { "notFound": { "badge": "Not found", - "title": "Goo goo gaa gaa", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", - "homeButton": "Go home" + "title": "We couldn't find that", + "text": "We have searched through our providers and cannot find the media you are looking for! We do not host the media and have no control over what is available. Please click 'Show details' below for more details.", + "homeButton": "Go home", + "detailsButton": "Show details" }, "items": { "pending": "Checking for videos...", @@ -88,16 +92,16 @@ } }, "playbackError": { - "badge": "Not found", - "title": "Whoops, it broke!", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", + "badge": "Playback error", + "title": "Failed to play video!", + "text": "There was an error trying to play the media. Please try again.", "homeButton": "Go home", "errors": { - "errorAborted": "The fetching of the associated resource was aborted by the user's request.", + "errorAborted": "The fetching of the media was aborted by the user's request.", "errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", "errorDecode": "Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.", - "errorNotSupported": "The associated resource or media provider object has been found to be unsuitable.", - "errorGenericMedia": "Unknown media error occured" + "errorNotSupported": "The media or media provider object is not supported.", + "errorGenericMedia": "Unknown media error occured." } }, "metadata": { @@ -109,8 +113,8 @@ }, "failed": { "badge": "Failed", - "title": "Failed to load meta data", - "text": "Oh, my apowogies, sweetie! The itty-bitty movie-web did its utmost bestest, but alas, no wucky videos to be spotted anywhere (´⊙ω⊙`) Please don't be angwy, wittle movie-web ish twying so hard. Can you find it in your heart to forgive? UwU 💖", + "title": "Failed to load metadata", + "text": "Could not load the media's metadata from TMDB. Please check whether TMDB is down or blocked on your internet connection.", "homeButton": "Go home" } }, @@ -131,8 +135,8 @@ "menus": { "settings": { "videoSection": "Video settings", - "experienceSection": "Viewing Experience", - "enableCaptions": "Enable Captions", + "experienceSection": "Viewing experience", + "enableCaptions": "Enable captions", "captionItem": "Caption settings", "sourceItem": "Video sources", "playbackItem": "Playback settings", @@ -156,11 +160,11 @@ }, "noEmbeds": { "title": "No embeds found", - "text": "We were unable to find any embeds for this source, please try another." + "text": "We were unable to find any embeds, please try a different source." }, "failed": { "title": "Failed to scrape", - "text": "We were unable to find any videos for this source. Don't come bitchin' to us about it, just try another source." + "text": "There was an error while trying to find any videos, please try a different source." } }, "captions": { @@ -170,28 +174,28 @@ "fixCapitals": "Fix capitalization", "delay": "Caption delay" }, - "customChoice": "Upload captions", + "customChoice": "Select caption from file", "offChoice": "Off", "unknownLanguage": "Unknown" }, "downloads": { "title": "Download", "disclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.", - "hlsExplanation": "Insert explanation for why you can't download HLS here", + "hlsExplanation": "This media is a HLS stream which cannot be downloaded on movie-web.", "downloadVideo": "Download video", "downloadCaption": "Download current caption", "onPc": { - "1": "On PC, right click the video and select Save video as", + "1": "On PC, click the download button then, on the new page, right click the video and select Save video as", "title": "Downloading on PC", "shortTitle": "Download / PC" }, "onAndroid": { - "1": "To download on Android, tap and hold on the video, then select save.", + "1": "To download on Android, click the download button then, on the new page, tap and hold on the video, then select save.", "title": "Downloading on Android", "shortTitle": "Download / Android" }, "onIos": { - "1": "To download on iOS, click , then Save to Files . All that's left to do now is to pick a nice and cozy folder for your video!", + "1": "To download on iOS, click the download button then, on the new page, click , then Save to Files .", "title": "Downloading on iOS", "shortTitle": "Download / iOS" } @@ -240,7 +244,7 @@ "loadingApp": "Loading application", "loadingUserError": { "text": "Failed to load your profile", - "textWithReset": "Failed to load your profile from your custom server, want to reset back to default?", + "textWithReset": "Failed to load your profile from your custom server, want to reset back to the default server?", "reset": "Reset custom server" }, "migration": { @@ -249,7 +253,7 @@ }, "dmca": { "title": "DMCA", - "text": "In an effort to address the copyright concerns associated with the website known as \"movie-web,\" the DMCA, or Digital Millennium Copyright Act, has been initiated to safeguard the intellectual property rights of content creators by reporting infringements on this platform, thereby adhering to legal protocols for takedown requests, which, like, you know, it's all about, like, maintaining the integrity of intellectual property, and, um, making sure, like, creators get their fair share, but then, it's, like, this intricate dance of digital legalities, where you have to, uh, like, navigate this labyrinth of code and bytes and, uh, send, you know, these, like, electronic documents that, um, point out the, uh, alleged infringement, and it's, like, this whole, like, teeter-totter of legality, where you're, like, balancing, um, the rights of the, you know, creators and the, um, operation of this, like, online, uh, entity, and, like, the DMCA, it's, like, this, um, powerful tool, but, uh, it's also, like, this, um, complex puzzle, where, you know, you're, like, seeking justice in the digital wilderness, and, uh, striving for harmony amidst the chaos of the internet, and, um, yeah, that's, like, the whole, like, DMCA-ing thing with movie-web, you know?" + "text": "Welcome to movie-web's DMCA contact page! We respect intellectual property rights and want to address any copyright concerns swiftly. If you believe your copyrighted work has been improperly used on our platform, please send a detailed DMCA notice to the email below. Please include a description of the copyrighted material, your contact details, and a statement of good faith belief. We're committed to resolving these matters promptly and appreciate your cooperation in keeping movie-web a place that respects creativity and copyrights." } }, "navigation": { @@ -266,8 +270,7 @@ }, "actions": { "copy": "Copy", - "copied": "Copied", - "next": "Next" + "copied": "Copied" }, "settings": { "unsaved": "You have unsaved changes", @@ -279,7 +282,7 @@ "hostname": "Hostname", "backendUrl": "Backend URL", "userId": "User ID", - "notLoggedIn": "Not logged in", + "notLoggedIn": "You are not logged in", "appVersion": "App version", "backendVersion": "Backend version", "unknownVersion": "Unknown", @@ -302,13 +305,13 @@ "title": "Account", "register": { "title": "Sync to the cloud", - "text": "Instantly share your watch progress between devices and keep them synced.", + "text": "Share your watch progress between devices and keep them synced.", "cta": "Get started" }, "profile": { "title": "Edit profile picture", - "firstColor": "First color", - "secondColor": "Second color", + "firstColor": "Profile color one", + "secondColor": "Profile color two", "userIcon": "User icon", "finish": "Finish editing" }, @@ -321,7 +324,7 @@ "accountDetails": { "editProfile": "Edit", "deviceNameLabel": "Device name", - "deviceNamePlaceholder": "Fremen tablet", + "deviceNamePlaceholder": "Personal phone", "logoutButton": "Log out" }, "actions": { @@ -360,7 +363,7 @@ }, "server": { "label": "Custom server", - "description": "To make the application function, all traffic is routed through proxies. Enable this if you want to bring your own workers.", + "description": "If you would like to connect to a custom backend to store your data, enable this and provide the URL.", "urlLabel": "Custom server URL" } } diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 0efd8e77..7a7b4882 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -81,20 +81,10 @@ export function UserAvatar(props: { ); } -export function NoUserAvatar(props: { - sizeClass?: string; - iconClass?: string; -}) { +export function NoUserAvatar(props: { iconClass?: string }) { return ( -
-
- -
+
+
); } diff --git a/src/components/LinksDropdown.tsx b/src/components/LinksDropdown.tsx index b6dfa3a6..fa4910ab 100644 --- a/src/components/LinksDropdown.tsx +++ b/src/components/LinksDropdown.tsx @@ -109,7 +109,7 @@ export function LinksDropdown(props: { children: React.ReactNode }) { return (
evt.key === "Enter" && toggleOpen()} diff --git a/src/components/buttons/Button.tsx b/src/components/buttons/Button.tsx index 4f36093f..0490189f 100644 --- a/src/components/buttons/Button.tsx +++ b/src/components/buttons/Button.tsx @@ -99,6 +99,8 @@ export function Button(props: Props) { ); } +// Sometimes you can't use normal button, due to not having access to a useHistory context +// When that happens, use this! interface ButtonPlainProps { onClick?: () => void; children?: ReactNode; diff --git a/src/components/form/Dropdown.tsx b/src/components/form/Dropdown.tsx index d3cb027b..c1e36c01 100644 --- a/src/components/form/Dropdown.tsx +++ b/src/components/form/Dropdown.tsx @@ -21,7 +21,7 @@ export function Dropdown(props: DropdownProps) { {() => ( <> - + {props.selectedItem.leftIcon ? props.selectedItem.leftIcon @@ -45,7 +45,7 @@ export function Dropdown(props: DropdownProps) { {props.options.map((opt) => ( - `flex gap-4 items-center relative cursor-default select-none py-3 pl-4 pr-4 ${ + `cursor-pointer flex gap-4 items-center relative select-none py-3 pl-4 pr-4 ${ active ? "bg-background-secondaryHover text-type-link" : "text-white" diff --git a/src/components/form/SearchBar.tsx b/src/components/form/SearchBar.tsx index 0573a285..717f964d 100644 --- a/src/components/form/SearchBar.tsx +++ b/src/components/form/SearchBar.tsx @@ -24,7 +24,7 @@ export const SearchBarInput = forwardRef( return ( ( "bg-search-focused": focused, })} /> -
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index da84527d..3704c446 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -55,7 +55,7 @@ export function Footer() {

{t("footer.legal.disclaimerText")}

-
+
{t("footer.links.github")} diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 8e56ffc1..d7cec107 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -59,6 +59,7 @@ export function Navigation(props: NavigationProps) {
) : null} +
-
-
+
+
@@ -99,7 +100,7 @@ export function Navigation(props: NavigationProps) {
-
+
{loggedIn ? : } diff --git a/src/components/player/atoms/AutoPlayStart.tsx b/src/components/player/atoms/AutoPlayStart.tsx index b5d02f29..7ae99e88 100644 --- a/src/components/player/atoms/AutoPlayStart.tsx +++ b/src/components/player/atoms/AutoPlayStart.tsx @@ -23,7 +23,7 @@ export function AutoPlayStart() { return (
s.display); const { isPaused } = usePlayerStore((s) => s.mediaPlaying); @@ -13,6 +13,7 @@ export function Pause(props: { iconSizeClass?: string }) { return ( setDelay(v)} @@ -241,7 +241,7 @@ export function CaptionSettingsView({ id }: { id: string }) { />
- {t("player.menus.captions.settings.delay")} + {t("player.menus.captions.settings.fixCapitals")}
diff --git a/src/pages/parts/auth/PassphraseGeneratePart.tsx b/src/pages/parts/auth/PassphraseGeneratePart.tsx index 43b4518c..bb9c6a9a 100644 --- a/src/pages/parts/auth/PassphraseGeneratePart.tsx +++ b/src/pages/parts/auth/PassphraseGeneratePart.tsx @@ -31,7 +31,7 @@ export function PassphraseGeneratePart(props: PassphraseGeneratePartProps) { diff --git a/src/pages/parts/errors/ErrorCard.tsx b/src/pages/parts/errors/ErrorCard.tsx index d3cf7738..3930326b 100644 --- a/src/pages/parts/errors/ErrorCard.tsx +++ b/src/pages/parts/errors/ErrorCard.tsx @@ -3,10 +3,13 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; +import { Modal } from "@/components/overlays/Modal"; import { DisplayError } from "@/components/player/display/displayInterface"; -export function ErrorCard(props: { error: DisplayError | string }) { - const [showErrorCard, setShowErrorCard] = useState(true); +export function ErrorCard(props: { + error: DisplayError | string; + onClose: () => void; +}) { const [hasCopied, setHasCopied] = useState(false); const hasCopiedUnsetDebounce = useRef | null>( null @@ -32,8 +35,6 @@ export function ErrorCard(props: { error: DisplayError | string }) { hasCopiedUnsetDebounce.current = setTimeout(() => setHasCopied(false), 2e3); } - if (!showErrorCard) return null; - return ( // I didn't put a here because it'd fade out, then jump height weirdly
@@ -60,7 +61,7 @@ export function ErrorCard(props: { error: DisplayError | string }) { @@ -72,3 +73,35 @@ export function ErrorCard(props: { error: DisplayError | string }) {
); } + +// use plain modal version if there is no access to history api (like in error boundary) +export function ErrorCardInPlainModal(props: { + error?: DisplayError | string; + onClose: () => void; + show?: boolean; +}) { + if (!props.show || !props.error) return null; + return ( +
+
+ +
+
+ ); +} + +export function ErrorCardInModal(props: { + error?: DisplayError | string; + id: string; + onClose: () => void; +}) { + if (!props.error) return null; + + return ( + +
+ +
+
+ ); +} diff --git a/src/pages/parts/errors/ErrorPart.tsx b/src/pages/parts/errors/ErrorPart.tsx index e61ec744..9d62b3dc 100644 --- a/src/pages/parts/errors/ErrorPart.tsx +++ b/src/pages/parts/errors/ErrorPart.tsx @@ -1,27 +1,24 @@ +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { ButtonPlain } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; -import { DisplayError } from "@/components/player/display/displayInterface"; import { Title } from "@/components/text/Title"; import { Paragraph } from "@/components/utils/Text"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; -import { ErrorCard } from "@/pages/parts/errors/ErrorCard"; +import { ErrorCardInPlainModal } from "@/pages/parts/errors/ErrorCard"; export function ErrorPart(props: { error: any; errorInfo: any }) { const { t } = useTranslation(); + const [showErrorCard, setShowErrorCard] = useState(false); const maxLineCount = 5; const errorLines = (props.errorInfo.componentStack || "") .split("\n") .slice(0, maxLineCount); - const error: DisplayError = { - errorName: "What does this do", - type: "global", - message: errorLines.join("\n"), - }; + const error = `${props.error.toString()}\n${errorLines.join("\n")}`; return (
@@ -30,15 +27,30 @@ export function ErrorPart(props: { error: any; errorInfo: any }) { {t("errors.badge")} {t("errors.title")} + {props.error.toString()} - - window.location.reload()} - > - {t("errors.reloadPage")} - + setShowErrorCard(false)} + error={error} + /> + +
+ window.location.reload()} + > + {t("errors.reloadPage")} + + setShowErrorCard(true)} + > + {t("errors.showError")} + +
diff --git a/src/pages/parts/player/PlaybackErrorPart.tsx b/src/pages/parts/player/PlaybackErrorPart.tsx index 03bf1e26..df6e6414 100644 --- a/src/pages/parts/player/PlaybackErrorPart.tsx +++ b/src/pages/parts/player/PlaybackErrorPart.tsx @@ -3,16 +3,18 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; +import { useModal } from "@/components/overlays/Modal"; import { Paragraph } from "@/components/text/Paragraph"; import { Title } from "@/components/text/Title"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; import { usePlayerStore } from "@/stores/player/store"; -import { ErrorCard } from "../errors/ErrorCard"; +import { ErrorCardInModal } from "../errors/ErrorCard"; export function PlaybackErrorPart() { const { t } = useTranslation(); const playbackError = usePlayerStore((s) => s.interface.error); + const modal = useModal("error"); return ( @@ -20,19 +22,31 @@ export function PlaybackErrorPart() { {t("player.playbackError.badge")} {t("player.playbackError.title")} {t("player.playbackError.text")} - - - - {/* Error */} - {playbackError ? : null} +
+ + +
+ {/* Error */} + modal.hide()} + error={playbackError} + id={modal.id} + />
); } diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 42d6dd00..bafd64fd 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -18,6 +18,7 @@ export function PlayerPart(props: PlayerPartProps) { const { showTargets, showTouchTargets } = useShouldShowControls(); const status = usePlayerStore((s) => s.status); const { isMobile } = useIsMobile(); + const isLoading = usePlayerStore((s) => s.mediaPlaying.isLoading); return ( @@ -38,10 +39,13 @@ export function PlayerPart(props: PlayerPartProps) { - + @@ -60,38 +64,54 @@ export function PlayerPart(props: PlayerPartProps) {
- - + {status === playerStatus.PLAYING ? ( + <> + + + + ) : null}
- {isMobile ? : null} - + {status === playerStatus.PLAYING ? ( + <> + {isMobile ? : null} + + + ) : null}
- - - - - + {status === playerStatus.PLAYING ? ( + <> + + + + + + + ) : null}
- - - - + {status === playerStatus.PLAYING ? ( + <> + + + + + + ) : null}
- + {status === playerStatus.PLAYING ? : null}
diff --git a/src/pages/parts/player/ScrapeErrorPart.tsx b/src/pages/parts/player/ScrapeErrorPart.tsx index 632f9280..41465717 100644 --- a/src/pages/parts/player/ScrapeErrorPart.tsx +++ b/src/pages/parts/player/ScrapeErrorPart.tsx @@ -4,12 +4,13 @@ import { useTranslation } from "react-i18next"; import { Button } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; +import { useModal } from "@/components/overlays/Modal"; import { Paragraph } from "@/components/text/Paragraph"; import { Title } from "@/components/text/Title"; import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; -import { ErrorCard } from "../errors/ErrorCard"; +import { ErrorCardInModal } from "../errors/ErrorCard"; export interface ScrapeErrorPartProps { data: { @@ -20,6 +21,8 @@ export interface ScrapeErrorPartProps { export function ScrapeErrorPart(props: ScrapeErrorPartProps) { const { t } = useTranslation(); + const modal = useModal("error"); + const error = useMemo(() => { const data = props.data; const amountError = Object.values(data.sources).filter( @@ -43,19 +46,32 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) { {t("player.scraping.notFound.title")} {t("player.scraping.notFound.text")} - - - - {/* Error */} - {error ? : null} +
+ + +
+ {error ? ( + modal.hide()} + error={error} + /> + ) : null} ); } diff --git a/src/pages/parts/settings/CaptionsPart.tsx b/src/pages/parts/settings/CaptionsPart.tsx index 0f37a7da..8327e343 100644 --- a/src/pages/parts/settings/CaptionsPart.tsx +++ b/src/pages/parts/settings/CaptionsPart.tsx @@ -1,5 +1,6 @@ import classNames from "classnames"; import { useState } from "react"; +import { Helmet } from "react-helmet-async"; import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; @@ -29,6 +30,11 @@ export function CaptionPreview(props: { "fixed inset-0 z-[60]": props.fullscreen, })} > + {props.fullscreen && props.show ? ( + + + + ) : null}

- {t("settings.connections.workers.label")} + {t("settings.connections.server.label")}

- {t("settings.connections.workers.description")} + {t("settings.connections.server.description")}

@@ -132,7 +132,7 @@ function BackendEdit({ backendUrl, setBackendUrl }: BackendEditProps) { <>

- {t("settings.connections.workers.urlLabel")} + {t("settings.connections.server.urlLabel")}

diff --git a/themes/default.ts b/themes/default.ts index 5e839998..f0dd5113 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -9,8 +9,8 @@ export const defaultTheme = { // Branding pill: { - background: "#1C1C36", - backgroundHover: "#1C1C36", + background: "#2e2e4d", + backgroundHover: "#3d3d61", highlight: "#714C97", }, @@ -97,7 +97,7 @@ export const defaultTheme = { dropdown: { background: "#171728", altBackground: "#151525", - highlight: "#FCEC61", + highlight: "#afa349", highlightHover: "#FCEC61", text: "#846D95", secondary: "#73739D", @@ -179,6 +179,11 @@ export const defaultTheme = { video: { buttonBackground: "#444B5C", + autoPlay: { + background: "#161C26", + hover: "#252533" + }, + scraping: { card: "#161620", error: "#E44F4F", @@ -211,6 +216,7 @@ export const defaultTheme = { active: "#0D1317", }, + type: { main: "#617A8A", secondary: "#374A56",