mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 09:11:53 +01:00
Merge branch 'dev' into variety-fixes
This commit is contained in:
commit
0f7c51c198
@ -10,6 +10,7 @@
|
|||||||
"@use-gesture/react": "^10.2.24",
|
"@use-gesture/react": "^10.2.24",
|
||||||
"core-js": "^3.29.1",
|
"core-js": "^3.29.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
"dompurify": "^3.0.1",
|
||||||
"fscreen": "^1.2.0",
|
"fscreen": "^1.2.0",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
@ -18,6 +19,7 @@
|
|||||||
"json5": "^2.2.0",
|
"json5": "^2.2.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"nanoid": "^4.0.0",
|
"nanoid": "^4.0.0",
|
||||||
|
"node-webvtt": "^1.9.4",
|
||||||
"ofetch": "^1.0.0",
|
"ofetch": "^1.0.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -59,6 +61,7 @@
|
|||||||
"@tailwindcss/line-clamp": "^0.4.2",
|
"@tailwindcss/line-clamp": "^0.4.2",
|
||||||
"@types/chromecast-caf-sender": "^1.0.5",
|
"@types/chromecast-caf-sender": "^1.0.5",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/dompurify": "^2.4.0",
|
||||||
"@types/fscreen": "^1.0.1",
|
"@types/fscreen": "^1.0.1",
|
||||||
"@types/lodash.throttle": "^4.1.7",
|
"@types/lodash.throttle": "^4.1.7",
|
||||||
"@types/node": "^17.0.15",
|
"@types/node": "^17.0.15",
|
||||||
|
27
src/@types/node_webtt.d.ts
vendored
Normal file
27
src/@types/node_webtt.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
declare module "node-webvtt" {
|
||||||
|
interface Cue {
|
||||||
|
identifier: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
styles: string;
|
||||||
|
}
|
||||||
|
interface Options {
|
||||||
|
meta?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
}
|
||||||
|
type ParserError = Error;
|
||||||
|
interface ParseResult {
|
||||||
|
valid: boolean;
|
||||||
|
strict: boolean;
|
||||||
|
cues: Cue[];
|
||||||
|
errors: ParserError[];
|
||||||
|
meta?: Map<string, string>;
|
||||||
|
}
|
||||||
|
interface Segment {
|
||||||
|
duration: number;
|
||||||
|
cues: Cue[];
|
||||||
|
}
|
||||||
|
function parse(text: string, options: Options): ParseResult;
|
||||||
|
function segment(input: string, segmentLength?: number): Segment[];
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
|
||||||
import toWebVTT from "srt-webvtt";
|
import toWebVTT from "srt-webvtt";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
|
export const sanitize = DOMPurify.sanitize;
|
||||||
export const CUSTOM_CAPTION_ID = "customCaption";
|
export const CUSTOM_CAPTION_ID = "customCaption";
|
||||||
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> {
|
||||||
if (caption.type === MWCaptionType.SRT) {
|
if (caption.type === MWCaptionType.SRT) {
|
||||||
|
@ -18,15 +18,14 @@ interface FLIXMediaBase {
|
|||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
type: "Movie" | "TV Series";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FLIXTVSerie extends FLIXMediaBase {
|
interface FLIXTVSerie extends FLIXMediaBase {
|
||||||
type: "TV Series";
|
|
||||||
seasons: number | null;
|
seasons: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FLIXMovie extends FLIXMediaBase {
|
interface FLIXMovie extends FLIXMediaBase {
|
||||||
type: "Movie";
|
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,14 +65,18 @@ registerProvider({
|
|||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
|
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
|
||||||
if (media.meta.type === MWMediaType.MOVIE) {
|
if (media.meta.type === MWMediaType.MOVIE) {
|
||||||
|
if (v.type !== "Movie") return false;
|
||||||
const movie = v as FLIXMovie;
|
const movie = v as FLIXMovie;
|
||||||
return (
|
return (
|
||||||
compareTitle(movie.title, media.meta.title) &&
|
compareTitle(movie.title, media.meta.title) &&
|
||||||
movie.releaseDate === media.meta.year
|
movie.releaseDate === media.meta.year
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (media.meta.type === MWMediaType.SERIES) {
|
||||||
|
if (v.type !== "TV Series") return false;
|
||||||
const serie = v as FLIXTVSerie;
|
const serie = v as FLIXTVSerie;
|
||||||
if (serie.seasons && media.meta.seasons) {
|
if (serie.seasons && media.meta.seasons) {
|
||||||
return (
|
return (
|
||||||
@ -81,7 +84,9 @@ registerProvider({
|
|||||||
serie.seasons === media.meta.seasons.length
|
serie.seasons === media.meta.seasons.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return compareTitle(serie.title, media.meta.title);
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
if (!foundItem) throw new Error("No watchable item found");
|
if (!foundItem) throw new Error("No watchable item found");
|
||||||
const flixId = foundItem.id;
|
const flixId = foundItem.id;
|
||||||
@ -110,6 +115,7 @@ registerProvider({
|
|||||||
)?.id;
|
)?.id;
|
||||||
}
|
}
|
||||||
if (!episodeId) throw new Error("No watchable item found");
|
if (!episodeId) throw new Error("No watchable item found");
|
||||||
|
|
||||||
const watchInfo = await proxiedFetch<any>("/watch", {
|
const watchInfo = await proxiedFetch<any>("/watch", {
|
||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
params: {
|
params: {
|
||||||
|
@ -39,6 +39,7 @@ export enum Icons {
|
|||||||
GEAR = "gear",
|
GEAR = "gear",
|
||||||
WATCH_PARTY = "watch_party",
|
WATCH_PARTY = "watch_party",
|
||||||
PICTURE_IN_PICTURE = "pictureInPicture",
|
PICTURE_IN_PICTURE = "pictureInPicture",
|
||||||
|
CHECKMARK = "checkmark",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
@ -85,6 +86,7 @@ const iconList: Record<Icons, string> = {
|
|||||||
gear: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M481.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-30.9 28.1c-7.7 7.1-11.4 17.5-10.9 27.9c.1 2.9 .2 5.8 .2 8.8s-.1 5.9-.2 8.8c-.5 10.5 3.1 20.9 10.9 27.9l30.9 28.1c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-39.7-12.6c-10-3.2-20.8-1.1-29.7 4.6c-4.9 3.1-9.9 6.1-15.1 8.7c-9.3 4.8-16.5 13.2-18.8 23.4l-8.9 40.7c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-8.9-40.7c-2.2-10.2-9.5-18.6-18.8-23.4c-5.2-2.7-10.2-5.6-15.1-8.7c-8.8-5.7-19.7-7.8-29.7-4.6L69.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l30.9-28.1c7.7-7.1 11.4-17.5 10.9-27.9c-.1-2.9-.2-5.8-.2-8.8s.1-5.9 .2-8.8c.5-10.5-3.1-20.9-10.9-27.9L8.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l39.7 12.6c10 3.2 20.8 1.1 29.7-4.6c4.9-3.1 9.9-6.1 15.1-8.7c9.3-4.8 16.5-13.2 18.8-23.4l8.9-40.7c2-9.1 9-16.3 18.2-17.8C213.3 1.2 227.5 0 242 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l8.9 40.7c2.2 10.2 9.4 18.6 18.8 23.4c5.2 2.7 10.2 5.6 15.1 8.7c8.8 5.7 19.7 7.7 29.7 4.6l39.7-12.6c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM242 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>`,
|
gear: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M481.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-30.9 28.1c-7.7 7.1-11.4 17.5-10.9 27.9c.1 2.9 .2 5.8 .2 8.8s-.1 5.9-.2 8.8c-.5 10.5 3.1 20.9 10.9 27.9l30.9 28.1c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-39.7-12.6c-10-3.2-20.8-1.1-29.7 4.6c-4.9 3.1-9.9 6.1-15.1 8.7c-9.3 4.8-16.5 13.2-18.8 23.4l-8.9 40.7c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-8.9-40.7c-2.2-10.2-9.5-18.6-18.8-23.4c-5.2-2.7-10.2-5.6-15.1-8.7c-8.8-5.7-19.7-7.8-29.7-4.6L69.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l30.9-28.1c7.7-7.1 11.4-17.5 10.9-27.9c-.1-2.9-.2-5.8-.2-8.8s.1-5.9 .2-8.8c.5-10.5-3.1-20.9-10.9-27.9L8.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l39.7 12.6c10 3.2 20.8 1.1 29.7-4.6c4.9-3.1 9.9-6.1 15.1-8.7c9.3-4.8 16.5-13.2 18.8-23.4l8.9-40.7c2-9.1 9-16.3 18.2-17.8C213.3 1.2 227.5 0 242 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l8.9 40.7c2.2 10.2 9.4 18.6 18.8 23.4c5.2 2.7 10.2 5.6 15.1 8.7c8.8 5.7 19.7 7.7 29.7 4.6l39.7-12.6c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM242 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg>`,
|
||||||
watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`,
|
watch_party: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M319.4 372c48.5-31.3 80.6-85.9 80.6-148c0-97.2-78.8-176-176-176S48 126.8 48 224c0 62.1 32.1 116.6 80.6 148c1.2 17.3 4 38 7.2 57.1l.2 1C56 395.8 0 316.5 0 224C0 100.3 100.3 0 224 0S448 100.3 448 224c0 92.5-56 171.9-136 206.1l.2-1.1c3.1-19.2 6-39.8 7.2-57zm-2.3-38.1c-1.6-5.7-3.9-11.1-7-16.2c-5.8-9.7-13.5-17-21.9-22.4c19.5-17.6 31.8-43 31.8-71.3c0-53-43-96-96-96s-96 43-96 96c0 28.3 12.3 53.8 31.8 71.3c-8.4 5.4-16.1 12.7-21.9 22.4c-3.1 5.1-5.4 10.5-7 16.2C99.8 307.5 80 268 80 224c0-79.5 64.5-144 144-144s144 64.5 144 144c0 44-19.8 83.5-50.9 109.9zM224 312c32.9 0 64 8.6 64 43.8c0 33-12.9 104.1-20.6 132.9c-5.1 19-24.5 23.4-43.4 23.4s-38.2-4.4-43.4-23.4c-7.8-28.5-20.6-99.7-20.6-132.8c0-35.1 31.1-43.8 64-43.8zm0-144a56 56 0 1 1 0 112 56 56 0 1 1 0-112z"/></svg>`,
|
||||||
pictureInPicture: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/></svg>`,
|
pictureInPicture: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/></svg>`,
|
||||||
|
checkmark: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 24 24"><path d="M9 22l-10-10.598 2.798-2.859 7.149 7.473 13.144-14.016 2.909 2.806z" /></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChromeCastButton() {
|
function ChromeCastButton() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
import { Redirect, Route, Switch } from "react-router-dom";
|
||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
import { WatchedContextProvider } from "@/state/watched";
|
import { WatchedContextProvider } from "@/state/watched";
|
||||||
|
import { SettingsProvider } from "@/state/settings";
|
||||||
|
|
||||||
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
import { NotFoundPage } from "@/views/notfound/NotFoundView";
|
||||||
import { MediaView } from "@/views/media/MediaView";
|
import { MediaView } from "@/views/media/MediaView";
|
||||||
@ -17,6 +18,7 @@ import { TestView } from "@/views/developer/TestView";
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<SettingsProvider>
|
||||||
<WatchedContextProvider>
|
<WatchedContextProvider>
|
||||||
<BookmarkContextProvider>
|
<BookmarkContextProvider>
|
||||||
<BannerContextProvider>
|
<BannerContextProvider>
|
||||||
@ -57,6 +59,7 @@ function App() {
|
|||||||
</BannerContextProvider>
|
</BannerContextProvider>
|
||||||
</BookmarkContextProvider>
|
</BookmarkContextProvider>
|
||||||
</WatchedContextProvider>
|
</WatchedContextProvider>
|
||||||
|
</SettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,3 +54,123 @@ body[data-no-select] {
|
|||||||
.google-cast-button:not(.casting) google-cast-launcher {
|
.google-cast-button:not(.casting) google-cast-launcher {
|
||||||
@apply brightness-[500];
|
@apply brightness-[500];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*generated with Input range slider CSS style generator (version 20211225)
|
||||||
|
https://toughengineer.github.io/demo/slider-styler*/
|
||||||
|
:root {
|
||||||
|
--slider-height: 0.25rem;
|
||||||
|
--slider-border-radius: 1em;
|
||||||
|
--slider-progress-background: #8652bb;
|
||||||
|
}
|
||||||
|
input[type=range].styled-slider {
|
||||||
|
height: var(--slider-height);
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #1C161B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*progress support*/
|
||||||
|
input[type=range].styled-slider.slider-progress {
|
||||||
|
--range: calc(var(--max) - var(--min));
|
||||||
|
--ratio: calc((var(--value) - var(--min)) / var(--range));
|
||||||
|
--sx: calc(0.5 * 1rem + var(--ratio) * (100% - 1rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*webkit*/
|
||||||
|
input[type=range].styled-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 2px #000000;
|
||||||
|
margin-top: calc(0.25em * 0.5 - 1rem * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-webkit-slider-runnable-track {
|
||||||
|
height: var(--slider-height);
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
margin-bottom: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-webkit-slider-thumb:hover {
|
||||||
|
background: #DCDCDC;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider.slider-progress::-webkit-slider-runnable-track {
|
||||||
|
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*mozilla*/
|
||||||
|
input[type=range].styled-slider::-moz-range-thumb {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 2px #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-moz-range-track {
|
||||||
|
height: var(--slider-height);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #1C161B;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-moz-range-thumb:hover {
|
||||||
|
background: #DCDCDC;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider.slider-progress::-moz-range-track {
|
||||||
|
background: linear-gradient(var(--slider-progress-background),var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*ms*/
|
||||||
|
input[type=range].styled-slider::-ms-fill-upper {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-ms-fill-lower {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-ms-thumb {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 2px #000000;
|
||||||
|
margin-top: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-ms-track {
|
||||||
|
height: var(--slider-height);
|
||||||
|
border-radius: var(--slider-border-radius);
|
||||||
|
background: #1C161B;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider::-ms-thumb:hover {
|
||||||
|
background: #DCDCDC;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range].styled-slider.slider-progress::-ms-fill-lower {
|
||||||
|
height: var(--slider-height);
|
||||||
|
border-radius: var(--slider-border-radius) 0 0 5px;
|
||||||
|
margin: -undefined 0 -undefined -undefined;
|
||||||
|
background: var(--slider-progress-background);
|
||||||
|
border: none;
|
||||||
|
border-right-width: 0;
|
||||||
|
}
|
||||||
|
@ -69,6 +69,13 @@
|
|||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "Customize",
|
||||||
|
"delay": "Delay",
|
||||||
|
"fontSize": "Size",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"color": "Color"
|
||||||
|
},
|
||||||
"episode": "E{{index}} - {{title}}",
|
"episode": "E{{index}} - {{title}}",
|
||||||
"noCaptions": "No captions",
|
"noCaptions": "No captions",
|
||||||
"linkedCaptions": "Linked captions",
|
"linkedCaptions": "Linked captions",
|
||||||
@ -84,7 +91,8 @@
|
|||||||
"embeds": "Choose which video to view",
|
"embeds": "Choose which video to view",
|
||||||
"seasons": "Choose which season you want to watch",
|
"seasons": "Choose which season you want to watch",
|
||||||
"episode": "Pick an episode",
|
"episode": "Pick an episode",
|
||||||
"captions": "Choose a subtitle language"
|
"captions": "Choose a subtitle language",
|
||||||
|
"captionPreferences": "Make subtitles look how you want it"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
78
src/state/settings/context.tsx
Normal file
78
src/state/settings/context.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useStore } from "@/utils/storage";
|
||||||
|
import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||||
|
import { SettingsStore } from "./store";
|
||||||
|
import { MWSettingsData } from "./types";
|
||||||
|
|
||||||
|
interface MWSettingsDataSetters {
|
||||||
|
setLanguage(language: string): void;
|
||||||
|
setCaptionDelay(delay: number): void;
|
||||||
|
setCaptionColor(color: string): void;
|
||||||
|
setCaptionFontSize(size: number): void;
|
||||||
|
setCaptionBackgroundColor(backgroundColor: string): void;
|
||||||
|
}
|
||||||
|
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
|
||||||
|
const SettingsContext = createContext<MWSettingsDataWrapper>(null as any);
|
||||||
|
export function SettingsProvider(props: { children: ReactNode }) {
|
||||||
|
function enforceRange(min: number, value: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(value, max));
|
||||||
|
}
|
||||||
|
const [settings, setSettings] = useStore(SettingsStore);
|
||||||
|
|
||||||
|
const context: MWSettingsDataWrapper = useMemo(() => {
|
||||||
|
const settingsContext: MWSettingsDataWrapper = {
|
||||||
|
...settings,
|
||||||
|
setLanguage(language) {
|
||||||
|
setSettings((oldSettings) => {
|
||||||
|
return {
|
||||||
|
...oldSettings,
|
||||||
|
language,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCaptionDelay(delay: number) {
|
||||||
|
setSettings((oldSettings) => {
|
||||||
|
const captionSettings = oldSettings.captionSettings;
|
||||||
|
captionSettings.delay = enforceRange(-10, delay, 10);
|
||||||
|
const newSettings = oldSettings;
|
||||||
|
return newSettings;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCaptionColor(color) {
|
||||||
|
setSettings((oldSettings) => {
|
||||||
|
const style = oldSettings.captionSettings.style;
|
||||||
|
style.color = color;
|
||||||
|
const newSettings = oldSettings;
|
||||||
|
return newSettings;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCaptionFontSize(size) {
|
||||||
|
setSettings((oldSettings) => {
|
||||||
|
const style = oldSettings.captionSettings.style;
|
||||||
|
style.fontSize = enforceRange(10, size, 60);
|
||||||
|
const newSettings = oldSettings;
|
||||||
|
return newSettings;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCaptionBackgroundColor(backgroundColor) {
|
||||||
|
setSettings((oldSettings) => {
|
||||||
|
const style = oldSettings.captionSettings.style;
|
||||||
|
style.backgroundColor = backgroundColor;
|
||||||
|
const newSettings = oldSettings;
|
||||||
|
return newSettings;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return settingsContext;
|
||||||
|
}, [settings, setSettings]);
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={context}>
|
||||||
|
{props.children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
return useContext(SettingsContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsContext;
|
1
src/state/settings/index.ts
Normal file
1
src/state/settings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./context";
|
22
src/state/settings/store.ts
Normal file
22
src/state/settings/store.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { createVersionedStore } from "@/utils/storage";
|
||||||
|
import { MWSettingsData } from "./types";
|
||||||
|
|
||||||
|
export const SettingsStore = createVersionedStore<MWSettingsData>()
|
||||||
|
.setKey("mw-settings")
|
||||||
|
.addVersion({
|
||||||
|
version: 0,
|
||||||
|
create(): MWSettingsData {
|
||||||
|
return {
|
||||||
|
language: "en",
|
||||||
|
captionSettings: {
|
||||||
|
delay: 0,
|
||||||
|
style: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: 25,
|
||||||
|
backgroundColor: "#00000096",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.build();
|
21
src/state/settings/types.ts
Normal file
21
src/state/settings/types.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export interface CaptionStyleSettings {
|
||||||
|
color: string;
|
||||||
|
/**
|
||||||
|
* Range is [10, 30]
|
||||||
|
*/
|
||||||
|
fontSize: number;
|
||||||
|
backgroundColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptionSettings {
|
||||||
|
/**
|
||||||
|
* Range is [-10, 10]s
|
||||||
|
*/
|
||||||
|
delay: number;
|
||||||
|
style: CaptionStyleSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MWSettingsData {
|
||||||
|
language: string;
|
||||||
|
captionSettings: CaptionSettings;
|
||||||
|
}
|
@ -27,9 +27,10 @@ import { ReactNode, useCallback, useState } from "react";
|
|||||||
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderAction";
|
||||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||||
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
||||||
|
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||||
|
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
||||||
import { SettingsAction } from "./actions/SettingsAction";
|
import { SettingsAction } from "./actions/SettingsAction";
|
||||||
import { DividerAction } from "./actions/DividerAction";
|
import { DividerAction } from "./actions/DividerAction";
|
||||||
import { PictureInPictureAction } from "./actions/PictureInPictureAction";
|
|
||||||
|
|
||||||
type Props = VideoPlayerBaseProps;
|
type Props = VideoPlayerBaseProps;
|
||||||
|
|
||||||
@ -165,6 +166,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
</Transition>
|
</Transition>
|
||||||
{show ? <PopoutProviderAction /> : null}
|
{show ? <PopoutProviderAction /> : null}
|
||||||
</BackdropAction>
|
</BackdropAction>
|
||||||
|
<CaptionRendererAction isControlsShown={show} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</VideoPlayerError>
|
</VideoPlayerError>
|
||||||
</>
|
</>
|
||||||
|
92
src/video/components/actions/CaptionRendererAction.tsx
Normal file
92
src/video/components/actions/CaptionRendererAction.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { useSettings } from "@/state/settings";
|
||||||
|
import { sanitize } from "@/backend/helpers/captions";
|
||||||
|
import { parse, Cue } from "node-webvtt";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||||
|
import { useProgress } from "../../state/logic/progress";
|
||||||
|
import { useSource } from "../../state/logic/source";
|
||||||
|
|
||||||
|
function CaptionCue({ text }: { text?: string }) {
|
||||||
|
const { captionSettings } = useSettings();
|
||||||
|
const textWithNewlines = (text || "").replaceAll(/\r?\n/g, "<br />");
|
||||||
|
|
||||||
|
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||||
|
// added a <br /> for newlines
|
||||||
|
const html = sanitize(textWithNewlines, {
|
||||||
|
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
|
||||||
|
ADD_TAGS: ["v", "lang"],
|
||||||
|
ALLOWED_ATTR: ["title", "lang"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
||||||
|
style={{
|
||||||
|
...captionSettings.style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
// its sanitised a few lines up
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: html,
|
||||||
|
}}
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CaptionRendererAction({
|
||||||
|
isControlsShown,
|
||||||
|
}: {
|
||||||
|
isControlsShown: boolean;
|
||||||
|
}) {
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const source = useSource(descriptor).source;
|
||||||
|
const videoTime = useProgress(descriptor).time;
|
||||||
|
const { captionSettings } = useSettings();
|
||||||
|
const captions = useRef<Cue[]>([]);
|
||||||
|
|
||||||
|
useAsync(async () => {
|
||||||
|
const url = source?.caption?.url;
|
||||||
|
if (url) {
|
||||||
|
// Is there a better way?
|
||||||
|
const result = await fetch(url);
|
||||||
|
// Uses UTF-8 by default
|
||||||
|
const text = await result.text();
|
||||||
|
captions.current = parse(text, { strict: false }).cues;
|
||||||
|
} else {
|
||||||
|
captions.current = [];
|
||||||
|
}
|
||||||
|
}, [source?.caption?.url]);
|
||||||
|
|
||||||
|
if (!captions.current.length) return null;
|
||||||
|
const isVisible = (start: number, end: number): boolean => {
|
||||||
|
const delayedStart = start + captionSettings.delay;
|
||||||
|
const delayedEnd = end + captionSettings.delay;
|
||||||
|
return (
|
||||||
|
Math.max(0, delayedStart) <= videoTime &&
|
||||||
|
Math.max(0, delayedEnd) >= videoTime
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Transition
|
||||||
|
className={[
|
||||||
|
"pointer-events-none absolute flex w-full flex-col items-center transition-[bottom]",
|
||||||
|
isControlsShown ? "bottom-24" : "bottom-12",
|
||||||
|
].join(" ")}
|
||||||
|
animation="slide-up"
|
||||||
|
show
|
||||||
|
>
|
||||||
|
{captions.current.map(
|
||||||
|
({ identifier, end, start, text }) =>
|
||||||
|
isVisible(start, end) && (
|
||||||
|
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
import { useMisc } from "@/video/state/logic/misc";
|
import { useMisc } from "@/video/state/logic/misc";
|
||||||
import { useSource } from "@/video/state/logic/source";
|
|
||||||
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
import { setProvider, unsetStateProvider } from "@/video/state/providers/utils";
|
||||||
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
import { createVideoStateProvider } from "@/video/state/providers/videoStateProvider";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
@ -13,7 +12,6 @@ interface Props {
|
|||||||
function VideoElement(props: Props) {
|
function VideoElement(props: Props) {
|
||||||
const descriptor = useVideoPlayerDescriptor();
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
const mediaPlaying = useMediaPlaying(descriptor);
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
const source = useSource(descriptor);
|
|
||||||
const misc = useMisc(descriptor);
|
const misc = useMisc(descriptor);
|
||||||
const ref = useRef<HTMLVideoElement>(null);
|
const ref = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
@ -44,11 +42,7 @@ function VideoElement(props: Props) {
|
|||||||
muted={mediaPlaying.volume === 0}
|
muted={mediaPlaying.volume === 0}
|
||||||
playsInline
|
playsInline
|
||||||
className="z-0 h-full w-full"
|
className="z-0 h-full w-full"
|
||||||
>
|
/>
|
||||||
{source.source?.caption ? (
|
|
||||||
<track default kind="captions" src={source.source.caption.url} />
|
|
||||||
) : null}
|
|
||||||
</video>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,6 @@ export function CaptionSelectionPopout(props: {
|
|||||||
const captionFile = e.target.files[0];
|
const captionFile = e.target.files[0];
|
||||||
setCustomCaption(captionFile);
|
setCustomCaption(captionFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FloatingView
|
<FloatingView
|
||||||
{...props.router.pageProps(props.prefix)}
|
{...props.router.pageProps(props.prefix)}
|
||||||
@ -81,6 +80,18 @@ export function CaptionSelectionPopout(props: {
|
|||||||
title={t("videoPlayer.popouts.captions")}
|
title={t("videoPlayer.popouts.captions")}
|
||||||
description={t("videoPlayer.popouts.descriptions.captions")}
|
description={t("videoPlayer.popouts.descriptions.captions")}
|
||||||
goBack={() => props.router.navigate("/")}
|
goBack={() => props.router.navigate("/")}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
props.router.navigate(`${props.prefix}/caption-settings`)
|
||||||
|
}
|
||||||
|
className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white"
|
||||||
|
>
|
||||||
|
<span>{t("videoPlayer.popouts.captionPreferences.title")}</span>
|
||||||
|
<Icon icon={Icons.GEAR} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FloatingCardView.Content noSection>
|
<FloatingCardView.Content noSection>
|
||||||
<PopoutSection>
|
<PopoutSection>
|
||||||
|
150
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
150
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||||
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
|
import { useSettings } from "@/state/settings";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ChangeEventHandler, useEffect, useRef } from "react";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
|
||||||
|
export type SliderProps = {
|
||||||
|
label: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
value: number;
|
||||||
|
valueDisplay?: string;
|
||||||
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Slider(props: SliderProps) {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const e = ref.current as HTMLInputElement;
|
||||||
|
e.style.setProperty("--value", e.value);
|
||||||
|
e.style.setProperty("--min", e.min === "" ? "0" : e.min);
|
||||||
|
e.style.setProperty("--max", e.max === "" ? "100" : e.max);
|
||||||
|
e.addEventListener("input", () => e.style.setProperty("--value", e.value));
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 flex flex-row gap-4">
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
<label className="font-bold">{props.label}</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
ref={ref}
|
||||||
|
className="styled-slider slider-progress"
|
||||||
|
onChange={props.onChange}
|
||||||
|
value={props.value}
|
||||||
|
max={props.max}
|
||||||
|
min={props.min}
|
||||||
|
step={props.step}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
||||||
|
<div className="text-center font-bold text-white">
|
||||||
|
{props.valueDisplay ?? props.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CaptionSettingsPopout(props: {
|
||||||
|
router: ReturnType<typeof useFloatingRouter>;
|
||||||
|
prefix: string;
|
||||||
|
}) {
|
||||||
|
// For now, won't add label texts to language files since options are prone to change
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
captionSettings,
|
||||||
|
setCaptionBackgroundColor,
|
||||||
|
setCaptionColor,
|
||||||
|
setCaptionDelay,
|
||||||
|
setCaptionFontSize,
|
||||||
|
} = useSettings();
|
||||||
|
const colors = ["#ffffff", "#00ffff", "#ffff00"];
|
||||||
|
return (
|
||||||
|
<FloatingView {...props.router.pageProps(props.prefix)} width={375}>
|
||||||
|
<FloatingCardView.Header
|
||||||
|
title={t("videoPlayer.popouts.captionPreferences.title")}
|
||||||
|
description={t("videoPlayer.popouts.descriptions.captionPreferences")}
|
||||||
|
goBack={() => props.router.navigate("/captions")}
|
||||||
|
/>
|
||||||
|
<FloatingCardView.Content>
|
||||||
|
<Slider
|
||||||
|
label={t("videoPlayer.popouts.captionPreferences.delay")}
|
||||||
|
max={10}
|
||||||
|
min={-10}
|
||||||
|
step={0.1}
|
||||||
|
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
||||||
|
value={captionSettings.delay}
|
||||||
|
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label="Size"
|
||||||
|
min={14}
|
||||||
|
step={1}
|
||||||
|
max={60}
|
||||||
|
value={captionSettings.style.fontSize}
|
||||||
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label={t("videoPlayer.popouts.captionPreferences.opacity")}
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={255}
|
||||||
|
valueDisplay={`${(
|
||||||
|
(parseInt(
|
||||||
|
captionSettings.style.backgroundColor.substring(7, 9),
|
||||||
|
16
|
||||||
|
) /
|
||||||
|
255) *
|
||||||
|
100
|
||||||
|
).toFixed(0)}%`}
|
||||||
|
value={parseInt(
|
||||||
|
captionSettings.style.backgroundColor.substring(7, 9),
|
||||||
|
16
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCaptionBackgroundColor(
|
||||||
|
`${captionSettings.style.backgroundColor.substring(
|
||||||
|
0,
|
||||||
|
7
|
||||||
|
)}${e.target.valueAsNumber.toString(16)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<label className="font-bold" htmlFor="color">
|
||||||
|
{t("videoPlayer.popouts.captionPreferences.color")}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
{colors.map((color) => (
|
||||||
|
<div
|
||||||
|
className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${
|
||||||
|
color === captionSettings.style.color ? "bg-[#1C161B]" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setCaptionColor(color)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
className={[
|
||||||
|
"absolute text-xs text-[#1C161B]",
|
||||||
|
color === captionSettings.style.color ? "" : "hidden",
|
||||||
|
].join(" ")}
|
||||||
|
icon={Icons.CHECKMARK}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FloatingCardView.Content>
|
||||||
|
</FloatingView>
|
||||||
|
);
|
||||||
|
}
|
@ -7,6 +7,7 @@ import { CaptionsSelectionAction } from "@/video/components/actions/list-entries
|
|||||||
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
||||||
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
||||||
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
||||||
|
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
||||||
|
|
||||||
export function SettingsPopout() {
|
export function SettingsPopout() {
|
||||||
const floatingRouter = useFloatingRouter();
|
const floatingRouter = useFloatingRouter();
|
||||||
@ -24,6 +25,10 @@ export function SettingsPopout() {
|
|||||||
</FloatingView>
|
</FloatingView>
|
||||||
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
||||||
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
|
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
|
||||||
|
<CaptionSettingsPopout
|
||||||
|
router={floatingRouter}
|
||||||
|
prefix="caption-settings"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
|
"typeRoots": ["./src/@types"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user