mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add subtitles to the player
This commit is contained in:
parent
9b62cb8a93
commit
6eccb2cede
@ -20,7 +20,7 @@ services:
|
||||
- postgres
|
||||
volumes:
|
||||
- ./back:/app
|
||||
- /app/out
|
||||
- /app/out/
|
||||
- kyoo:/var/lib/kyoo
|
||||
- ./video:/video
|
||||
front:
|
||||
@ -29,7 +29,8 @@ services:
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./front:/app
|
||||
- /app/nodes_modules
|
||||
- /app/node_modules/
|
||||
- /app/.next/
|
||||
ports:
|
||||
- "3000:3000"
|
||||
restart: on-failure
|
||||
@ -55,6 +56,7 @@ services:
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
|
||||
|
@ -7,5 +7,6 @@
|
||||
"mute": "Toggle mute",
|
||||
"volume": "Volume",
|
||||
"subtitles": "Subtitles",
|
||||
"subtitle-none": "None",
|
||||
"fullscreen": "Fullscreen"
|
||||
}
|
||||
|
@ -7,5 +7,6 @@
|
||||
"mute": "Muet",
|
||||
"volume": "Volume",
|
||||
"subtitles": "Sous titres",
|
||||
"subtitle-none": "Aucun",
|
||||
"fullscreen": "Plein-écran"
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
|
||||
/**
|
||||
* @type {import("next").NextConfig}
|
||||
*/
|
||||
@ -25,6 +27,21 @@ const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
swcMinify: true,
|
||||
output: "standalone",
|
||||
webpack: (config) => {
|
||||
config.plugins = [
|
||||
...config.plugins,
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
context: "node_modules/@jellyfin/libass-wasm/dist/js/",
|
||||
from: "*",
|
||||
to: "static/chunks/",
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
return config;
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
|
@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.9.3",
|
||||
"@emotion/styled": "^11.9.3",
|
||||
"@jellyfin/libass-wasm": "^4.1.1",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.8.7",
|
||||
"next": "12.2.2",
|
||||
@ -38,6 +39,7 @@
|
||||
"@types/node": "18.0.3",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"eslint": "8.19.0",
|
||||
"eslint-config-next": "12.2.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
@ -25,7 +25,11 @@ import { Link } from "~/utils/link";
|
||||
import { Image } from "./poster";
|
||||
|
||||
export const episodeDisplayNumber = (
|
||||
episode: { seasonNumber?: number; episodeNumber?: number; absoluteNumber?: number },
|
||||
episode: {
|
||||
seasonNumber?: number | null;
|
||||
episodeNumber?: number | null;
|
||||
absoluteNumber?: number | null;
|
||||
},
|
||||
def?: string,
|
||||
) => {
|
||||
if (typeof episode.seasonNumber === "number" && typeof episode.episodeNumber === "number")
|
||||
|
@ -60,6 +60,7 @@ export const TrackP = ResourceP.extend({
|
||||
*/
|
||||
displayName: z.string(),
|
||||
});
|
||||
export type Track = z.infer<typeof TrackP>;
|
||||
|
||||
export const ChapterP = z.object({
|
||||
/**
|
||||
|
@ -228,6 +228,78 @@ const Item = ({ item, layout }: { item?: LibraryItem; layout: Layout }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const SortByMenu = ({
|
||||
sortKey,
|
||||
setSort,
|
||||
sortOrd,
|
||||
setSortOrd,
|
||||
anchor,
|
||||
onClose,
|
||||
}: {
|
||||
sortKey: SortBy;
|
||||
setSort: (sort: SortBy) => void;
|
||||
sortOrd: SortOrd;
|
||||
setSortOrd: (sort: SortOrd) => void;
|
||||
anchor: HTMLElement;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id="sortby-menu"
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "sortby",
|
||||
}}
|
||||
anchorEl={anchor}
|
||||
open={!!anchor}
|
||||
onClose={onClose}
|
||||
>
|
||||
{Object.values(SortBy).map((x) => (
|
||||
<MenuItem
|
||||
key={x}
|
||||
selected={sortKey === x}
|
||||
onClick={() => setSort(x)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
selected={sortOrd === SortOrd.Asc}
|
||||
onClick={() => setSortOrd(SortOrd.Asc)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemIcon>
|
||||
<South fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("browse.sortord.asc")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
selected={sortOrd === SortOrd.Desc}
|
||||
onClick={() => setSortOrd(SortOrd.Desc)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemIcon>
|
||||
<North fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("browse.sortord.desc")}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const BrowseSettings = ({
|
||||
sortKey,
|
||||
setSort,
|
||||
@ -244,7 +316,6 @@ const BrowseSettings = ({
|
||||
setLayout: (layout: Layout) => void;
|
||||
}) => {
|
||||
const [sortAnchor, setSortAnchor] = useState<HTMLElement | null>(null);
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
const switchViewTitle = layout === Layout.Grid
|
||||
@ -265,7 +336,7 @@ const BrowseSettings = ({
|
||||
aria-controls={sortAnchor ? "sorby-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={sortAnchor ? "true" : undefined}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => setSortAnchor(event.currentTarget)}
|
||||
onClick={(event) => setSortAnchor(event.currentTarget)}
|
||||
>
|
||||
<Sort />
|
||||
{t("browse.sortby", { key: t(`browse.sortkey.${sortKey}`) })}
|
||||
@ -282,56 +353,16 @@ const BrowseSettings = ({
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
<Menu
|
||||
id="sortby-menu"
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "sortby",
|
||||
}}
|
||||
anchorEl={sortAnchor}
|
||||
open={!!sortAnchor}
|
||||
onClose={() => setSortAnchor(null)}
|
||||
>
|
||||
{Object.values(SortBy).map((x) => (
|
||||
<MenuItem
|
||||
key={x}
|
||||
selected={sortKey === x}
|
||||
onClick={() => setSort(x)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Divider />
|
||||
<MenuItem
|
||||
selected={sortOrd === SortOrd.Asc}
|
||||
onClick={() => setSortOrd(SortOrd.Asc)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemIcon>
|
||||
<South fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("browse.sortord.asc")}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
selected={sortOrd === SortOrd.Desc}
|
||||
onClick={() => setSortOrd(SortOrd.Desc)}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemIcon>
|
||||
<North fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t("browse.sortord.desc")}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
{sortAnchor && (
|
||||
<SortByMenu
|
||||
sortKey={sortKey}
|
||||
sortOrd={sortOrd}
|
||||
setSort={setSort}
|
||||
setSortOrd={setSortOrd}
|
||||
anchor={sortAnchor}
|
||||
onClose={() => setSortAnchor(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -20,10 +20,18 @@
|
||||
|
||||
import { QueryIdentifier, QueryPage } from "~/utils/query";
|
||||
import { withRoute } from "~/utils/router";
|
||||
import { WatchItem, WatchItemP, Chapter } from "~/models/resources/watch-item";
|
||||
import { WatchItem, WatchItemP, Chapter, Track } from "~/models/resources/watch-item";
|
||||
import { useFetch } from "~/utils/query";
|
||||
import { ErrorPage } from "~/components/errors";
|
||||
import { useState, useRef, useEffect, HTMLProps, memo, useMemo, useCallback } from "react";
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
useEffect,
|
||||
memo,
|
||||
useMemo,
|
||||
useCallback,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
@ -32,6 +40,10 @@ import {
|
||||
Typography,
|
||||
Skeleton,
|
||||
Slider,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
BoxProps,
|
||||
} from "@mui/material";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import {
|
||||
@ -50,7 +62,11 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { Poster } from "~/components/poster";
|
||||
import { episodeDisplayNumber } from "~/components/episode";
|
||||
import { Link } from "~/utils/link";
|
||||
import NextLink from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
// @ts-ignore
|
||||
import SubtitleOctopus from "@jellyfin/libass-wasm"
|
||||
|
||||
const toTimerString = (timer: number, duration?: number) => {
|
||||
if (!duration) duration = timer;
|
||||
@ -58,6 +74,74 @@ const toTimerString = (timer: number, duration?: number) => {
|
||||
return new Date(timer * 1000).toISOString().substring(14, 19);
|
||||
};
|
||||
|
||||
const SubtitleMenu = ({
|
||||
subtitles,
|
||||
setSubtitle,
|
||||
selectedID,
|
||||
anchor,
|
||||
onClose,
|
||||
}: {
|
||||
subtitles: Track[];
|
||||
setSubtitle: (subtitle: Track | null) => void;
|
||||
selectedID?: number;
|
||||
anchor: HTMLElement;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation("player");
|
||||
const { subtitle, ...queryWithoutSubs } = router.query;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id="subtitle-menu"
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "subtitle",
|
||||
}}
|
||||
anchorEl={anchor}
|
||||
open={!!anchor}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
selected={!selectedID}
|
||||
onClick={() => {
|
||||
setSubtitle(null);
|
||||
onClose();
|
||||
}}
|
||||
component={Link}
|
||||
to={{ query: queryWithoutSubs }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemText>{t("subtitle-none")}</ListItemText>
|
||||
</MenuItem>
|
||||
{subtitles.map((sub) => (
|
||||
<MenuItem
|
||||
key={sub.id}
|
||||
selected={selectedID == sub.id}
|
||||
onClick={() => {
|
||||
setSubtitle(sub);
|
||||
onClose();
|
||||
}}
|
||||
component={Link}
|
||||
to={{ query: { ...router.query, subtitle: sub.language ?? sub.id } }}
|
||||
shallow
|
||||
replace
|
||||
>
|
||||
<ListItemText>{sub.displayName}</ListItemText>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingIndicator = () => {
|
||||
return (
|
||||
<Box
|
||||
@ -304,24 +388,50 @@ const LeftButtons = memo(function LeftButtons({
|
||||
const RightButtons = memo(function RightButton({
|
||||
isFullscreen,
|
||||
toggleFullscreen,
|
||||
subtitles,
|
||||
selectedSubtitle,
|
||||
selectSubtitle,
|
||||
}: {
|
||||
isFullscreen: boolean;
|
||||
toggleFullscreen: () => void;
|
||||
subtitles?: Track[];
|
||||
selectedSubtitle: Track | null;
|
||||
selectSubtitle: (track: Track | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("player");
|
||||
const [subtitleAnchor, setSubtitleAnchor] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
return (
|
||||
<Box sx={{ "> *": { mx: "8px !important" } }}>
|
||||
<Tooltip title={t("subtitles")}>
|
||||
<IconButton aria-label={t("subtitles")} sx={{ color: "white" }}>
|
||||
<ClosedCaption />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Box sx={{ "> *": { m: "8px !important" } }}>
|
||||
{subtitles && (
|
||||
<Tooltip title={t("subtitles")}>
|
||||
<IconButton
|
||||
id="sortby"
|
||||
aria-label={t("subtitles")}
|
||||
aria-controls={subtitleAnchor ? "subtitle-menu" : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={subtitleAnchor ? "true" : undefined}
|
||||
onClick={(event) => setSubtitleAnchor(event.currentTarget)}
|
||||
sx={{ color: "white" }}
|
||||
>
|
||||
<ClosedCaption />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t("fullscreen")}>
|
||||
<IconButton onClick={toggleFullscreen} aria-label={t("fullscreen")} sx={{ color: "white" }}>
|
||||
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{subtitleAnchor && (
|
||||
<SubtitleMenu
|
||||
subtitles={subtitles!}
|
||||
anchor={subtitleAnchor}
|
||||
setSubtitle={selectSubtitle}
|
||||
selectedID={selectedSubtitle?.id}
|
||||
onClose={() => setSubtitleAnchor(null)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@ -356,6 +466,67 @@ const Back = memo(function Back({ name, href }: { name?: string; href: string })
|
||||
);
|
||||
});
|
||||
|
||||
const useSubtitleController = (player: RefObject<HTMLVideoElement>): [Track | null, (value: Track | null) => void] => {
|
||||
const [selectedSubtitle, setSubtitle] = useState<Track | null>(null);
|
||||
const [htmlTrack, setHtmlTrack] = useState<HTMLTrackElement | null>(null);
|
||||
const [subocto, setSubOcto] = useState<SubtitleOctopus | null>(null);
|
||||
|
||||
return [
|
||||
selectedSubtitle,
|
||||
useCallback(
|
||||
(value: Track | null) => {
|
||||
const removeHtmlSubtitle = () => {
|
||||
if (htmlTrack) htmlTrack.remove();
|
||||
setHtmlTrack(null);
|
||||
};
|
||||
const removeOctoSub = () => {
|
||||
if (subocto) {
|
||||
subocto.freeTrack();
|
||||
subocto.dispose();
|
||||
}
|
||||
setSubOcto(null);
|
||||
};
|
||||
|
||||
if (!player.current) return;
|
||||
|
||||
setSubtitle(value);
|
||||
if (!value) {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
} else if (value.codec === "vtt" || value.codec === "srt") {
|
||||
removeOctoSub();
|
||||
const track: HTMLTrackElement = htmlTrack ?? document.createElement("track");
|
||||
track.kind = "subtitles";
|
||||
track.label = value.displayName;
|
||||
if (value.language) track.srclang = value.language;
|
||||
track.src = `subtitle/${value.slug}.vtt`;
|
||||
track.className = "subtitle_container";
|
||||
track.default = true;
|
||||
track.onload = () => {
|
||||
if (player.current) player.current.textTracks[0].mode = "showing";
|
||||
};
|
||||
player.current.appendChild(track);
|
||||
setHtmlTrack(track);
|
||||
} else if (value.codec === "ass") {
|
||||
removeHtmlSubtitle();
|
||||
removeOctoSub();
|
||||
setSubOcto(
|
||||
new SubtitleOctopus({
|
||||
video: player.current,
|
||||
subUrl: `/api/subtitle/${value.slug}`,
|
||||
workerUrl: "/_next/static/chunks/subtitles-octopus-worker.js",
|
||||
legacyWorkerUrl: "/_next/static/chunks/subtitles-octopus-worker-legacy.js",
|
||||
/* fonts: */
|
||||
renderMode: "wasm-blend",
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[htmlTrack, subocto, player],
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const useVideoController = () => {
|
||||
const player = useRef<HTMLVideoElement>(null);
|
||||
const [isPlaying, setPlay] = useState(true);
|
||||
@ -366,6 +537,7 @@ const useVideoController = () => {
|
||||
const [volume, setVolume] = useState(100);
|
||||
const [isMuted, setMute] = useState(false);
|
||||
const [isFullscreen, setFullscreen] = useState(false);
|
||||
const [selectedSubtitle, selectSubtitle] = useSubtitleController(player);
|
||||
|
||||
useEffect(() => {
|
||||
if (!player?.current?.duration) return;
|
||||
@ -373,7 +545,7 @@ const useVideoController = () => {
|
||||
}, [player]);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (!player?.current) return;
|
||||
if (!player.current) return;
|
||||
if (!isPlaying) {
|
||||
player.current.play();
|
||||
} else {
|
||||
@ -390,7 +562,7 @@ const useVideoController = () => {
|
||||
}
|
||||
}, [isFullscreen]);
|
||||
|
||||
const videoProps: HTMLProps<HTMLVideoElement> = useMemo(
|
||||
const videoProps: BoxProps<"video"> = useMemo(
|
||||
() => ({
|
||||
ref: player,
|
||||
onClick: togglePlay,
|
||||
@ -418,7 +590,17 @@ const useVideoController = () => {
|
||||
[player, togglePlay, toggleFullscreen],
|
||||
);
|
||||
return {
|
||||
state: { isPlaying, isLoading, progress, duration, buffered, volume, isMuted, isFullscreen },
|
||||
state: {
|
||||
isPlaying,
|
||||
isLoading,
|
||||
progress,
|
||||
duration,
|
||||
buffered,
|
||||
volume,
|
||||
isMuted,
|
||||
isFullscreen,
|
||||
selectedSubtitle,
|
||||
},
|
||||
videoProps,
|
||||
togglePlay,
|
||||
toggleMute: useCallback(() => {
|
||||
@ -439,6 +621,7 @@ const useVideoController = () => {
|
||||
},
|
||||
[player],
|
||||
),
|
||||
selectSubtitle,
|
||||
};
|
||||
};
|
||||
|
||||
@ -447,19 +630,31 @@ const query = (slug: string): QueryIdentifier<WatchItem> => ({
|
||||
parser: WatchItemP,
|
||||
});
|
||||
|
||||
//
|
||||
// Callback used to hide the controls when the mouse goes iddle. This is stored globally to clear the old timeout
|
||||
// if the mouse moves again
|
||||
let mouseCallback: NodeJS.Timeout;
|
||||
|
||||
const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
const { data, error } = useFetch(query(slug));
|
||||
const {
|
||||
state: { isPlaying, isLoading, progress, duration, buffered, volume, isMuted, isFullscreen },
|
||||
state: {
|
||||
isPlaying,
|
||||
isLoading,
|
||||
progress,
|
||||
duration,
|
||||
buffered,
|
||||
volume,
|
||||
isMuted,
|
||||
isFullscreen,
|
||||
selectedSubtitle,
|
||||
},
|
||||
videoProps,
|
||||
togglePlay,
|
||||
toggleMute,
|
||||
toggleFullscreen,
|
||||
setProgress,
|
||||
setVolume,
|
||||
selectSubtitle,
|
||||
} = useVideoController();
|
||||
const [showHover, setHover] = useState(false);
|
||||
const [mouseMoved, setMouseMoved] = useState(false);
|
||||
@ -494,7 +689,7 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
<Box
|
||||
component="video"
|
||||
src={data?.link.direct}
|
||||
{...(videoProps as any)}
|
||||
{...videoProps}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
@ -511,15 +706,19 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
<Box
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
sx={ displayControls ? {
|
||||
visibility: "visible",
|
||||
opacity: 1,
|
||||
transition: "opacity .2s ease-in",
|
||||
} : {
|
||||
visibility: "hidden",
|
||||
opacity: 0,
|
||||
transition: "opacity .4s ease-out, visibility 0s .4s",
|
||||
}}
|
||||
sx={
|
||||
displayControls
|
||||
? {
|
||||
visibility: "visible",
|
||||
opacity: 1,
|
||||
transition: "opacity .2s ease-in",
|
||||
}
|
||||
: {
|
||||
visibility: "hidden",
|
||||
opacity: 0,
|
||||
transition: "opacity .4s ease-out, visibility 0s .4s",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Back
|
||||
name={data?.name}
|
||||
@ -566,7 +765,13 @@ const Player: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
{toTimerString(progress, duration)} : {toTimerString(duration)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<RightButtons isFullscreen={isFullscreen} toggleFullscreen={toggleFullscreen} />
|
||||
<RightButtons
|
||||
isFullscreen={isFullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
subtitles={data?.subtitles}
|
||||
selectedSubtitle={selectedSubtitle}
|
||||
selectSubtitle={selectSubtitle}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
117
front/yarn.lock
117
front/yarn.lock
@ -195,6 +195,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||
|
||||
"@jellyfin/libass-wasm@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@jellyfin/libass-wasm/-/libass-wasm-4.1.1.tgz#d1c0e789844e1ad5d3b36acaeb7351e59f5b7d9a"
|
||||
integrity sha512-xQVJw+lZUg4U1TmLS80reBECfPtpCgRF8hhUSvUUQM9g68OvINyUU3K2yqRH+8tomGpghiRaIcr/bUJ83e0veA==
|
||||
|
||||
"@mui/base@5.0.0-alpha.88":
|
||||
version "5.0.0-alpha.88"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.88.tgz#0930d1849c74ba62a28ab2d8533de88764173ba4"
|
||||
@ -404,6 +409,11 @@
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
@ -535,6 +545,20 @@ acorn@^8.7.1:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
||||
|
||||
ajv-formats@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
|
||||
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
|
||||
dependencies:
|
||||
ajv "^8.0.0"
|
||||
|
||||
ajv-keywords@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16"
|
||||
integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
|
||||
ajv@^6.10.0, ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
@ -545,6 +569,16 @@ ajv@^6.10.0, ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^8.0.0, ajv@^8.8.0:
|
||||
version "8.11.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
|
||||
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-regex@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
|
||||
@ -774,6 +808,18 @@ copy-anything@^3.0.2:
|
||||
dependencies:
|
||||
is-what "^4.1.6"
|
||||
|
||||
copy-webpack-plugin@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
|
||||
integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
|
||||
dependencies:
|
||||
fast-glob "^3.2.11"
|
||||
glob-parent "^6.0.1"
|
||||
globby "^13.1.1"
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^4.0.0"
|
||||
serialize-javascript "^6.0.0"
|
||||
|
||||
core-js-pure@^3.20.2:
|
||||
version "3.23.4"
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.23.4.tgz#aba5c7fb297063444f6bf93afb0362151679a012"
|
||||
@ -1180,6 +1226,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@^3.2.11:
|
||||
version "3.2.12"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
||||
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
version "3.2.11"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
|
||||
@ -1351,6 +1408,17 @@ globby@^11.1.0:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
globby@^13.1.1:
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515"
|
||||
integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ==
|
||||
dependencies:
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.2.11"
|
||||
ignore "^5.2.0"
|
||||
merge2 "^1.4.1"
|
||||
slash "^4.0.0"
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
|
||||
@ -1580,6 +1648,11 @@ json-schema-traverse@^0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-schema-traverse@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
@ -1981,6 +2054,11 @@ next@12.2.2:
|
||||
"@next/swc-win32-ia32-msvc" "12.2.2"
|
||||
"@next/swc-win32-x64-msvc" "12.2.2"
|
||||
|
||||
normalize-path@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
@ -2183,6 +2261,13 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
|
||||
dependencies:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
@ -2260,6 +2345,11 @@ remove-accents@0.4.2:
|
||||
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
|
||||
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
@ -2309,6 +2399,11 @@ sade@^1.7.3:
|
||||
dependencies:
|
||||
mri "^1.1.0"
|
||||
|
||||
safe-buffer@^5.1.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
@ -2321,6 +2416,16 @@ scheduler@^0.23.0:
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
schema-utils@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7"
|
||||
integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
ajv "^8.8.0"
|
||||
ajv-formats "^2.1.1"
|
||||
ajv-keywords "^5.0.0"
|
||||
|
||||
semver@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||
@ -2333,6 +2438,13 @@ semver@^7.3.7:
|
||||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
serialize-javascript@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
|
||||
integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
@ -2359,6 +2471,11 @@ slash@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
slash@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||
|
||||
source-map-js@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user