mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 22:55:07 +01:00
Merge branch 'dev' of https://github.com/frost768/movie-web into feat/subtitle-rendering
This commit is contained in:
commit
315c3de3ab
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "movie-web",
|
"name": "movie-web",
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "https://movie.squeezebox.dev",
|
"homepage": "https://movie.squeezebox.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
5
public/_headers
Normal file
5
public/_headers
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
Referrer-Policy: origin-when-cross-origin
|
@ -1,7 +1,6 @@
|
|||||||
window.__CONFIG__ = {
|
window.__CONFIG__ = {
|
||||||
// url must NOT end with a slash
|
// url must NOT end with a slash
|
||||||
VITE_CORS_PROXY_URL: "",
|
VITE_CORS_PROXY_URL: "",
|
||||||
|
|
||||||
VITE_TMDB_API_KEY: "b030404650f279792a8d3287232358e3",
|
VITE_TMDB_API_KEY: "b030404650f279792a8d3287232358e3",
|
||||||
VITE_OMDB_API_KEY: "aa0937c0",
|
VITE_OMDB_API_KEY: "aa0937c0",
|
||||||
};
|
};
|
||||||
|
@ -54,12 +54,17 @@ export async function getMetaFromId(
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imdbId = data.external_ids.find(
|
let imdbId = data.external_ids.find(
|
||||||
(v) => v.provider === "imdb_latest"
|
(v) => v.provider === "imdb_latest"
|
||||||
)?.external_id;
|
)?.external_id;
|
||||||
const tmdbId = data.external_ids.find(
|
if (!imdbId)
|
||||||
|
imdbId = data.external_ids.find((v) => v.provider === "imdb")?.external_id;
|
||||||
|
|
||||||
|
let tmdbId = data.external_ids.find(
|
||||||
(v) => v.provider === "tmdb_latest"
|
(v) => v.provider === "tmdb_latest"
|
||||||
)?.external_id;
|
)?.external_id;
|
||||||
|
if (!tmdbId)
|
||||||
|
tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id;
|
||||||
|
|
||||||
if (!imdbId || !tmdbId) throw new Error("not enough info");
|
if (!imdbId || !tmdbId) throw new Error("not enough info");
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ export enum Icons {
|
|||||||
CIRCLE_EXCLAMATION = "circle_exclamation",
|
CIRCLE_EXCLAMATION = "circle_exclamation",
|
||||||
DOWNLOAD = "download",
|
DOWNLOAD = "download",
|
||||||
SETTINGS = "settings",
|
SETTINGS = "settings",
|
||||||
|
PICTURE_IN_PICTURE = "pictureInPicture",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
@ -81,6 +82,7 @@ const iconList: Record<Icons, string> = {
|
|||||||
casting: "",
|
casting: "",
|
||||||
download: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
download: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`,
|
||||||
settings: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="white" stroke="currentColor"><!--! 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>`,
|
settings: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512" fill="white" stroke="currentColor"><!--! 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>`,
|
||||||
|
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>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChromeCastButton() {
|
function ChromeCastButton() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
|
export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb";
|
||||||
export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
|
export const GITHUB_LINK = "https://github.com/movie-web/movie-web";
|
||||||
export const APP_VERSION = "3.0.4";
|
export const APP_VERSION = "3.0.5";
|
||||||
export const GA_ID = "G-44YVXRL61C";
|
export const GA_ID = "G-44YVXRL61C";
|
||||||
|
@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
@apply bg-denim-100 text-denim-700 font-open-sans overflow-x-hidden;
|
@apply bg-denim-100 font-open-sans text-denim-700 overflow-x-hidden;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-full], html[data-full] body {
|
html[data-full],
|
||||||
|
html[data-full] body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ body[data-no-select] {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,8 @@
|
|||||||
"episodes": "Episodes",
|
"episodes": "Episodes",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"pictureInPicture": "Picture in Picture"
|
||||||
},
|
},
|
||||||
"popouts": {
|
"popouts": {
|
||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
|
@ -38,3 +38,11 @@ export function canWebkitFullscreen(): boolean {
|
|||||||
export function canFullscreen(): boolean {
|
export function canFullscreen(): boolean {
|
||||||
return canFullscreenAnyElement() || canWebkitFullscreen();
|
return canFullscreenAnyElement() || canWebkitFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canPictureInPicture(): boolean {
|
||||||
|
return "pictureInPictureEnabled" in document;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canWebkitPictureInPicture(): boolean {
|
||||||
|
return "webkitSupportsPresentationMode" in document.createElement("video");
|
||||||
|
}
|
||||||
|
@ -31,6 +31,7 @@ import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderA
|
|||||||
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 { DownloadAction } from "@/video/components/actions/DownloadAction";
|
import { DownloadAction } from "@/video/components/actions/DownloadAction";
|
||||||
|
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||||
import { CaptionRenderer } from "./CaptionRenderer";
|
import { CaptionRenderer } from "./CaptionRenderer";
|
||||||
|
|
||||||
type Props = VideoPlayerBaseProps;
|
type Props = VideoPlayerBaseProps;
|
||||||
@ -145,6 +146,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
<div />
|
<div />
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<DownloadAction />
|
<DownloadAction />
|
||||||
|
<PictureInPictureAction />
|
||||||
<CaptionsSelectionAction />
|
<CaptionsSelectionAction />
|
||||||
<SeriesSelectionAction />
|
<SeriesSelectionAction />
|
||||||
<SourceSelectionAction />
|
<SourceSelectionAction />
|
||||||
@ -162,6 +164,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
<ChromecastAction />
|
<ChromecastAction />
|
||||||
<AirplayAction />
|
<AirplayAction />
|
||||||
<DownloadAction />
|
<DownloadAction />
|
||||||
|
<PictureInPictureAction />
|
||||||
<CaptionsSelectionAction />
|
<CaptionsSelectionAction />
|
||||||
<FullscreenAction />
|
<FullscreenAction />
|
||||||
</>
|
</>
|
||||||
|
@ -28,7 +28,7 @@ export function DownloadAction(props: Props) {
|
|||||||
href={isHLS ? undefined : sourceInterface.source?.url}
|
href={isHLS ? undefined : sourceInterface.source?.url}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
download={title ? normalizeTitle(title) : undefined}
|
download={title ? `${normalizeTitle(title)}.mp4` : undefined}
|
||||||
>
|
>
|
||||||
<VideoPlayerIconButton
|
<VideoPlayerIconButton
|
||||||
className={props.className}
|
className={props.className}
|
||||||
|
40
src/video/components/actions/PictureInPictureAction.tsx
Normal file
40
src/video/components/actions/PictureInPictureAction.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import {
|
||||||
|
canPictureInPicture,
|
||||||
|
canWebkitPictureInPicture,
|
||||||
|
} from "@/utils/detectFeatures";
|
||||||
|
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PictureInPictureAction(props: Props) {
|
||||||
|
const { isMobile } = useIsMobile();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const descriptor = useVideoPlayerDescriptor();
|
||||||
|
const controls = useControls(descriptor);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
controls.togglePictureInPicture();
|
||||||
|
}, [controls]);
|
||||||
|
|
||||||
|
if (!canPictureInPicture() && !canWebkitPictureInPicture()) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerIconButton
|
||||||
|
className={props.className}
|
||||||
|
icon={Icons.PICTURE_IN_PICTURE}
|
||||||
|
onClick={handleClick}
|
||||||
|
text={
|
||||||
|
isMobile ? (t("videoPlayer.buttons.pictureInPicture") as string) : ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -13,6 +13,7 @@ export type ControlMethods = {
|
|||||||
setMeta(data?: VideoPlayerMeta): void;
|
setMeta(data?: VideoPlayerMeta): void;
|
||||||
setCurrentEpisode(sId: string, eId: string): void;
|
setCurrentEpisode(sId: string, eId: string): void;
|
||||||
setDraggingTime(num: number): void;
|
setDraggingTime(num: number): void;
|
||||||
|
togglePictureInPicture(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useControls(
|
export function useControls(
|
||||||
@ -100,5 +101,9 @@ export function useControls(
|
|||||||
updateMeta(descriptor, state);
|
updateMeta(descriptor, state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
togglePictureInPicture() {
|
||||||
|
state.stateProvider?.togglePictureInPicture();
|
||||||
|
updateInterface(descriptor, state);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,9 @@ export function createCastingStateProvider(
|
|||||||
state.pausedWhenSeeking = state.mediaPlaying.isPaused;
|
state.pausedWhenSeeking = state.mediaPlaying.isPaused;
|
||||||
this.pause();
|
this.pause();
|
||||||
},
|
},
|
||||||
|
togglePictureInPicture() {
|
||||||
|
// no picture in picture while casting
|
||||||
|
},
|
||||||
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);
|
||||||
|
@ -19,6 +19,7 @@ export type VideoPlayerStateController = {
|
|||||||
setCaption(id: string, url: string): void;
|
setCaption(id: string, url: string): void;
|
||||||
clearCaption(): void;
|
clearCaption(): void;
|
||||||
getId(): string;
|
getId(): string;
|
||||||
|
togglePictureInPicture(): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
export type VideoPlayerStateProvider = VideoPlayerStateController & {
|
||||||
|
@ -5,6 +5,8 @@ import {
|
|||||||
canFullscreen,
|
canFullscreen,
|
||||||
canFullscreenAnyElement,
|
canFullscreenAnyElement,
|
||||||
canWebkitFullscreen,
|
canWebkitFullscreen,
|
||||||
|
canPictureInPicture,
|
||||||
|
canWebkitPictureInPicture,
|
||||||
} from "@/utils/detectFeatures";
|
} from "@/utils/detectFeatures";
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { updateInterface } from "@/video/state/logic/interface";
|
import { updateInterface } from "@/video/state/logic/interface";
|
||||||
@ -207,6 +209,23 @@ export function createVideoStateProvider(
|
|||||||
updateSource(descriptor, state);
|
updateSource(descriptor, state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
togglePictureInPicture() {
|
||||||
|
if (canWebkitPictureInPicture()) {
|
||||||
|
const webkitPlayer = player as any;
|
||||||
|
webkitPlayer.webkitSetPresentationMode(
|
||||||
|
webkitPlayer.webkitPresentationMode === "picture-in-picture"
|
||||||
|
? "inline"
|
||||||
|
: "picture-in-picture"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (canPictureInPicture()) {
|
||||||
|
if (player !== document.pictureInPictureElement) {
|
||||||
|
player.requestPictureInPicture();
|
||||||
|
} else {
|
||||||
|
document.exitPictureInPicture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
providerStart() {
|
providerStart() {
|
||||||
this.setVolume(getStoredVolume());
|
this.setVolume(getStoredVolume());
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user