mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01:00
Merge branch 'dev' of https://github.com/movie-web/movie-web into subtitle-file-type-control
This commit is contained in:
commit
a910c1c18c
20 changed files with 222 additions and 57 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.0.9",
|
"version": "3.0.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie.squeezebox.dev",
|
"homepage": "https://movie.squeezebox.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -22,6 +22,7 @@ registerProvider({
|
||||||
displayName: "NetFilm",
|
displayName: "NetFilm",
|
||||||
rank: 15,
|
rank: 15,
|
||||||
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
|
disabled: true, // The creator has asked us (very nicely) to leave him alone. Until (if) we self-host, netfilm should remain disabled
|
||||||
|
|
||||||
async scrape({ media, episode, progress }) {
|
async scrape({ media, episode, progress }) {
|
||||||
if (!this.type.includes(media.meta.type)) {
|
if (!this.type.includes(media.meta.type)) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ export enum Icons {
|
||||||
WATCH_PARTY = "watch_party",
|
WATCH_PARTY = "watch_party",
|
||||||
PICTURE_IN_PICTURE = "pictureInPicture",
|
PICTURE_IN_PICTURE = "pictureInPicture",
|
||||||
CHECKMARK = "checkmark",
|
CHECKMARK = "checkmark",
|
||||||
|
TACHOMETER = "tachometer",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
|
@ -87,6 +88,7 @@ const iconList: Record<Icons, string> = {
|
||||||
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>`,
|
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>`,
|
||||||
|
tachometer: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" fill="currentColor" viewBox="0 0 576 512"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M128 288c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zm154.65-97.08l16.24-48.71c1.16-3.45 3.18-6.35 4.92-9.43-4.73-2.76-9.94-4.78-15.81-4.78-17.67 0-32 14.33-32 32 0 15.78 11.63 28.29 26.65 30.92zM176 176c-17.67 0-32 14.33-32 32s14.33 32 32 32 32-14.33 32-32-14.33-32-32-32zM288 32C128.94 32 0 160.94 0 320c0 52.8 14.25 102.26 39.06 144.8 5.61 9.62 16.3 15.2 27.44 15.2h443c11.14 0 21.83-5.58 27.44-15.2C561.75 422.26 576 372.8 576 320c0-159.06-128.94-288-288-288zm212.27 400H75.73C57.56 397.63 48 359.12 48 320 48 187.66 155.66 80 288 80s240 107.66 240 240c0 39.12-9.56 77.63-27.73 112zM416 320c0 17.67 14.33 32 32 32s32-14.33 32-32-14.33-32-32-32-32 14.33-32 32zm-56.41-182.77c-12.72-4.23-26.16 2.62-30.38 15.17l-45.34 136.01C250.49 290.58 224 318.06 224 352c0 11.72 3.38 22.55 8.88 32h110.25c5.5-9.45 8.88-20.28 8.88-32 0-19.45-8.86-36.66-22.55-48.4l45.34-136.01c4.17-12.57-2.64-26.17-15.21-30.36zM432 208c0-15.8-11.66-28.33-26.72-30.93-.07.21-.07.43-.14.65l-19.5 58.49c4.37 2.24 9.11 3.8 14.36 3.8 17.67-.01 32-14.34 32-32.01z"/></svg>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChromeCastButton() {
|
function ChromeCastButton() {
|
||||||
|
|
47
src/components/Slider.tsx
Normal file
47
src/components/Slider.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { ChangeEventHandler, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
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">
|
||||||
|
{props.label ? (
|
||||||
|
<label className="font-bold">{props.label}</label>
|
||||||
|
) : null}
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
ref={ref}
|
||||||
|
className="styled-slider slider-progress mt-[20px]"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ export function FloatingView(props: Props) {
|
||||||
data-floating-page={props.show ? "true" : undefined}
|
data-floating-page={props.show ? "true" : undefined}
|
||||||
style={{
|
style={{
|
||||||
height: props.height ? `${props.height}px` : undefined,
|
height: props.height ? `${props.height}px` : undefined,
|
||||||
|
maxHeight: "70vh",
|
||||||
width: props.width ? width : undefined,
|
width: props.width ? width : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -21,8 +21,20 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const bind = useDrag(
|
const bind = useDrag(
|
||||||
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => {
|
({
|
||||||
|
last,
|
||||||
|
velocity: [, vy],
|
||||||
|
direction: [, dy],
|
||||||
|
movement: [, my],
|
||||||
|
...event
|
||||||
|
}) => {
|
||||||
if (closing.current) return;
|
if (closing.current) return;
|
||||||
|
|
||||||
|
const isInScrollable = (event.target as HTMLDivElement).closest(
|
||||||
|
".overflow-y-auto"
|
||||||
|
);
|
||||||
|
if (isInScrollable) return; // Don't attempt to swipe the thing away if it's a scroll area unless the scroll area is at the top and the user is swiping down
|
||||||
|
|
||||||
const height = cardRect?.height ?? 0;
|
const height = cardRect?.height ?? 0;
|
||||||
if (last) {
|
if (last) {
|
||||||
// if past half height downwards
|
// if past half height downwards
|
||||||
|
@ -69,7 +81,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-x-0 mx-auto max-w-[400px] origin-bottom-left touch-none"
|
className="is-mobile-view absolute inset-x-0 mx-auto max-w-[400px] origin-bottom-left touch-none"
|
||||||
style={{
|
style={{
|
||||||
transform: `translateY(${
|
transform: `translateY(${
|
||||||
window.innerHeight - (cardRect?.height ?? 0) + 200
|
window.innerHeight - (cardRect?.height ?? 0) + 200
|
||||||
|
|
|
@ -38,6 +38,7 @@ body[data-no-select] {
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
@ -55,6 +56,10 @@ body[data-no-select] {
|
||||||
@apply brightness-[500];
|
@apply brightness-[500];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-mobile-view .overflow-y-auto {
|
||||||
|
height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
/*generated with Input range slider CSS style generator (version 20211225)
|
/*generated with Input range slider CSS style generator (version 20211225)
|
||||||
https://toughengineer.github.io/demo/slider-styler*/
|
https://toughengineer.github.io/demo/slider-styler*/
|
||||||
:root {
|
:root {
|
||||||
|
@ -62,6 +67,7 @@ https://toughengineer.github.io/demo/slider-styler*/
|
||||||
--slider-border-radius: 1em;
|
--slider-border-radius: 1em;
|
||||||
--slider-progress-background: #8652bb;
|
--slider-progress-background: #8652bb;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range].styled-slider {
|
input[type=range].styled-slider {
|
||||||
height: var(--slider-height);
|
height: var(--slider-height);
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
@ -101,7 +107,7 @@ input[type=range].styled-slider::-webkit-slider-thumb:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range].styled-slider.slider-progress::-webkit-slider-runnable-track {
|
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;
|
background: linear-gradient(var(--slider-progress-background), var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*mozilla*/
|
/*mozilla*/
|
||||||
|
@ -127,7 +133,7 @@ input[type=range].styled-slider::-moz-range-thumb:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range].styled-slider.slider-progress::-moz-range-track {
|
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;
|
background: linear-gradient(var(--slider-progress-background), var(--slider-progress-background)) 0/var(--sx) 100% no-repeat, #1C161B;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*ms*/
|
/*ms*/
|
||||||
|
|
|
@ -63,12 +63,15 @@
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"pictureInPicture": "Picture in Picture"
|
"pictureInPicture": "Picture in Picture",
|
||||||
|
"playbackSpeed": "Playback speed"
|
||||||
},
|
},
|
||||||
"popouts": {
|
"popouts": {
|
||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
|
"playbackSpeed": "Playback speed",
|
||||||
|
"customPlaybackSpeed": "Custom playback speed",
|
||||||
"captionPreferences": {
|
"captionPreferences": {
|
||||||
"title": "Customize",
|
"title": "Customize",
|
||||||
"delay": "Delay",
|
"delay": "Delay",
|
||||||
|
@ -82,6 +85,7 @@
|
||||||
"customCaption": "Custom caption",
|
"customCaption": "Custom caption",
|
||||||
"uploadCustomCaption": "Upload caption",
|
"uploadCustomCaption": "Upload caption",
|
||||||
"noEmbeds": "No embeds were found for this source",
|
"noEmbeds": "No embeds were found for this source",
|
||||||
|
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
|
||||||
"embedsError": "Something went wrong loading the embeds for this thing that you like"
|
"embedsError": "Something went wrong loading the embeds for this thing that you like"
|
||||||
|
@ -92,7 +96,8 @@
|
||||||
"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"
|
"captionPreferences": "Make subtitles look how you want it",
|
||||||
|
"playbackSpeed": "Change the playback speed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { PopoutListAction } from "../../popouts/PopoutUtils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClick: () => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlaybackSpeedSelectionAction(props: Props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoutListAction icon={Icons.TACHOMETER} onClick={props.onClick}>
|
||||||
|
{t("videoPlayer.buttons.playbackSpeed")}
|
||||||
|
</PopoutListAction>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,8 +3,8 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary";
|
||||||
import { Link } from "@/components/text/Link";
|
import { Link } from "@/components/text/Link";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import { Component } from "react";
|
import { Component } from "react";
|
||||||
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
|
||||||
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
import { VideoPlayerHeader } from "./VideoPlayerHeader";
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
|
|
|
@ -3,52 +3,9 @@ import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChangeEventHandler, useEffect, useRef } from "react";
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { Slider } from "@/components/Slider";
|
||||||
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: {
|
export function CaptionSettingsPopout(props: {
|
||||||
router: ReturnType<typeof useFloatingRouter>;
|
router: ReturnType<typeof useFloatingRouter>;
|
||||||
|
@ -73,7 +30,7 @@ export function CaptionSettingsPopout(props: {
|
||||||
/>
|
/>
|
||||||
<FloatingCardView.Content>
|
<FloatingCardView.Content>
|
||||||
<Slider
|
<Slider
|
||||||
label={t("videoPlayer.popouts.captionPreferences.delay")}
|
label={t("videoPlayer.popouts.captionPreferences.delay") as string}
|
||||||
max={10}
|
max={10}
|
||||||
min={-10}
|
min={-10}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
|
@ -90,7 +47,7 @@ export function CaptionSettingsPopout(props: {
|
||||||
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
label={t("videoPlayer.popouts.captionPreferences.opacity")}
|
label={t("videoPlayer.popouts.captionPreferences.opacity") as string}
|
||||||
step={1}
|
step={1}
|
||||||
min={0}
|
min={0}
|
||||||
max={255}
|
max={255}
|
||||||
|
|
73
src/video/components/popouts/PlaybackSpeedPopout.tsx
Normal file
73
src/video/components/popouts/PlaybackSpeedPopout.tsx
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||||
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMediaPlaying } from "@/video/state/logic/mediaplaying";
|
||||||
|
import { Slider } from "@/components/Slider";
|
||||||
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
|
const speedSelectionOptions = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
|
||||||
|
|
||||||
|
export function PlaybackSpeedPopout(props: {
|
||||||
|
router: ReturnType<typeof useFloatingRouter>;
|
||||||
|
prefix: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
const mediaPlaying = useMediaPlaying(descriptor);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FloatingView
|
||||||
|
{...props.router.pageProps(props.prefix)}
|
||||||
|
width={320}
|
||||||
|
height={500}
|
||||||
|
>
|
||||||
|
<FloatingCardView.Header
|
||||||
|
title={t("videoPlayer.popouts.playbackSpeed")}
|
||||||
|
description={t("videoPlayer.popouts.descriptions.playbackSpeed")}
|
||||||
|
goBack={() => props.router.navigate("/")}
|
||||||
|
/>
|
||||||
|
<FloatingCardView.Content noSection>
|
||||||
|
<PopoutSection>
|
||||||
|
{speedSelectionOptions.map((speed) => (
|
||||||
|
<PopoutListEntry
|
||||||
|
key={speed}
|
||||||
|
active={mediaPlaying.playbackSpeed === speed}
|
||||||
|
onClick={() => {
|
||||||
|
controls.setPlaybackSpeed(speed);
|
||||||
|
controls.closePopout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{speed}x
|
||||||
|
</PopoutListEntry>
|
||||||
|
))}
|
||||||
|
</PopoutSection>
|
||||||
|
|
||||||
|
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-300 px-5 py-3 text-xs font-bold uppercase">
|
||||||
|
<Icon className="text-base" icon={Icons.TACHOMETER} />
|
||||||
|
<span>{t("videoPlayer.popouts.customPlaybackSpeed")}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<PopoutSection className="pt-0">
|
||||||
|
<div>
|
||||||
|
<Slider
|
||||||
|
min={0.1}
|
||||||
|
max={10}
|
||||||
|
step={0.1}
|
||||||
|
value={mediaPlaying.playbackSpeed}
|
||||||
|
valueDisplay={`${mediaPlaying.playbackSpeed}x`}
|
||||||
|
onChange={(e: { target: { valueAsNumber: number } }) =>
|
||||||
|
controls.setPlaybackSpeed(e.target.valueAsNumber)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PopoutSection>
|
||||||
|
</FloatingCardView.Content>
|
||||||
|
</FloatingView>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,9 +5,11 @@ import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
|
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
|
||||||
import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction";
|
import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction";
|
||||||
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
||||||
|
import { PlaybackSpeedSelectionAction } from "@/video/components/actions/list-entries/PlaybackSpeedSelectionAction";
|
||||||
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
||||||
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
||||||
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
||||||
|
import { PlaybackSpeedPopout } from "./PlaybackSpeedPopout";
|
||||||
|
|
||||||
export function SettingsPopout() {
|
export function SettingsPopout() {
|
||||||
const floatingRouter = useFloatingRouter();
|
const floatingRouter = useFloatingRouter();
|
||||||
|
@ -21,6 +23,9 @@ export function SettingsPopout() {
|
||||||
<DownloadAction />
|
<DownloadAction />
|
||||||
<SourceSelectionAction onClick={() => navigate("/source")} />
|
<SourceSelectionAction onClick={() => navigate("/source")} />
|
||||||
<CaptionsSelectionAction onClick={() => navigate("/captions")} />
|
<CaptionsSelectionAction onClick={() => navigate("/captions")} />
|
||||||
|
<PlaybackSpeedSelectionAction
|
||||||
|
onClick={() => navigate("/playback-speed")}
|
||||||
|
/>
|
||||||
</FloatingCardView.Content>
|
</FloatingCardView.Content>
|
||||||
</FloatingView>
|
</FloatingView>
|
||||||
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
||||||
|
@ -29,6 +34,7 @@ export function SettingsPopout() {
|
||||||
router={floatingRouter}
|
router={floatingRouter}
|
||||||
prefix="caption-settings"
|
prefix="caption-settings"
|
||||||
/>
|
/>
|
||||||
|
<PlaybackSpeedPopout router={floatingRouter} prefix="playback-speed" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export function resetForSource(s: VideoPlayerState) {
|
||||||
isFirstLoading: true,
|
isFirstLoading: true,
|
||||||
hasPlayedOnce: false,
|
hasPlayedOnce: false,
|
||||||
volume: state.mediaPlaying.volume, // volume settings needs to persist through resets
|
volume: state.mediaPlaying.volume, // volume settings needs to persist through resets
|
||||||
|
playbackSpeed: 1,
|
||||||
};
|
};
|
||||||
state.progress = {
|
state.progress = {
|
||||||
time: 0,
|
time: 0,
|
||||||
|
@ -42,6 +43,7 @@ function initPlayer(): VideoPlayerState {
|
||||||
isFirstLoading: true,
|
isFirstLoading: true,
|
||||||
hasPlayedOnce: false,
|
hasPlayedOnce: false,
|
||||||
volume: 0,
|
volume: 0,
|
||||||
|
playbackSpeed: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
progress: {
|
progress: {
|
||||||
|
|
|
@ -14,6 +14,7 @@ export type ControlMethods = {
|
||||||
setCurrentEpisode(sId: string, eId: string): void;
|
setCurrentEpisode(sId: string, eId: string): void;
|
||||||
setDraggingTime(num: number): void;
|
setDraggingTime(num: number): void;
|
||||||
togglePictureInPicture(): void;
|
togglePictureInPicture(): void;
|
||||||
|
setPlaybackSpeed(num: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useControls(
|
export function useControls(
|
||||||
|
@ -105,5 +106,9 @@ export function useControls(
|
||||||
state.stateProvider?.togglePictureInPicture();
|
state.stateProvider?.togglePictureInPicture();
|
||||||
updateInterface(descriptor, state);
|
updateInterface(descriptor, state);
|
||||||
},
|
},
|
||||||
|
setPlaybackSpeed(num) {
|
||||||
|
state.stateProvider?.setPlaybackSpeed(num);
|
||||||
|
updateInterface(descriptor, state);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export type VideoMediaPlayingEvent = {
|
||||||
hasPlayedOnce: boolean;
|
hasPlayedOnce: boolean;
|
||||||
isFirstLoading: boolean;
|
isFirstLoading: boolean;
|
||||||
volume: number;
|
volume: number;
|
||||||
|
playbackSpeed: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMediaPlayingFromState(
|
function getMediaPlayingFromState(
|
||||||
|
@ -26,6 +27,7 @@ function getMediaPlayingFromState(
|
||||||
isDragSeeking: state.mediaPlaying.isDragSeeking,
|
isDragSeeking: state.mediaPlaying.isDragSeeking,
|
||||||
isFirstLoading: state.mediaPlaying.isFirstLoading,
|
isFirstLoading: state.mediaPlaying.isFirstLoading,
|
||||||
volume: state.mediaPlaying.volume,
|
volume: state.mediaPlaying.volume,
|
||||||
|
playbackSpeed: state.mediaPlaying.playbackSpeed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,23 @@ export function createCastingStateProvider(
|
||||||
togglePictureInPicture() {
|
togglePictureInPicture() {
|
||||||
// no picture in picture while casting
|
// no picture in picture while casting
|
||||||
},
|
},
|
||||||
|
setPlaybackSpeed(num) {
|
||||||
|
const mediaInfo = new chrome.cast.media.MediaInfo(
|
||||||
|
state.meta?.meta.meta.id ?? "video",
|
||||||
|
"video/mp4"
|
||||||
|
);
|
||||||
|
(mediaInfo as any).contentUrl = state.source?.url;
|
||||||
|
mediaInfo.streamType = chrome.cast.media.StreamType.BUFFERED;
|
||||||
|
mediaInfo.metadata = new chrome.cast.media.MovieMediaMetadata();
|
||||||
|
mediaInfo.metadata.title = state.meta?.meta.meta.title ?? "";
|
||||||
|
mediaInfo.customData = {
|
||||||
|
playbackRate: num,
|
||||||
|
};
|
||||||
|
const request = new chrome.cast.media.LoadRequest(mediaInfo);
|
||||||
|
request.autoplay = true;
|
||||||
|
const session = ins?.getCurrentSession();
|
||||||
|
session?.loadMedia(request);
|
||||||
|
},
|
||||||
async setVolume(v) {
|
async setVolume(v) {
|
||||||
// clamp time between 0 and 1
|
// clamp time between 0 and 1
|
||||||
let volume = Math.min(v, 1);
|
let volume = Math.min(v, 1);
|
||||||
|
@ -114,7 +131,7 @@ export function createCastingStateProvider(
|
||||||
movieMeta.title = state.meta?.meta.meta.title ?? "";
|
movieMeta.title = state.meta?.meta.meta.title ?? "";
|
||||||
|
|
||||||
const mediaInfo = new chrome.cast.media.MediaInfo(
|
const mediaInfo = new chrome.cast.media.MediaInfo(
|
||||||
state.meta?.meta.meta.id ?? "hello",
|
state.meta?.meta.meta.id ?? "video",
|
||||||
"video/mp4"
|
"video/mp4"
|
||||||
);
|
);
|
||||||
(mediaInfo as any).contentUrl = source?.source;
|
(mediaInfo as any).contentUrl = source?.source;
|
||||||
|
|
|
@ -22,6 +22,7 @@ export type VideoPlayerStateController = {
|
||||||
clearCaption(): void;
|
clearCaption(): void;
|
||||||
getId(): string;
|
getId(): string;
|
||||||
togglePictureInPicture(): void;
|
togglePictureInPicture(): void;
|
||||||
|
setPlaybackSpeed(num: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
||||||
|
|
|
@ -228,6 +228,11 @@ export function createVideoStateProvider(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setPlaybackSpeed(num) {
|
||||||
|
player.playbackRate = num;
|
||||||
|
state.mediaPlaying.playbackSpeed = num;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
},
|
||||||
providerStart() {
|
providerStart() {
|
||||||
this.setVolume(getStoredVolume());
|
this.setVolume(getStoredVolume());
|
||||||
|
|
||||||
|
@ -276,6 +281,10 @@ export function createVideoStateProvider(
|
||||||
state.mediaPlaying.isLoading = false;
|
state.mediaPlaying.isLoading = false;
|
||||||
updateMediaPlaying(descriptor, state);
|
updateMediaPlaying(descriptor, state);
|
||||||
};
|
};
|
||||||
|
const ratechange = () => {
|
||||||
|
state.mediaPlaying.playbackSpeed = player.playbackRate;
|
||||||
|
updateMediaPlaying(descriptor, state);
|
||||||
|
};
|
||||||
const fullscreenchange = () => {
|
const fullscreenchange = () => {
|
||||||
state.interface.isFullscreen =
|
state.interface.isFullscreen =
|
||||||
!!document.fullscreenElement || // other browsers
|
!!document.fullscreenElement || // other browsers
|
||||||
|
@ -326,6 +335,7 @@ export function createVideoStateProvider(
|
||||||
player.addEventListener("timeupdate", timeupdate);
|
player.addEventListener("timeupdate", timeupdate);
|
||||||
player.addEventListener("loadedmetadata", loadedmetadata);
|
player.addEventListener("loadedmetadata", loadedmetadata);
|
||||||
player.addEventListener("canplay", canplay);
|
player.addEventListener("canplay", canplay);
|
||||||
|
player.addEventListener("ratechange", ratechange);
|
||||||
fscreen.addEventListener("fullscreenchange", fullscreenchange);
|
fscreen.addEventListener("fullscreenchange", fullscreenchange);
|
||||||
player.addEventListener("error", error);
|
player.addEventListener("error", error);
|
||||||
player.addEventListener(
|
player.addEventListener(
|
||||||
|
|
|
@ -42,6 +42,7 @@ export type VideoPlayerState = {
|
||||||
isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing
|
isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing
|
||||||
hasPlayedOnce: boolean; // has the video played at all?
|
hasPlayedOnce: boolean; // has the video played at all?
|
||||||
volume: number;
|
volume: number;
|
||||||
|
playbackSpeed: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// state related to video progress
|
// state related to video progress
|
||||||
|
|
Loading…
Reference in a new issue