diff --git a/package.json b/package.json index f82c0c00..32cb361f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.0.4", + "version": "3.0.5", "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": { diff --git a/public/_headers b/public/_headers new file mode 100644 index 00000000..1216e42d --- /dev/null +++ b/public/_headers @@ -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 diff --git a/public/config.js b/public/config.js index eb936081..b69f60eb 100644 --- a/public/config.js +++ b/public/config.js @@ -1,7 +1,6 @@ window.__CONFIG__ = { // url must NOT end with a slash VITE_CORS_PROXY_URL: "", - VITE_TMDB_API_KEY: "b030404650f279792a8d3287232358e3", VITE_OMDB_API_KEY: "aa0937c0", }; diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index cb622e3b..c0c9e92c 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -54,12 +54,17 @@ export async function getMetaFromId( throw err; } - const imdbId = data.external_ids.find( + let imdbId = data.external_ids.find( (v) => v.provider === "imdb_latest" )?.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" )?.external_id; + if (!tmdbId) + tmdbId = data.external_ids.find((v) => v.provider === "tmdb")?.external_id; if (!imdbId || !tmdbId) throw new Error("not enough info"); diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index a41f5374..31c09b16 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -37,6 +37,7 @@ export enum Icons { CIRCLE_EXCLAMATION = "circle_exclamation", DOWNLOAD = "download", SETTINGS = "settings", + PICTURE_IN_PICTURE = "pictureInPicture", } export interface IconProps { @@ -81,6 +82,7 @@ const iconList: Record = { casting: "", download: ``, settings: ``, + pictureInPicture: ``, }; function ChromeCastButton() { diff --git a/src/setup/constants.ts b/src/setup/constants.ts index db766055..8350efdd 100644 --- a/src/setup/constants.ts +++ b/src/setup/constants.ts @@ -1,4 +1,4 @@ export const DISCORD_LINK = "https://discord.gg/Jhqt4Xzpfb"; 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"; diff --git a/src/setup/index.css b/src/setup/index.css index c3d97c26..ebb56bec 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -4,12 +4,13 @@ html, 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: 100dvh; } -html[data-full], html[data-full] body { +html[data-full], +html[data-full] body { overscroll-behavior-y: none; } @@ -46,7 +47,7 @@ body[data-no-select] { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; - -webkit-box-orient: vertical; + -webkit-box-orient: vertical; overflow: hidden; } diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index 78832ad1..daf248c1 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -61,7 +61,8 @@ "episodes": "Episodes", "source": "Source", "captions": "Captions", - "download": "Download" + "download": "Download", + "pictureInPicture": "Picture in Picture" }, "popouts": { "sources": "Sources", diff --git a/src/utils/detectFeatures.ts b/src/utils/detectFeatures.ts index 15be4c69..af62720d 100644 --- a/src/utils/detectFeatures.ts +++ b/src/utils/detectFeatures.ts @@ -38,3 +38,11 @@ export function canWebkitFullscreen(): boolean { export function canFullscreen(): boolean { return canFullscreenAnyElement() || canWebkitFullscreen(); } + +export function canPictureInPicture(): boolean { + return "pictureInPictureEnabled" in document; +} + +export function canWebkitPictureInPicture(): boolean { + return "webkitSupportsPresentationMode" in document.createElement("video"); +} diff --git a/src/video/components/VideoPlayer.tsx b/src/video/components/VideoPlayer.tsx index c41734ed..158877dd 100644 --- a/src/video/components/VideoPlayer.tsx +++ b/src/video/components/VideoPlayer.tsx @@ -31,6 +31,7 @@ import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderA import { ChromecastAction } from "@/video/components/actions/ChromecastAction"; import { CastingTextAction } from "@/video/components/actions/CastingTextAction"; import { DownloadAction } from "@/video/components/actions/DownloadAction"; +import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction"; import { CaptionRenderer } from "./CaptionRenderer"; type Props = VideoPlayerBaseProps; @@ -145,6 +146,7 @@ export function VideoPlayer(props: Props) {
+ @@ -162,6 +164,7 @@ export function VideoPlayer(props: Props) { + diff --git a/src/video/components/actions/DownloadAction.tsx b/src/video/components/actions/DownloadAction.tsx index 10d59ce5..307f14c7 100644 --- a/src/video/components/actions/DownloadAction.tsx +++ b/src/video/components/actions/DownloadAction.tsx @@ -28,7 +28,7 @@ export function DownloadAction(props: Props) { href={isHLS ? undefined : sourceInterface.source?.url} rel="noreferrer" target="_blank" - download={title ? normalizeTitle(title) : undefined} + download={title ? `${normalizeTitle(title)}.mp4` : undefined} > { + controls.togglePictureInPicture(); + }, [controls]); + + if (!canPictureInPicture() && !canWebkitPictureInPicture()) return null; + + return ( + + ); +} diff --git a/src/video/state/logic/controls.ts b/src/video/state/logic/controls.ts index 56c89a10..f21ec05a 100644 --- a/src/video/state/logic/controls.ts +++ b/src/video/state/logic/controls.ts @@ -13,6 +13,7 @@ export type ControlMethods = { setMeta(data?: VideoPlayerMeta): void; setCurrentEpisode(sId: string, eId: string): void; setDraggingTime(num: number): void; + togglePictureInPicture(): void; }; export function useControls( @@ -100,5 +101,9 @@ export function useControls( updateMeta(descriptor, state); } }, + togglePictureInPicture() { + state.stateProvider?.togglePictureInPicture(); + updateInterface(descriptor, state); + }, }; } diff --git a/src/video/state/providers/castingStateProvider.ts b/src/video/state/providers/castingStateProvider.ts index 961a616d..ab111f37 100644 --- a/src/video/state/providers/castingStateProvider.ts +++ b/src/video/state/providers/castingStateProvider.ts @@ -84,6 +84,9 @@ export function createCastingStateProvider( state.pausedWhenSeeking = state.mediaPlaying.isPaused; this.pause(); }, + togglePictureInPicture() { + // no picture in picture while casting + }, async setVolume(v) { // clamp time between 0 and 1 let volume = Math.min(v, 1); diff --git a/src/video/state/providers/providerTypes.ts b/src/video/state/providers/providerTypes.ts index e34b950d..1643a980 100644 --- a/src/video/state/providers/providerTypes.ts +++ b/src/video/state/providers/providerTypes.ts @@ -19,6 +19,7 @@ export type VideoPlayerStateController = { setCaption(id: string, url: string): void; clearCaption(): void; getId(): string; + togglePictureInPicture(): void; }; export type VideoPlayerStateProvider = VideoPlayerStateController & { diff --git a/src/video/state/providers/videoStateProvider.ts b/src/video/state/providers/videoStateProvider.ts index e5fbaa38..0f0971b5 100644 --- a/src/video/state/providers/videoStateProvider.ts +++ b/src/video/state/providers/videoStateProvider.ts @@ -5,6 +5,8 @@ import { canFullscreen, canFullscreenAnyElement, canWebkitFullscreen, + canPictureInPicture, + canWebkitPictureInPicture, } from "@/utils/detectFeatures"; import { MWStreamType } from "@/backend/helpers/streams"; import { updateInterface } from "@/video/state/logic/interface"; @@ -207,6 +209,23 @@ export function createVideoStateProvider( 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() { this.setVolume(getStoredVolume());