diff --git a/index.html b/index.html index 2fc088df..51cba30f 100644 --- a/index.html +++ b/index.html @@ -134,7 +134,8 @@ - + + diff --git a/src/assets/css/index.css b/src/assets/css/index.css index 08b24f68..5b42a899 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -4,9 +4,10 @@ html, body { - @apply bg-background-main font-open-sans text-type-text; + @apply bg-background-main font-main text-type-text; min-height: 100vh; min-height: 100dvh; + font-size: 1.0235em; } html[data-full], diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 5786e446..72ee1fbf 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -355,7 +355,7 @@ "unknownOption": "Unknown" }, "subtitles": { - "customChoice": "Select subtitle from file", + "customChoice": "Drop or upload file", "customizeLabel": "Customize", "offChoice": "Off", "settings": { @@ -364,7 +364,8 @@ "fixCapitals": "Fix capitalization" }, "title": "Subtitles", - "unknownLanguage": "Unknown" + "unknownLanguage": "Unknown", + "dropSubtitleFile": "Drop subtitle file here" } }, "metadata": { @@ -423,11 +424,17 @@ "notFound": { "badge": "Not found", "detailsButton": "Show details", - "reloadButton": "Try again", "homeButton": "Go home", - "text": "We can not find the media you are looking for or no one provides it... Did you enable the extension for this site?", - "text2": "We can not find the media you are looking for or no one provides it...", + "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.", "title": "We couldn't find that" + }, + "extensionFailure": { + "badge": "Not found", + "homeButton": "Go home", + "enableExtension": "Enable extension", + "disabledTitle": "Extension disabled", + "text": "You've installed the movie-web extension. To start using it, complete a few preliminary steps. Have you enabled the extension for this site?", + "title": "Extension Disabled" } }, "time": { diff --git a/src/components/DropFile.tsx b/src/components/DropFile.tsx new file mode 100644 index 00000000..8b0ab84e --- /dev/null +++ b/src/components/DropFile.tsx @@ -0,0 +1,51 @@ +import { useEffect, useState } from "react"; +import type { DragEvent, ReactNode } from "react"; + +interface FileDropHandlerProps { + children: ReactNode; + className: string; + onDrop: (event: DragEvent) => void; + onDraggingChange: (isDragging: boolean) => void; +} + +export function FileDropHandler(props: FileDropHandlerProps) { + const [dragging, setDragging] = useState(false); + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + setDragging(true); + }; + + const handleDragLeave = (event: DragEvent) => { + if (!event.currentTarget.contains(event.relatedTarget as Node)) { + setDragging(false); + } + }; + + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + }; + + const handleDrop = (event: DragEvent) => { + event.preventDefault(); + setDragging(false); + + props.onDrop(event); + }; + + useEffect(() => { + props.onDraggingChange(dragging); + }, [dragging, props]); + + return ( + + {props.children} + + ); +} diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 814831a1..1f9d0935 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -65,6 +65,7 @@ export enum Icons { DONATION = "donation", CIRCLE_QUESTION = "circle_question", BRUSH = "brush", + UPLOAD = "upload", } export interface IconProps { @@ -136,6 +137,7 @@ const iconList: Record = { donation: ``, circle_question: ``, brush: ``, + upload: ``, }; function ChromeCastButton() { diff --git a/src/components/layout/BrandPill.tsx b/src/components/layout/BrandPill.tsx index 5d673bde..46d11ccf 100644 --- a/src/components/layout/BrandPill.tsx +++ b/src/components/layout/BrandPill.tsx @@ -2,14 +2,15 @@ import classNames from "classnames"; import { useTranslation } from "react-i18next"; import { Icon, Icons } from "@/components/Icon"; +import { useIsMobile } from "@/hooks/useIsMobile"; export function BrandPill(props: { clickable?: boolean; - hideTextOnMobile?: boolean; header?: boolean; backgroundClass?: string; }) { const { t } = useTranslation(); + const isMobile = useIsMobile(); return ( - + {t("global.name")} diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 8524ecc8..035567e2 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -1,11 +1,14 @@ +import classNames from "classnames"; import Fuse from "fuse.js"; -import { useMemo, useRef, useState } from "react"; +import { type DragEvent, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useAsyncFn } from "react-use"; import { convert } from "subsrt-ts"; import { subtitleTypeList } from "@/backend/helpers/subs"; +import { FileDropHandler } from "@/components/DropFile"; import { FlagIcon } from "@/components/FlagIcon"; +import { Icon, Icons } from "@/components/Icon"; import { useCaptions } from "@/components/player/hooks/useCaptions"; import { Menu } from "@/components/player/internals/ContextMenu"; import { Input } from "@/components/player/internals/ContextMenu/Input"; @@ -123,6 +126,34 @@ export function CaptionsView({ id }: { id: string }) { const { selectCaptionById, disable } = useCaptions(); const captionList = usePlayerStore((s) => s.captionList); const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); + const [dragging, setDragging] = useState(false); + const setCaption = usePlayerStore((s) => s.setCaption); + + function onDrop(event: DragEvent) { + const files = event.dataTransfer.files; + const firstFile = files[0]; + if (!files || !firstFile) return; + + const fileExtension = `.${firstFile.name.split(".").pop()}`; + if (!fileExtension || !subtitleTypeList.includes(fileExtension)) { + return; + } + + const reader = new FileReader(); + reader.addEventListener("load", (e) => { + if (!e.target || typeof e.target.result !== "string") return; + + const converted = convert(e.target.result, "srt"); + + setCaption({ + language: "custom", + srtData: converted, + id: "custom-caption", + }); + }); + + reader.readAsText(firstFile); + } const captions = useMemo( () => @@ -164,6 +195,20 @@ export function CaptionsView({ id }: { id: string }) { return ( <> + + + + + {t("player.menus.subtitles.dropSubtitleFile")} + + + + router.navigate("/")} rightSide={ @@ -178,17 +223,28 @@ export function CaptionsView({ id }: { id: string }) { > {t("player.menus.subtitles.title")} + + { + setDragging(isDragging); + }} + onDrop={(event) => onDrop(event)} + > - - - disable()} selected={!selectedCaptionId}> - {t("player.menus.subtitles.offChoice")} - - - {content} - + + disable()} + selected={!selectedCaptionId} + > + {t("player.menus.subtitles.offChoice")} + + + {content} + + > ); } diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index db351dda..ef05cbfc 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -2,8 +2,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useAsyncFn, useInterval } from "react-use"; -import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; -import { extensionInfo, sendPage } from "@/backend/extension/messaging"; +import { sendPage } from "@/backend/extension/messaging"; import { Button } from "@/components/buttons/Button"; import { Icon, Icons } from "@/components/Icon"; import { Loading } from "@/components/layout/Loading"; @@ -22,24 +21,8 @@ import { ExtensionDetectionResult, detectExtensionInstall, } from "@/utils/detectFeatures"; - -type ExtensionStatus = - | "unknown" - | "failed" - | "disallowed" - | "noperms" - | "outdated" - | "success"; - -async function getExtensionState(): Promise { - const info = await extensionInfo(); - if (!info) return "unknown"; // cant talk to extension - if (!info.success) return "failed"; // extension failed to respond - if (!info.allowed) return "disallowed"; // extension is not enabled on this page - if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks - if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old - return "success"; // no problems -} +import { getExtensionState } from "@/utils/onboarding"; +import type { ExtensionStatus } from "@/utils/onboarding"; function RefreshBar() { const { t } = useTranslation(); diff --git a/src/pages/parts/admin/WorkerTestPart.tsx b/src/pages/parts/admin/WorkerTestPart.tsx index 9fea1fbe..033640ad 100644 --- a/src/pages/parts/admin/WorkerTestPart.tsx +++ b/src/pages/parts/admin/WorkerTestPart.tsx @@ -88,10 +88,12 @@ export function WorkerTestPart() { status: "success", }); } catch (err) { + const error = err as Error; + error.message = error.message.replace(worker.url, "WORKER_URL"); updateWorker(worker.id, { id: worker.id, status: "error", - error: err as Error, + error, }); } }); diff --git a/src/pages/parts/player/ScrapeErrorPart.tsx b/src/pages/parts/player/ScrapeErrorPart.tsx index 532dc630..5de7e650 100644 --- a/src/pages/parts/player/ScrapeErrorPart.tsx +++ b/src/pages/parts/player/ScrapeErrorPart.tsx @@ -2,8 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; -import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; -import { extensionInfo } from "@/backend/extension/messaging"; +import { sendPage } from "@/backend/extension/messaging"; import { Button } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; @@ -12,18 +11,11 @@ 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 { ExtensionStatus, getExtensionState } from "@/utils/onboarding"; import { getProviderApiUrls } from "@/utils/proxyUrls"; import { ErrorCardInModal } from "../errors/ErrorCard"; -type ExtensionStatus = - | "unknown" - | "failed" - | "disallowed" - | "noperms" - | "outdated" - | "success"; - export interface ScrapeErrorPartProps { data: { sources: Record; @@ -31,22 +23,14 @@ export interface ScrapeErrorPartProps { }; } -async function getExtensionState(): Promise { - const info = await extensionInfo(); - if (!info) return "unknown"; // cant talk to extension - if (!info.success) return "failed"; // extension failed to respond - if (!info.allowed) return "disallowed"; // extension is not enabled on this page - if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks - if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old - return "success"; // no problems -} - export function ScrapeErrorPart(props: ScrapeErrorPartProps) { const { t } = useTranslation(); const modal = useModal("error"); const location = useLocation(); const [extensionState, setExtensionState] = useState("unknown"); + const [title, setTitle] = useState(t("player.scraping.notFound.title")); + const [icon, setIcon] = useState(Icons.WAND); const error = useMemo(() => { const data = props.data; @@ -65,46 +49,80 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) { }, [props, location]); useEffect(() => { - getExtensionState().then(setExtensionState); - }, []); + getExtensionState().then((state: ExtensionStatus) => { + setExtensionState(state); + if (state === "disallowed") { + setTitle(t("player.scraping.extensionFailure.disabledTitle")); + setIcon(Icons.LOCK); + } + }); + }, [t]); + + if (extensionState === "disallowed") { + return ( + + + {t("player.scraping.notFound.badge")} + {title} + + + ), + }} + /> + + + + {t("player.scraping.notFound.homeButton")} + + { + sendPage({ + page: "PermissionGrant", + redirectUrl: window.location.href, + }); + }} + theme="purple" + padding="md:px-12 p-2.5" + className="mt-6" + > + {t("player.scraping.extensionFailure.enableExtension")} + + + + {error ? ( + modal.hide()} + error={error} + /> + ) : null} + + ); + } return ( - - {t("player.scraping.notFound.badge")} - - {t("player.scraping.notFound.title")} + {t("player.scraping.notFound.badge")} + {title} - {extensionState === "disallowed" ? ( - - ), - }} - /> - ) : ( - - ), - }} - /> - )} + , + }} + /> - window.location.reload()} - theme="secondary" - padding="md:px-12 p-2.5" - className="mt-6" - > - {t("player.scraping.notFound.reloadButton")} - {t("player.scraping.notFound.homeButton")} + { + sendPage({ + page: "PermissionGrant", + redirectUrl: window.location.href, + }); + }} + theme="purple" + padding="md:px-12 p-2.5" + className="mt-6" + > + {t("player.scraping.notFound.detailsButton")} + - modal.show()} - theme="purple" - padding="md:px-12 p-2.5" - className="mt-6" - > - {t("player.scraping.notFound.detailsButton")} - {error ? ( { - const info = await extensionInfo(); - if (!info) return "unknown"; // cant talk to extension - if (!info.success) return "failed"; // extension failed to respond - if (!info.allowed) return "disallowed"; // extension is not enabled on this page - if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks - if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old - return "success"; // no problems -} +import { ExtensionStatus, getExtensionState } from "@/utils/onboarding"; export function Layout(props: { children: ReactNode }) { const bannerSize = useBannerSize(); const location = useBannerStore((s) => s.location); const [extensionState, setExtensionState] = useState("unknown"); - const [isMobile, setIsMobile] = useState(false); + const { isMobile } = useIsMobile(); useEffect(() => { let isMounted = true; @@ -39,19 +21,8 @@ export function Layout(props: { children: ReactNode }) { } }); - // Instead use isMobile like this `const { isMobile } = useIsMobile();` - const mediaQuery = window.matchMedia("(max-width: 768px)"); // Adjust the max-width as per your needs - setIsMobile(mediaQuery.matches); - - const handleResize = () => { - setIsMobile(mediaQuery.matches); - }; - - mediaQuery.addListener(handleResize); - return () => { isMounted = false; - mediaQuery.removeListener(handleResize); }; }, []); diff --git a/src/stores/banner/BannerLocation.tsx b/src/stores/banner/BannerLocation.tsx index e4549ca8..0c784dba 100644 --- a/src/stores/banner/BannerLocation.tsx +++ b/src/stores/banner/BannerLocation.tsx @@ -3,8 +3,8 @@ import { Trans, useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { Icon, Icons } from "@/components/Icon"; -import { ExtensionStatus } from "@/setup/Layout"; import { useBannerStore, useRegisterBanner } from "@/stores/banner"; +import type { ExtensionStatus } from "@/utils/onboarding"; export function Banner(props: { children: React.ReactNode; diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts index c2678b1c..d4ee10fd 100644 --- a/src/utils/onboarding.ts +++ b/src/utils/onboarding.ts @@ -1,8 +1,30 @@ -import { isExtensionActive } from "@/backend/extension/messaging"; +import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; +import { + extensionInfo, + isExtensionActive, +} from "@/backend/extension/messaging"; import { conf } from "@/setup/config"; import { useAuthStore } from "@/stores/auth"; import { useOnboardingStore } from "@/stores/onboarding"; +export type ExtensionStatus = + | "unknown" + | "failed" + | "disallowed" + | "noperms" + | "outdated" + | "success"; + +export async function getExtensionState(): Promise { + const info = await extensionInfo(); + if (!info) return "unknown"; // cant talk to extension + if (!info.success) return "failed"; // extension failed to respond + if (!info.allowed) return "disallowed"; // extension is not enabled on this page + if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks + if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old + return "success"; // no problems +} + export async function needsOnboarding(): Promise { // if onboarding is dislabed, no onboarding needed if (!conf().HAS_ONBOARDING) return false; diff --git a/tailwind.config.ts b/tailwind.config.ts index 913a59fd..9448a59f 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -16,7 +16,7 @@ const config: Config = { /* fonts */ fontFamily: { - "open-sans": "'Open Sans'", + "main": "'DM Sans'", }, /* animations */