mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-13 19:19:10 +01:00
Merge branch 'dev' into dev
This commit is contained in:
commit
9aebffd9c9
@ -162,4 +162,4 @@
|
|||||||
<script type="module" src="/src/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -148,6 +148,7 @@
|
|||||||
},
|
},
|
||||||
"media": {
|
"media": {
|
||||||
"episodeDisplay": "S{{season}} E{{episode}}",
|
"episodeDisplay": "S{{season}} E{{episode}}",
|
||||||
|
"unreleased": "Unreleased",
|
||||||
"types": {
|
"types": {
|
||||||
"movie": "Movie",
|
"movie": "Movie",
|
||||||
"show": "Show"
|
"show": "Show"
|
||||||
@ -294,6 +295,7 @@
|
|||||||
"enableSubtitles": "Enable Subtitles",
|
"enableSubtitles": "Enable Subtitles",
|
||||||
"experienceSection": "Viewing experience",
|
"experienceSection": "Viewing experience",
|
||||||
"playbackItem": "Playback settings",
|
"playbackItem": "Playback settings",
|
||||||
|
"audioItem": "Audio",
|
||||||
"qualityItem": "Quality",
|
"qualityItem": "Quality",
|
||||||
"sourceItem": "Video sources",
|
"sourceItem": "Video sources",
|
||||||
"subtitleItem": "Subtitle settings",
|
"subtitleItem": "Subtitle settings",
|
||||||
@ -316,7 +318,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": {
|
||||||
@ -325,7 +327,8 @@
|
|||||||
"fixCapitals": "Fix capitalization"
|
"fixCapitals": "Fix capitalization"
|
||||||
},
|
},
|
||||||
"title": "Subtitles",
|
"title": "Subtitles",
|
||||||
"unknownLanguage": "Unknown"
|
"unknownLanguage": "Unknown",
|
||||||
|
"dropSubtitleFile": "Drop subtitle file here"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -386,6 +389,13 @@
|
|||||||
"homeButton": "Go home",
|
"homeButton": "Go home",
|
||||||
"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.",
|
"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"
|
"title": "We couldn't find that"
|
||||||
|
},
|
||||||
|
"extensionFailure": {
|
||||||
|
"badge": "Extension disabled",
|
||||||
|
"homeButton": "Go home",
|
||||||
|
"enableExtension": "Enable extension",
|
||||||
|
"title": "Please enable the extension",
|
||||||
|
"text": "You've installed the movie-web extension. To start using it, you need to enable the extension for this site."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time": {
|
"time": {
|
||||||
|
@ -43,7 +43,7 @@ export function formatTMDBMetaResult(
|
|||||||
title: movie.title,
|
title: movie.title,
|
||||||
object_type: mediaTypeToTMDB(type),
|
object_type: mediaTypeToTMDB(type),
|
||||||
poster: getMediaPoster(movie.poster_path) ?? undefined,
|
poster: getMediaPoster(movie.poster_path) ?? undefined,
|
||||||
original_release_year: new Date(movie.release_date).getFullYear(),
|
original_release_date: new Date(movie.release_date),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (type === MWMediaType.SERIES) {
|
if (type === MWMediaType.SERIES) {
|
||||||
@ -58,7 +58,7 @@ export function formatTMDBMetaResult(
|
|||||||
title: v.name,
|
title: v.name,
|
||||||
})),
|
})),
|
||||||
poster: getMediaPoster(show.poster_path) ?? undefined,
|
poster: getMediaPoster(show.poster_path) ?? undefined,
|
||||||
original_release_year: new Date(show.first_air_date).getFullYear(),
|
original_release_date: new Date(show.first_air_date),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ export function formatTMDBMeta(
|
|||||||
return {
|
return {
|
||||||
title: media.title,
|
title: media.title,
|
||||||
id: media.id.toString(),
|
id: media.id.toString(),
|
||||||
year: media.original_release_year?.toString(),
|
year: media.original_release_date?.getFullYear()?.toString(),
|
||||||
poster: media.poster,
|
poster: media.poster,
|
||||||
type,
|
type,
|
||||||
seasons: seasons as any,
|
seasons: seasons as any,
|
||||||
@ -94,7 +94,8 @@ export function formatTMDBMetaToMediaItem(media: TMDBMediaResult): MediaItem {
|
|||||||
return {
|
return {
|
||||||
title: media.title,
|
title: media.title,
|
||||||
id: media.id.toString(),
|
id: media.id.toString(),
|
||||||
year: media.original_release_year ?? 0,
|
year: media.original_release_date?.getFullYear() ?? 0,
|
||||||
|
release_date: media.original_release_date,
|
||||||
poster: media.poster,
|
poster: media.poster,
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
@ -260,7 +261,7 @@ export function formatTMDBSearchResult(
|
|||||||
title: show.name,
|
title: show.name,
|
||||||
poster: getMediaPoster(show.poster_path),
|
poster: getMediaPoster(show.poster_path),
|
||||||
id: show.id,
|
id: show.id,
|
||||||
original_release_year: new Date(show.first_air_date).getFullYear(),
|
original_release_date: new Date(show.first_air_date),
|
||||||
object_type: mediatype,
|
object_type: mediatype,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -271,7 +272,7 @@ export function formatTMDBSearchResult(
|
|||||||
title: movie.title,
|
title: movie.title,
|
||||||
poster: getMediaPoster(movie.poster_path),
|
poster: getMediaPoster(movie.poster_path),
|
||||||
id: movie.id,
|
id: movie.id,
|
||||||
original_release_year: new Date(movie.release_date).getFullYear(),
|
original_release_date: new Date(movie.release_date),
|
||||||
object_type: mediatype,
|
object_type: mediatype,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export type TMDBMediaResult = {
|
|||||||
title: string;
|
title: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
id: number;
|
id: number;
|
||||||
original_release_year?: number;
|
original_release_date?: Date;
|
||||||
object_type: TMDBContentTypes;
|
object_type: TMDBContentTypes;
|
||||||
seasons?: TMDBSeasonShort[];
|
seasons?: TMDBSeasonShort[];
|
||||||
};
|
};
|
||||||
|
51
src/components/DropFile.tsx
Normal file
51
src/components/DropFile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -64,6 +64,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 {
|
||||||
@ -134,6 +135,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() {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
@ -24,6 +25,20 @@ export interface MediaCardProps {
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkReleased(media: MediaItem): boolean {
|
||||||
|
const isReleasedYear = Boolean(
|
||||||
|
media.year && media.year <= new Date().getFullYear(),
|
||||||
|
);
|
||||||
|
const isReleasedDate = Boolean(
|
||||||
|
media.release_date && media.release_date <= new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the media has a release date, use that, otherwise use the year
|
||||||
|
const isReleased = media.release_date ? isReleasedDate : isReleasedYear;
|
||||||
|
|
||||||
|
return isReleased;
|
||||||
|
}
|
||||||
|
|
||||||
function MediaCardContent({
|
function MediaCardContent({
|
||||||
media,
|
media,
|
||||||
linkable,
|
linkable,
|
||||||
@ -35,10 +50,19 @@ function MediaCardContent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
const percentageString = `${Math.round(percentage ?? 0).toFixed(0)}%`;
|
||||||
|
|
||||||
const canLink = linkable && !closable;
|
const isReleased = useCallback(() => checkReleased(media), [media]);
|
||||||
|
|
||||||
|
const canLink = linkable && !closable && isReleased();
|
||||||
|
|
||||||
const dotListContent = [t(`media.types.${media.type}`)];
|
const dotListContent = [t(`media.types.${media.type}`)];
|
||||||
if (media.year) dotListContent.push(media.year.toFixed());
|
|
||||||
|
if (media.year) {
|
||||||
|
dotListContent.push(media.year.toFixed());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isReleased()) {
|
||||||
|
dotListContent.push(t("media.unreleased"));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flare.Base
|
<Flare.Base
|
||||||
@ -58,14 +82,14 @@ function MediaCardContent({
|
|||||||
/>
|
/>
|
||||||
<Flare.Child
|
<Flare.Child
|
||||||
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${
|
className={`pointer-events-auto relative mb-2 p-3 transition-transform duration-100 ${
|
||||||
canLink ? "group-hover:scale-95" : ""
|
canLink ? "group-hover:scale-95" : "opacity-60"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100",
|
"relative mb-4 pb-[150%] w-full overflow-hidden rounded-xl bg-mediaCard-hoverBackground bg-cover bg-center transition-[border-radius] duration-100",
|
||||||
{
|
{
|
||||||
"group-hover:rounded-lg": !closable,
|
"group-hover:rounded-lg": canLink,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@ -142,7 +166,12 @@ function MediaCardContent({
|
|||||||
export function MediaCard(props: MediaCardProps) {
|
export function MediaCard(props: MediaCardProps) {
|
||||||
const content = <MediaCardContent {...props} />;
|
const content = <MediaCardContent {...props} />;
|
||||||
|
|
||||||
const canLink = props.linkable && !props.closable;
|
const isReleased = useCallback(
|
||||||
|
() => checkReleased(props.media),
|
||||||
|
[props.media],
|
||||||
|
);
|
||||||
|
|
||||||
|
const canLink = props.linkable && !props.closable && isReleased();
|
||||||
|
|
||||||
let link = canLink
|
let link = canLink
|
||||||
? `/media/${encodeURIComponent(mediaItemToId(props.media))}`
|
? `/media/${encodeURIComponent(mediaItemToId(props.media))}`
|
||||||
@ -157,7 +186,7 @@ export function MediaCard(props: MediaCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.linkable) return <span>{content}</span>;
|
if (!canLink) return <span>{content}</span>;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={link}
|
to={link}
|
||||||
|
@ -14,6 +14,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
|
|||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
import { AudioView } from "./settings/AudioView";
|
||||||
import { CaptionSettingsView } from "./settings/CaptionSettingsView";
|
import { CaptionSettingsView } from "./settings/CaptionSettingsView";
|
||||||
import { CaptionsView } from "./settings/CaptionsView";
|
import { CaptionsView } from "./settings/CaptionsView";
|
||||||
import { DownloadRoutes } from "./settings/Downloads";
|
import { DownloadRoutes } from "./settings/Downloads";
|
||||||
@ -46,6 +47,11 @@ function SettingsOverlay({ id }: { id: string }) {
|
|||||||
<QualityView id={id} />
|
<QualityView id={id} />
|
||||||
</Menu.Card>
|
</Menu.Card>
|
||||||
</OverlayPage>
|
</OverlayPage>
|
||||||
|
<OverlayPage id={id} path="/audio" width={343} height={431}>
|
||||||
|
<Menu.Card>
|
||||||
|
<AudioView id={id} />
|
||||||
|
</Menu.Card>
|
||||||
|
</OverlayPage>
|
||||||
<OverlayPage id={id} path="/captions" width={343} height={431}>
|
<OverlayPage id={id} path="/captions" width={343} height={431}>
|
||||||
<Menu.CardWithScrollable>
|
<Menu.CardWithScrollable>
|
||||||
<CaptionsView id={id} />
|
<CaptionsView id={id} />
|
||||||
|
65
src/components/player/atoms/settings/AudioView.tsx
Normal file
65
src/components/player/atoms/settings/AudioView.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { FlagIcon } from "@/components/FlagIcon";
|
||||||
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
import { AudioTrack } from "@/stores/player/slices/source";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { getPrettyLanguageNameFromLocale } from "@/utils/language";
|
||||||
|
|
||||||
|
import { SelectableLink } from "../../internals/ContextMenu/Links";
|
||||||
|
|
||||||
|
export function AudioOption(props: {
|
||||||
|
langCode?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
selected?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectableLink selected={props.selected} onClick={props.onClick}>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<span data-code={props.langCode} className="mr-3 inline-flex">
|
||||||
|
<FlagIcon langCode={props.langCode} />
|
||||||
|
</span>
|
||||||
|
<span>{props.children}</span>
|
||||||
|
</span>
|
||||||
|
</SelectableLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AudioView({ id }: { id: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const unknownChoice = t("player.menus.subtitles.unknownLanguage");
|
||||||
|
|
||||||
|
const router = useOverlayRouter(id);
|
||||||
|
const audioTracks = usePlayerStore((s) => s.audioTracks);
|
||||||
|
const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack);
|
||||||
|
const changeAudioTrack = usePlayerStore((s) => s.display?.changeAudioTrack);
|
||||||
|
|
||||||
|
const change = useCallback(
|
||||||
|
(track: AudioTrack) => {
|
||||||
|
changeAudioTrack?.(track);
|
||||||
|
router.close();
|
||||||
|
},
|
||||||
|
[router, changeAudioTrack],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu.BackLink onClick={() => router.navigate("/")}>Audio</Menu.BackLink>
|
||||||
|
<Menu.Section className="flex flex-col pb-4">
|
||||||
|
{audioTracks.map((v) => (
|
||||||
|
<AudioOption
|
||||||
|
key={v.id}
|
||||||
|
selected={v.id === currentAudioTrack?.id}
|
||||||
|
langCode={v.language}
|
||||||
|
onClick={audioTracks.includes(v) ? () => change(v) : undefined}
|
||||||
|
>
|
||||||
|
{getPrettyLanguageNameFromLocale(v.language) ?? unknownChoice}
|
||||||
|
</AudioOption>
|
||||||
|
))}
|
||||||
|
</Menu.Section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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
|
||||||
<CaptionOption onClick={() => disable()} selected={!selectedCaptionId}>
|
onClick={() => disable()}
|
||||||
{t("player.menus.subtitles.offChoice")}
|
selected={!selectedCaptionId}
|
||||||
</CaptionOption>
|
>
|
||||||
<CustomCaptionOption />
|
{t("player.menus.subtitles.offChoice")}
|
||||||
{content}
|
</CaptionOption>
|
||||||
</Menu.ScrollToActiveSection>
|
<CustomCaptionOption />
|
||||||
|
{content}
|
||||||
|
</Menu.ScrollToActiveSection>
|
||||||
|
</FileDropHandler>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||||
|
const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack);
|
||||||
const selectedCaptionLanguage = usePlayerStore(
|
const selectedCaptionLanguage = usePlayerStore(
|
||||||
(s) => s.caption.selected?.language,
|
(s) => s.caption.selected?.language,
|
||||||
);
|
);
|
||||||
@ -35,6 +36,11 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
t("player.menus.subtitles.unknownLanguage")
|
t("player.menus.subtitles.unknownLanguage")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const selectedAudioLanguagePretty = currentAudioTrack
|
||||||
|
? getPrettyLanguageNameFromLocale(currentAudioTrack.language) ??
|
||||||
|
t("player.menus.subtitles.unknownLanguage")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const source = usePlayerStore((s) => s.source);
|
const source = usePlayerStore((s) => s.source);
|
||||||
|
|
||||||
const downloadable = source?.type === "file" || source?.type === "hls";
|
const downloadable = source?.type === "file" || source?.type === "hls";
|
||||||
@ -51,6 +57,15 @@ export function SettingsMenu({ id }: { id: string }) {
|
|||||||
>
|
>
|
||||||
{t("player.menus.settings.qualityItem")}
|
{t("player.menus.settings.qualityItem")}
|
||||||
</Menu.ChevronLink>
|
</Menu.ChevronLink>
|
||||||
|
{currentAudioTrack && (
|
||||||
|
<Menu.ChevronLink
|
||||||
|
onClick={() => router.navigate("/audio")}
|
||||||
|
rightText={selectedAudioLanguagePretty ?? undefined}
|
||||||
|
>
|
||||||
|
{t("player.menus.settings.audioItem")}
|
||||||
|
</Menu.ChevronLink>
|
||||||
|
)}
|
||||||
|
|
||||||
<Menu.ChevronLink
|
<Menu.ChevronLink
|
||||||
onClick={() => router.navigate("/source")}
|
onClick={() => router.navigate("/source")}
|
||||||
rightText={sourceName}
|
rightText={sourceName}
|
||||||
|
@ -81,6 +81,24 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
emit("qualities", convertedLevels);
|
emit("qualities", convertedLevels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reportAudioTracks() {
|
||||||
|
if (!hls) return;
|
||||||
|
const currentTrack = hls.audioTracks[hls.audioTrack];
|
||||||
|
emit("changedaudiotrack", {
|
||||||
|
id: currentTrack.id.toString(),
|
||||||
|
label: currentTrack.name,
|
||||||
|
language: currentTrack.lang ?? "unknown",
|
||||||
|
});
|
||||||
|
emit(
|
||||||
|
"audiotracks",
|
||||||
|
hls.audioTracks.map((v) => ({
|
||||||
|
id: v.id.toString(),
|
||||||
|
label: v.name,
|
||||||
|
language: v.lang ?? "unknown",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function setupQualityForHls() {
|
function setupQualityForHls() {
|
||||||
if (videoElement && canPlayHlsNatively(videoElement)) {
|
if (videoElement && canPlayHlsNatively(videoElement)) {
|
||||||
return; // nothing to change
|
return; // nothing to change
|
||||||
@ -155,6 +173,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
if (!hls) return;
|
if (!hls) return;
|
||||||
reportLevels();
|
reportLevels();
|
||||||
setupQualityForHls();
|
setupQualityForHls();
|
||||||
|
reportAudioTracks();
|
||||||
|
|
||||||
if (isExtensionActiveCached()) {
|
if (isExtensionActiveCached()) {
|
||||||
hls.on(Hls.Events.LEVEL_LOADED, async (_, data) => {
|
hls.on(Hls.Events.LEVEL_LOADED, async (_, data) => {
|
||||||
@ -464,5 +483,18 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
hls?.setSubtitleOption({ lang });
|
hls?.setSubtitleOption({ lang });
|
||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
changeAudioTrack(track) {
|
||||||
|
if (!hls) return;
|
||||||
|
const audioTrack = hls?.audioTracks.find(
|
||||||
|
(t) => t.id.toString() === track.id,
|
||||||
|
);
|
||||||
|
if (!audioTrack) return;
|
||||||
|
hls.audioTrack = hls.audioTracks.indexOf(audioTrack);
|
||||||
|
emit("changedaudiotrack", {
|
||||||
|
id: audioTrack.id.toString(),
|
||||||
|
label: audioTrack.name,
|
||||||
|
language: audioTrack.lang ?? "unknown",
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -283,5 +283,8 @@ export function makeChromecastDisplayInterface(
|
|||||||
async setSubtitlePreference() {
|
async setSubtitlePreference() {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
changeAudioTrack() {
|
||||||
|
// cant change audio tracks
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MediaPlaylist } from "hls.js";
|
import { MediaPlaylist } from "hls.js";
|
||||||
|
|
||||||
import { MWMediaType } from "@/backend/metadata/types/mw";
|
import { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { CaptionListItem } from "@/stores/player/slices/source";
|
import { AudioTrack, CaptionListItem } from "@/stores/player/slices/source";
|
||||||
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
|
||||||
import { Listener } from "@/utils/events";
|
import { Listener } from "@/utils/events";
|
||||||
|
|
||||||
@ -25,6 +25,8 @@ export type DisplayInterfaceEvents = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
qualities: SourceQuality[];
|
qualities: SourceQuality[];
|
||||||
changedquality: SourceQuality | null;
|
changedquality: SourceQuality | null;
|
||||||
|
audiotracks: AudioTrack[];
|
||||||
|
changedaudiotrack: AudioTrack | null;
|
||||||
needstrack: boolean;
|
needstrack: boolean;
|
||||||
canairplay: boolean;
|
canairplay: boolean;
|
||||||
playbackrate: number;
|
playbackrate: number;
|
||||||
@ -60,6 +62,7 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
|||||||
automaticQuality: boolean,
|
automaticQuality: boolean,
|
||||||
preferredQuality: SourceQuality | null,
|
preferredQuality: SourceQuality | null,
|
||||||
): void;
|
): void;
|
||||||
|
changeAudioTrack(audioTrack: AudioTrack): void;
|
||||||
processVideoElement(video: HTMLVideoElement): void;
|
processVideoElement(video: HTMLVideoElement): void;
|
||||||
processContainerElement(container: HTMLElement): void;
|
processContainerElement(container: HTMLElement): void;
|
||||||
toggleFullscreen(): void;
|
toggleFullscreen(): void;
|
||||||
|
@ -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/extension";
|
||||||
type ExtensionStatus =
|
import type { ExtensionStatus } from "@/utils/extension";
|
||||||
| "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();
|
||||||
|
@ -83,10 +83,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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
import { sendPage } 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";
|
||||||
@ -10,6 +11,8 @@ 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 { getExtensionState } from "@/utils/extension";
|
||||||
|
import type { ExtensionStatus } from "@/utils/extension";
|
||||||
import { getProviderApiUrls } from "@/utils/proxyUrls";
|
import { getProviderApiUrls } from "@/utils/proxyUrls";
|
||||||
|
|
||||||
import { ErrorCardInModal } from "../errors/ErrorCard";
|
import { ErrorCardInModal } from "../errors/ErrorCard";
|
||||||
@ -25,6 +28,8 @@ 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] =
|
||||||
|
useState<ExtensionStatus>("unknown");
|
||||||
|
|
||||||
const error = useMemo(() => {
|
const error = useMemo(() => {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
@ -42,6 +47,58 @@ export function ScrapeErrorPart(props: ScrapeErrorPartProps) {
|
|||||||
return str;
|
return str;
|
||||||
}, [props, location]);
|
}, [props, location]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getExtensionState().then((state: ExtensionStatus) => {
|
||||||
|
setExtensionState(state);
|
||||||
|
});
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
if (extensionState === "disallowed") {
|
||||||
|
return (
|
||||||
|
<ErrorLayout>
|
||||||
|
<ErrorContainer>
|
||||||
|
<IconPill icon={Icons.LOCK}>
|
||||||
|
{t("player.scraping.extensionFailure.badge")}
|
||||||
|
</IconPill>
|
||||||
|
<Title>{t("player.scraping.extensionFailure.title")}</Title>
|
||||||
|
<Paragraph>
|
||||||
|
<Trans
|
||||||
|
i18nKey="player.scraping.extensionFailure.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.extensionFailure.homeButton")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
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>
|
||||||
|
</ErrorLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
<ErrorContainer>
|
<ErrorContainer>
|
||||||
|
@ -75,6 +75,16 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
|
|||||||
s.currentQuality = quality;
|
s.currentQuality = quality;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
newDisplay.on("audiotracks", (audioTracks) => {
|
||||||
|
set((s) => {
|
||||||
|
s.audioTracks = audioTracks;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
newDisplay.on("changedaudiotrack", (audioTrack) => {
|
||||||
|
set((s) => {
|
||||||
|
s.currentAudioTrack = audioTrack;
|
||||||
|
});
|
||||||
|
});
|
||||||
newDisplay.on("needstrack", (needsTrack) => {
|
newDisplay.on("needstrack", (needsTrack) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.caption.asTrack = needsTrack;
|
s.caption.asTrack = needsTrack;
|
||||||
|
@ -56,12 +56,20 @@ export interface CaptionListItem {
|
|||||||
hls?: boolean;
|
hls?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AudioTrack {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
language: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SourceSlice {
|
export interface SourceSlice {
|
||||||
status: PlayerStatus;
|
status: PlayerStatus;
|
||||||
source: SourceSliceSource | null;
|
source: SourceSliceSource | null;
|
||||||
sourceId: string | null;
|
sourceId: string | null;
|
||||||
qualities: SourceQuality[];
|
qualities: SourceQuality[];
|
||||||
|
audioTracks: AudioTrack[];
|
||||||
currentQuality: SourceQuality | null;
|
currentQuality: SourceQuality | null;
|
||||||
|
currentAudioTrack: AudioTrack | null;
|
||||||
captionList: CaptionListItem[];
|
captionList: CaptionListItem[];
|
||||||
caption: {
|
caption: {
|
||||||
selected: Caption | null;
|
selected: Caption | null;
|
||||||
@ -109,8 +117,10 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
|
|||||||
source: null,
|
source: null,
|
||||||
sourceId: null,
|
sourceId: null,
|
||||||
qualities: [],
|
qualities: [],
|
||||||
|
audioTracks: [],
|
||||||
captionList: [],
|
captionList: [],
|
||||||
currentQuality: null,
|
currentQuality: null,
|
||||||
|
currentAudioTrack: null,
|
||||||
status: playerStatus.IDLE,
|
status: playerStatus.IDLE,
|
||||||
meta: null,
|
meta: null,
|
||||||
caption: {
|
caption: {
|
||||||
|
20
src/utils/extension.ts
Normal file
20
src/utils/extension.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility";
|
||||||
|
import { extensionInfo } from "@/backend/extension/messaging";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -2,6 +2,7 @@ export interface MediaItem {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
year?: number;
|
year?: number;
|
||||||
|
release_date?: Date;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
type: "show" | "movie";
|
type: "show" | "movie";
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user