1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

Merge pull request #1 from sussy-code/dev

Merge dev to main
This commit is contained in:
Captain Jack Sparrow 2024-03-26 21:55:19 -04:00 committed by GitHub
commit 66b1714371
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 257 additions and 137 deletions

View file

@ -134,7 +134,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
<script src="/config.js"></script> <script src="/config.js"></script>

View file

@ -4,9 +4,10 @@
html, html,
body { 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: 100vh;
min-height: 100dvh; min-height: 100dvh;
font-size: 1.0235em;
} }
html[data-full], html[data-full],

View file

@ -355,7 +355,7 @@
"unknownOption": "Unknown" "unknownOption": "Unknown"
}, },
"subtitles": { "subtitles": {
"customChoice": "Select subtitle from file", "customChoice": "Drop or upload file",
"customizeLabel": "Customize", "customizeLabel": "Customize",
"offChoice": "Off", "offChoice": "Off",
"settings": { "settings": {
@ -364,7 +364,8 @@
"fixCapitals": "Fix capitalization" "fixCapitals": "Fix capitalization"
}, },
"title": "Subtitles", "title": "Subtitles",
"unknownLanguage": "Unknown" "unknownLanguage": "Unknown",
"dropSubtitleFile": "Drop subtitle file here"
} }
}, },
"metadata": { "metadata": {
@ -423,11 +424,17 @@
"notFound": { "notFound": {
"badge": "Not found", "badge": "Not found",
"detailsButton": "Show details", "detailsButton": "Show details",
"reloadButton": "Try again",
"homeButton": "Go home", "homeButton": "Go home",
"text": "We can not find the media you are looking for or no one provides it... <bold>Did you enable the extension for this site?</bold>", "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.",
"text2": "We can not find the media you are looking for or no one provides it...",
"title": "We couldn't find that" "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": { "time": {

View file

@ -0,0 +1,51 @@
import { useEffect, useState } from "react";
import type { DragEvent, ReactNode } from "react";
interface FileDropHandlerProps {
children: ReactNode;
className: string;
onDrop: (event: DragEvent<HTMLDivElement>) => void;
onDraggingChange: (isDragging: boolean) => void;
}
export function FileDropHandler(props: FileDropHandlerProps) {
const [dragging, setDragging] = useState(false);
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setDragging(true);
};
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
setDragging(false);
}
};
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();
setDragging(false);
props.onDrop(event);
};
useEffect(() => {
props.onDraggingChange(dragging);
}, [dragging, props]);
return (
<div
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
className={props.className}
>
{props.children}
</div>
);
}

View file

@ -65,6 +65,7 @@ export enum Icons {
DONATION = "donation", DONATION = "donation",
CIRCLE_QUESTION = "circle_question", CIRCLE_QUESTION = "circle_question",
BRUSH = "brush", BRUSH = "brush",
UPLOAD = "upload",
} }
export interface IconProps { export interface IconProps {
@ -136,6 +137,7 @@ const iconList: Record<Icons, string> = {
donation: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M163.9 136.9c-29.4-29.8-29.4-78.2 0-108s77-29.8 106.4 0l17.7 18 17.7-18c29.4-29.8 77-29.8 106.4 0s29.4 78.2 0 108L310.5 240.1c-6.2 6.3-14.3 9.4-22.5 9.4s-16.3-3.1-22.5-9.4L163.9 136.9zM568.2 336.3c13.1 17.8 9.3 42.8-8.5 55.9L433.1 485.5c-23.4 17.2-51.6 26.5-80.7 26.5H192 32c-17.7 0-32-14.3-32-32V416c0-17.7 14.3-32 32-32H68.8l44.9-36c22.7-18.2 50.9-28 80-28H272h16 64c17.7 0 32 14.3 32 32s-14.3 32-32 32H288 272c-8.8 0-16 7.2-16 16s7.2 16 16 16H392.6l119.7-88.2c17.8-13.1 42.8-9.3 55.9 8.5zM193.6 384l0 0-.9 0c.3 0 .6 0 .9 0z"/></svg>`, donation: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M163.9 136.9c-29.4-29.8-29.4-78.2 0-108s77-29.8 106.4 0l17.7 18 17.7-18c29.4-29.8 77-29.8 106.4 0s29.4 78.2 0 108L310.5 240.1c-6.2 6.3-14.3 9.4-22.5 9.4s-16.3-3.1-22.5-9.4L163.9 136.9zM568.2 336.3c13.1 17.8 9.3 42.8-8.5 55.9L433.1 485.5c-23.4 17.2-51.6 26.5-80.7 26.5H192 32c-17.7 0-32-14.3-32-32V416c0-17.7 14.3-32 32-32H68.8l44.9-36c22.7-18.2 50.9-28 80-28H272h16 64c17.7 0 32 14.3 32 32s-14.3 32-32 32H288 272c-8.8 0-16 7.2-16 16s7.2 16 16 16H392.6l119.7-88.2c17.8-13.1 42.8-9.3 55.9 8.5zM193.6 384l0 0-.9 0c.3 0 .6 0 .9 0z"/></svg>`,
circle_question: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`, circle_question: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
brush: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M162.4 6c-1.5-3.6-5-6-8.9-6h-19c-3.9 0-7.5 2.4-8.9 6L104.9 57.7c-3.2 8-14.6 8-17.8 0L66.4 6c-1.5-3.6-5-6-8.9-6H48C21.5 0 0 21.5 0 48V224v22.4V256H9.6 374.4 384v-9.6V224 48c0-26.5-21.5-48-48-48H230.5c-3.9 0-7.5 2.4-8.9 6L200.9 57.7c-3.2 8-14.6 8-17.8 0L162.4 6zM0 288v32c0 35.3 28.7 64 64 64h64v64c0 35.3 28.7 64 64 64s64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V288H0zM192 432a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" fill="currentColor"/></svg>`, brush: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M162.4 6c-1.5-3.6-5-6-8.9-6h-19c-3.9 0-7.5 2.4-8.9 6L104.9 57.7c-3.2 8-14.6 8-17.8 0L66.4 6c-1.5-3.6-5-6-8.9-6H48C21.5 0 0 21.5 0 48V224v22.4V256H9.6 374.4 384v-9.6V224 48c0-26.5-21.5-48-48-48H230.5c-3.9 0-7.5 2.4-8.9 6L200.9 57.7c-3.2 8-14.6 8-17.8 0L162.4 6zM0 288v32c0 35.3 28.7 64 64 64h64v64c0 35.3 28.7 64 64 64s64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V288H0zM192 432a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" fill="currentColor"/></svg>`,
upload: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path opacity="1" fill="currentColor" d="M320 480H64c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32H192V144c0 26.5 21.5 48 48 48H352V448c0 17.7-14.3 32-32 32zM240 160c-8.8 0-16-7.2-16-16V32.5c2.8 .7 5.4 2.1 7.4 4.2L347.3 152.6c2.1 2.1 3.5 4.6 4.2 7.4H240zM64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V163.9c0-12.7-5.1-24.9-14.1-33.9L254.1 14.1c-9-9-21.2-14.1-33.9-14.1H64zM208 278.6l52.7 52.7c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6l-80-80c-6.2-6.2-16.4-6.2-22.6 0l-80 80c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L176 278.6V400c0 8.8 7.2 16 16 16s16-7.2 16-16V278.6z"/></svg>`,
}; };
function ChromeCastButton() { function ChromeCastButton() {

View file

@ -2,13 +2,14 @@ import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { useIsMobile } from "@/hooks/useIsMobile";
export function BrandPill(props: { export function BrandPill(props: {
clickable?: boolean; clickable?: boolean;
hideTextOnMobile?: boolean;
backgroundClass?: string; backgroundClass?: string;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const isMobile = useIsMobile();
return ( return (
<div <div
@ -20,11 +21,11 @@ export function BrandPill(props: {
: "", : "",
)} )}
> >
<Icon className="text-xl" icon={Icons.MOVIE_WEB} /> <Icon className="text-2xl" icon={Icons.MOVIE_WEB} />
<span <span
className={[ className={[
"font-semibold text-white", "font-semibold text-white",
props.hideTextOnMobile ? "hidden sm:block" : "", isMobile ? "hidden sm:block" : "",
].join(" ")} ].join(" ")}
> >
{t("global.name")} {t("global.name")}

View file

@ -1,5 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import React, { useCallback, useEffect } from "react"; import React, { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";

View file

@ -1,11 +1,14 @@
import classNames from "classnames";
import Fuse from "fuse.js"; 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 { useTranslation } from "react-i18next";
import { useAsyncFn } from "react-use"; import { useAsyncFn } from "react-use";
import { convert } from "subsrt-ts"; import { convert } from "subsrt-ts";
import { subtitleTypeList } from "@/backend/helpers/subs"; import { subtitleTypeList } from "@/backend/helpers/subs";
import { FileDropHandler } from "@/components/DropFile";
import { FlagIcon } from "@/components/FlagIcon"; import { FlagIcon } from "@/components/FlagIcon";
import { Icon, Icons } from "@/components/Icon";
import { useCaptions } from "@/components/player/hooks/useCaptions"; import { useCaptions } from "@/components/player/hooks/useCaptions";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
import { Input } from "@/components/player/internals/ContextMenu/Input"; import { Input } from "@/components/player/internals/ContextMenu/Input";
@ -123,6 +126,34 @@ export function CaptionsView({ id }: { id: string }) {
const { selectCaptionById, disable } = useCaptions(); const { selectCaptionById, disable } = useCaptions();
const captionList = usePlayerStore((s) => s.captionList); const captionList = usePlayerStore((s) => s.captionList);
const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList); const getHlsCaptionList = usePlayerStore((s) => s.display?.getCaptionList);
const [dragging, setDragging] = useState(false);
const setCaption = usePlayerStore((s) => s.setCaption);
function onDrop(event: DragEvent<HTMLDivElement>) {
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( const captions = useMemo(
() => () =>
@ -164,6 +195,20 @@ export function CaptionsView({ id }: { id: string }) {
return ( return (
<> <>
<div> <div>
<div
className={classNames(
"absolute inset-0 flex items-center justify-center text-white z-10 pointer-events-none transition-opacity duration-300",
dragging ? "opacity-100" : "opacity-0",
)}
>
<div className="flex flex-col items-center">
<Icon className="text-5xl mb-4" icon={Icons.UPLOAD} />
<span className="text-xl weight font-medium">
{t("player.menus.subtitles.dropSubtitleFile")}
</span>
</div>
</div>
<Menu.BackLink <Menu.BackLink
onClick={() => router.navigate("/")} onClick={() => router.navigate("/")}
rightSide={ rightSide={
@ -178,17 +223,28 @@ export function CaptionsView({ id }: { id: string }) {
> >
{t("player.menus.subtitles.title")} {t("player.menus.subtitles.title")}
</Menu.BackLink> </Menu.BackLink>
</div>
<FileDropHandler
className={`transition duration-300 ${dragging ? "opacity-20" : ""}`}
onDraggingChange={(isDragging) => {
setDragging(isDragging);
}}
onDrop={(event) => onDrop(event)}
>
<div className="mt-3"> <div className="mt-3">
<Input value={searchQuery} onInput={setSearchQuery} /> <Input value={searchQuery} onInput={setSearchQuery} />
</div> </div>
</div>
<Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3"> <Menu.ScrollToActiveSection className="!pt-1 mt-2 pb-3">
<CaptionOption onClick={() => disable()} selected={!selectedCaptionId}> <CaptionOption
onClick={() => disable()}
selected={!selectedCaptionId}
>
{t("player.menus.subtitles.offChoice")} {t("player.menus.subtitles.offChoice")}
</CaptionOption> </CaptionOption>
<CustomCaptionOption /> <CustomCaptionOption />
{content} {content}
</Menu.ScrollToActiveSection> </Menu.ScrollToActiveSection>
</FileDropHandler>
</> </>
); );
} }

View file

@ -2,8 +2,7 @@ import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useAsyncFn, useInterval } from "react-use"; import { useAsyncFn, useInterval } from "react-use";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; import { sendPage } from "@/backend/extension/messaging";
import { extensionInfo, sendPage } from "@/backend/extension/messaging";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading"; import { Loading } from "@/components/layout/Loading";
@ -22,24 +21,8 @@ import {
ExtensionDetectionResult, ExtensionDetectionResult,
detectExtensionInstall, detectExtensionInstall,
} from "@/utils/detectFeatures"; } from "@/utils/detectFeatures";
import { getExtensionState } from "@/utils/onboarding";
type ExtensionStatus = import type { ExtensionStatus } from "@/utils/onboarding";
| "unknown"
| "failed"
| "disallowed"
| "noperms"
| "outdated"
| "success";
async function getExtensionState(): Promise<ExtensionStatus> {
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
}
function RefreshBar() { function RefreshBar() {
const { t } = useTranslation(); const { t } = useTranslation();

View file

@ -88,10 +88,12 @@ export function WorkerTestPart() {
status: "success", status: "success",
}); });
} catch (err) { } catch (err) {
const error = err as Error;
error.message = error.message.replace(worker.url, "WORKER_URL");
updateWorker(worker.id, { updateWorker(worker.id, {
id: worker.id, id: worker.id,
status: "error", status: "error",
error: err as Error, error,
}); });
} }
}); });

View file

@ -2,8 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; import { sendPage } from "@/backend/extension/messaging";
import { extensionInfo } from "@/backend/extension/messaging";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
import { Icons } from "@/components/Icon"; import { Icons } from "@/components/Icon";
import { IconPill } from "@/components/layout/IconPill"; import { IconPill } from "@/components/layout/IconPill";
@ -12,18 +11,11 @@ import { Paragraph } from "@/components/text/Paragraph";
import { Title } from "@/components/text/Title"; import { Title } from "@/components/text/Title";
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape"; import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout";
import { ExtensionStatus, getExtensionState } from "@/utils/onboarding";
import { getProviderApiUrls } from "@/utils/proxyUrls"; import { getProviderApiUrls } from "@/utils/proxyUrls";
import { ErrorCardInModal } from "../errors/ErrorCard"; import { ErrorCardInModal } from "../errors/ErrorCard";
type ExtensionStatus =
| "unknown"
| "failed"
| "disallowed"
| "noperms"
| "outdated"
| "success";
export interface ScrapeErrorPartProps { export interface ScrapeErrorPartProps {
data: { data: {
sources: Record<string, ScrapingSegment>; sources: Record<string, ScrapingSegment>;
@ -31,22 +23,14 @@ export interface ScrapeErrorPartProps {
}; };
} }
async function getExtensionState(): Promise<ExtensionStatus> {
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) { export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const modal = useModal("error"); const modal = useModal("error");
const location = useLocation(); const location = useLocation();
const [extensionState, setExtensionState] = const [extensionState, setExtensionState] =
useState<ExtensionStatus>("unknown"); useState<ExtensionStatus>("unknown");
const [title, setTitle] = useState(t("player.scraping.notFound.title"));
const [icon, setIcon] = useState(Icons.WAND);
const error = useMemo(() => { const error = useMemo(() => {
const data = props.data; const data = props.data;
@ -65,46 +49,32 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
}, [props, location]); }, [props, location]);
useEffect(() => { 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 ( return (
<ErrorLayout> <ErrorLayout>
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}> <IconPill icon={icon}>{t("player.scraping.notFound.badge")}</IconPill>
{t("player.scraping.notFound.badge")} <Title>{title}</Title>
</IconPill>
<Title>{t("player.scraping.notFound.title")}</Title>
<Paragraph> <Paragraph>
{extensionState === "disallowed" ? (
<Trans <Trans
i18nKey="player.scraping.notFound.text" i18nKey="player.scraping.extensionFailure.text"
components={{ components={{
bold: ( bold: (
<span className="font-bold" style={{ color: "#cfcfcf" }} /> <span className="font-bold" style={{ color: "#cfcfcf" }} />
), ),
}} }}
/> />
) : (
<Trans
i18nKey="player.scraping.notFound.text2"
components={{
bold: (
<span className="font-bold" style={{ color: "#cfcfcf" }} />
),
}}
/>
)}
</Paragraph> </Paragraph>
<div className="flex gap-3"> <div className="flex gap-3">
<Button
onClick={() => window.location.reload()}
theme="secondary"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("player.scraping.notFound.reloadButton")}
</Button>
<Button <Button
href="/" href="/"
theme="secondary" theme="secondary"
@ -113,15 +83,68 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
> >
{t("player.scraping.notFound.homeButton")} {t("player.scraping.notFound.homeButton")}
</Button> </Button>
</div>
<Button <Button
onClick={() => modal.show()} onClick={() => {
sendPage({
page: "PermissionGrant",
redirectUrl: window.location.href,
});
}}
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("player.scraping.extensionFailure.enableExtension")}
</Button>
</div>
</ErrorContainer>
{error ? (
<ErrorCardInModal
id={modal.id}
onClose={() => modal.hide()}
error={error}
/>
) : null}
</ErrorLayout>
);
}
return (
<ErrorLayout>
<ErrorContainer>
<IconPill icon={icon}>{t("player.scraping.notFound.badge")}</IconPill>
<Title>{title}</Title>
<Paragraph>
<Trans
i18nKey="player.scraping.notFound.text"
components={{
bold: <span className="font-bold" style={{ color: "#cfcfcf" }} />,
}}
/>
</Paragraph>
<div className="flex gap-3">
<Button
href="/"
theme="secondary"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("player.scraping.notFound.homeButton")}
</Button>
<Button
onClick={() => {
sendPage({
page: "PermissionGrant",
redirectUrl: window.location.href,
});
}}
theme="purple" theme="purple"
padding="md:px-12 p-2.5" padding="md:px-12 p-2.5"
className="mt-6" className="mt-6"
> >
{t("player.scraping.notFound.detailsButton")} {t("player.scraping.notFound.detailsButton")}
</Button> </Button>
</div>
</ErrorContainer> </ErrorContainer>
{error ? ( {error ? (
<ErrorCardInModal <ErrorCardInModal

View file

@ -1,34 +1,16 @@
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; import { useIsMobile } from "@/hooks/useIsMobile";
import { extensionInfo } from "@/backend/extension/messaging";
import { useBannerSize, useBannerStore } from "@/stores/banner"; import { useBannerSize, useBannerStore } from "@/stores/banner";
import { ExtensionBanner } from "@/stores/banner/BannerLocation"; import { ExtensionBanner } from "@/stores/banner/BannerLocation";
import { ExtensionStatus, getExtensionState } from "@/utils/onboarding";
export type ExtensionStatus =
| "unknown"
| "failed"
| "disallowed"
| "noperms"
| "outdated"
| "success";
async function getExtensionState(): Promise<ExtensionStatus> {
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 Layout(props: { children: ReactNode }) { export function Layout(props: { children: ReactNode }) {
const bannerSize = useBannerSize(); const bannerSize = useBannerSize();
const location = useBannerStore((s) => s.location); const location = useBannerStore((s) => s.location);
const [extensionState, setExtensionState] = const [extensionState, setExtensionState] =
useState<ExtensionStatus>("unknown"); useState<ExtensionStatus>("unknown");
const [isMobile, setIsMobile] = useState(false); const { isMobile } = useIsMobile();
useEffect(() => { useEffect(() => {
let isMounted = true; 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 () => { return () => {
isMounted = false; isMounted = false;
mediaQuery.removeListener(handleResize);
}; };
}, []); }, []);

View file

@ -3,8 +3,8 @@ import { Trans, useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { Icon, Icons } from "@/components/Icon"; import { Icon, Icons } from "@/components/Icon";
import { ExtensionStatus } from "@/setup/Layout";
import { useBannerStore, useRegisterBanner } from "@/stores/banner"; import { useBannerStore, useRegisterBanner } from "@/stores/banner";
import type { ExtensionStatus } from "@/utils/onboarding";
export function Banner(props: { export function Banner(props: {
children: React.ReactNode; children: React.ReactNode;

View file

@ -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 { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useOnboardingStore } from "@/stores/onboarding"; import { useOnboardingStore } from "@/stores/onboarding";
export type ExtensionStatus =
| "unknown"
| "failed"
| "disallowed"
| "noperms"
| "outdated"
| "success";
export async function getExtensionState(): Promise<ExtensionStatus> {
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<boolean> { export async function needsOnboarding(): Promise<boolean> {
// if onboarding is dislabed, no onboarding needed // if onboarding is dislabed, no onboarding needed
if (!conf().HAS_ONBOARDING) return false; if (!conf().HAS_ONBOARDING) return false;

View file

@ -16,7 +16,7 @@ const config: Config = {
/* fonts */ /* fonts */
fontFamily: { fontFamily: {
"open-sans": "'Open Sans'", "main": "'DM Sans'",
}, },
/* animations */ /* animations */