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

Merge branch 'dev' of https://github.com/movie-web/movie-web into subtitle-file-type-control

This commit is contained in:
frost768 2023-04-17 17:49:31 +03:00
commit a910c1c18c
20 changed files with 222 additions and 57 deletions

View file

@ -1,6 +1,6 @@
{
"name": "movie-web",
"version": "3.0.9",
"version": "3.0.10",
"private": true,
"homepage": "https://movie.squeezebox.dev",
"dependencies": {

View file

@ -22,6 +22,7 @@ registerProvider({
displayName: "NetFilm",
rank: 15,
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 }) {
if (!this.type.includes(media.meta.type)) {

View file

@ -40,6 +40,7 @@ export enum Icons {
WATCH_PARTY = "watch_party",
PICTURE_IN_PICTURE = "pictureInPicture",
CHECKMARK = "checkmark",
TACHOMETER = "tachometer",
}
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>`,
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>`,
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() {

47
src/components/Slider.tsx Normal file
View 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>
);
}

View file

@ -29,6 +29,7 @@ export function FloatingView(props: Props) {
data-floating-page={props.show ? "true" : undefined}
style={{
height: props.height ? `${props.height}px` : undefined,
maxHeight: "70vh",
width: props.width ? width : undefined,
}}
>

View file

@ -21,8 +21,20 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
}));
const bind = useDrag(
({ last, velocity: [, vy], direction: [, dy], movement: [, my] }) => {
({
last,
velocity: [, vy],
direction: [, dy],
movement: [, my],
...event
}) => {
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;
if (last) {
// if past half height downwards
@ -69,7 +81,7 @@ export function FloatingCardMobilePosition(props: MobilePositionProps) {
return (
<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={{
transform: `translateY(${
window.innerHeight - (cardRect?.height ?? 0) + 200

View file

@ -38,6 +38,7 @@ body[data-no-select] {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
@ -55,6 +56,10 @@ body[data-no-select] {
@apply brightness-[500];
}
.is-mobile-view .overflow-y-auto {
height: 60vh;
}
/*generated with Input range slider CSS style generator (version 20211225)
https://toughengineer.github.io/demo/slider-styler*/
:root {
@ -62,6 +67,7 @@ https://toughengineer.github.io/demo/slider-styler*/
--slider-border-radius: 1em;
--slider-progress-background: #8652bb;
}
input[type=range].styled-slider {
height: var(--slider-height);
-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 {
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*/
@ -127,7 +133,7 @@ input[type=range].styled-slider::-moz-range-thumb:hover {
}
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*/
@ -172,4 +178,4 @@ input[type=range].styled-slider.slider-progress::-ms-fill-lower {
background: var(--slider-progress-background);
border: none;
border-right-width: 0;
}
}

View file

@ -63,12 +63,15 @@
"captions": "Captions",
"download": "Download",
"settings": "Settings",
"pictureInPicture": "Picture in Picture"
"pictureInPicture": "Picture in Picture",
"playbackSpeed": "Playback speed"
},
"popouts": {
"sources": "Sources",
"seasons": "Seasons",
"captions": "Captions",
"playbackSpeed": "Playback speed",
"customPlaybackSpeed": "Custom playback speed",
"captionPreferences": {
"title": "Customize",
"delay": "Delay",
@ -82,6 +85,7 @@
"customCaption": "Custom caption",
"uploadCustomCaption": "Upload caption",
"noEmbeds": "No embeds were found for this source",
"errors": {
"loadingWentWong": "Something went wrong loading the episodes for {{seasonTitle}}",
"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",
"episode": "Pick an episode",
"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": {

View file

@ -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>
);
}

View file

@ -3,8 +3,8 @@ import { ErrorMessage } from "@/components/layout/ErrorBoundary";
import { Link } from "@/components/text/Link";
import { conf } from "@/setup/config";
import { Component } from "react";
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
import { Trans } from "react-i18next";
import type { ReactNode } from "react-router-dom/node_modules/@types/react/index";
import { VideoPlayerHeader } from "./VideoPlayerHeader";
interface ErrorBoundaryState {

View file

@ -3,52 +3,9 @@ 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>
);
}
import { Slider } from "@/components/Slider";
export function CaptionSettingsPopout(props: {
router: ReturnType<typeof useFloatingRouter>;
@ -73,7 +30,7 @@ export function CaptionSettingsPopout(props: {
/>
<FloatingCardView.Content>
<Slider
label={t("videoPlayer.popouts.captionPreferences.delay")}
label={t("videoPlayer.popouts.captionPreferences.delay") as string}
max={10}
min={-10}
step={0.1}
@ -90,7 +47,7 @@ export function CaptionSettingsPopout(props: {
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/>
<Slider
label={t("videoPlayer.popouts.captionPreferences.opacity")}
label={t("videoPlayer.popouts.captionPreferences.opacity") as string}
step={1}
min={0}
max={255}

View 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>
);
}

View file

@ -5,9 +5,11 @@ import { useFloatingRouter } from "@/hooks/useFloatingRouter";
import { DownloadAction } from "@/video/components/actions/list-entries/DownloadAction";
import { CaptionsSelectionAction } from "@/video/components/actions/list-entries/CaptionsSelectionAction";
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
import { PlaybackSpeedSelectionAction } from "@/video/components/actions/list-entries/PlaybackSpeedSelectionAction";
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
import { SourceSelectionPopout } from "./SourceSelectionPopout";
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
import { PlaybackSpeedPopout } from "./PlaybackSpeedPopout";
export function SettingsPopout() {
const floatingRouter = useFloatingRouter();
@ -21,6 +23,9 @@ export function SettingsPopout() {
<DownloadAction />
<SourceSelectionAction onClick={() => navigate("/source")} />
<CaptionsSelectionAction onClick={() => navigate("/captions")} />
<PlaybackSpeedSelectionAction
onClick={() => navigate("/playback-speed")}
/>
</FloatingCardView.Content>
</FloatingView>
<SourceSelectionPopout router={floatingRouter} prefix="source" />
@ -29,6 +34,7 @@ export function SettingsPopout() {
router={floatingRouter}
prefix="caption-settings"
/>
<PlaybackSpeedPopout router={floatingRouter} prefix="playback-speed" />
</>
);
}

View file

@ -13,6 +13,7 @@ export function resetForSource(s: VideoPlayerState) {
isFirstLoading: true,
hasPlayedOnce: false,
volume: state.mediaPlaying.volume, // volume settings needs to persist through resets
playbackSpeed: 1,
};
state.progress = {
time: 0,
@ -42,6 +43,7 @@ function initPlayer(): VideoPlayerState {
isFirstLoading: true,
hasPlayedOnce: false,
volume: 0,
playbackSpeed: 1,
},
progress: {

View file

@ -14,6 +14,7 @@ export type ControlMethods = {
setCurrentEpisode(sId: string, eId: string): void;
setDraggingTime(num: number): void;
togglePictureInPicture(): void;
setPlaybackSpeed(num: number): void;
};
export function useControls(
@ -105,5 +106,9 @@ export function useControls(
state.stateProvider?.togglePictureInPicture();
updateInterface(descriptor, state);
},
setPlaybackSpeed(num) {
state.stateProvider?.setPlaybackSpeed(num);
updateInterface(descriptor, state);
},
};
}

View file

@ -12,6 +12,7 @@ export type VideoMediaPlayingEvent = {
hasPlayedOnce: boolean;
isFirstLoading: boolean;
volume: number;
playbackSpeed: number;
};
function getMediaPlayingFromState(
@ -26,6 +27,7 @@ function getMediaPlayingFromState(
isDragSeeking: state.mediaPlaying.isDragSeeking,
isFirstLoading: state.mediaPlaying.isFirstLoading,
volume: state.mediaPlaying.volume,
playbackSpeed: state.mediaPlaying.playbackSpeed,
};
}

View file

@ -87,6 +87,23 @@ export function createCastingStateProvider(
togglePictureInPicture() {
// 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) {
// clamp time between 0 and 1
let volume = Math.min(v, 1);
@ -114,7 +131,7 @@ export function createCastingStateProvider(
movieMeta.title = state.meta?.meta.meta.title ?? "";
const mediaInfo = new chrome.cast.media.MediaInfo(
state.meta?.meta.meta.id ?? "hello",
state.meta?.meta.meta.id ?? "video",
"video/mp4"
);
(mediaInfo as any).contentUrl = source?.source;

View file

@ -22,6 +22,7 @@ export type VideoPlayerStateController = {
clearCaption(): void;
getId(): string;
togglePictureInPicture(): void;
setPlaybackSpeed(num: number): void;
};
export type VideoPlayerStateProvider = VideoPlayerStateController & {

View file

@ -228,6 +228,11 @@ export function createVideoStateProvider(
}
}
},
setPlaybackSpeed(num) {
player.playbackRate = num;
state.mediaPlaying.playbackSpeed = num;
updateMediaPlaying(descriptor, state);
},
providerStart() {
this.setVolume(getStoredVolume());
@ -276,6 +281,10 @@ export function createVideoStateProvider(
state.mediaPlaying.isLoading = false;
updateMediaPlaying(descriptor, state);
};
const ratechange = () => {
state.mediaPlaying.playbackSpeed = player.playbackRate;
updateMediaPlaying(descriptor, state);
};
const fullscreenchange = () => {
state.interface.isFullscreen =
!!document.fullscreenElement || // other browsers
@ -326,6 +335,7 @@ export function createVideoStateProvider(
player.addEventListener("timeupdate", timeupdate);
player.addEventListener("loadedmetadata", loadedmetadata);
player.addEventListener("canplay", canplay);
player.addEventListener("ratechange", ratechange);
fscreen.addEventListener("fullscreenchange", fullscreenchange);
player.addEventListener("error", error);
player.addEventListener(

View file

@ -42,6 +42,7 @@ export type VideoPlayerState = {
isFirstLoading: boolean; // first buffering of the video, when set to false the video can start playing
hasPlayedOnce: boolean; // has the video played at all?
volume: number;
playbackSpeed: number;
};
// state related to video progress