Merge pull request #157 from zisra/dev

Download button
This commit is contained in:
mrjvs 2023-02-24 22:35:46 +01:00 committed by GitHub
commit 281785a0ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 68 additions and 11 deletions

View File

@ -3,5 +3,5 @@ window.__CONFIG__ = {
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",
}; };

View File

@ -34,6 +34,7 @@ export enum Icons {
CAPTIONS = "captions", CAPTIONS = "captions",
LINK = "link", LINK = "link",
CASTING = "casting", CASTING = "casting",
DOWNLOAD = "download",
} }
export interface IconProps { export interface IconProps {
@ -75,6 +76,7 @@ const iconList: Record<Icons, string> = {
captions: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M0 96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM200 208c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48s21.5-48 48-48zm144 48c0-26.5 21.5-48 48-48c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48z"/></svg>`, captions: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path fill="currentColor" d="M0 96C0 60.7 28.7 32 64 32H512c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96zM200 208c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48s21.5-48 48-48zm144 48c0-26.5 21.5-48 48-48c14.2 0 27 6.1 35.8 16c8.8 9.9 24 10.7 33.9 1.9s10.7-24 1.9-33.9c-17.5-19.6-43.1-32-71.5-32c-53 0-96 43-96 96s43 96 96 96c28.4 0 54-12.4 71.5-32c8.8-9.9 8-25-1.9-33.9s-25-8-33.9 1.9c-8.8 9.9-21.6 16-35.8 16c-26.5 0-48-21.5-48-48z"/></svg>`,
link: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`, link: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
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>`,
}; };
function ChromeCastButton() { function ChromeCastButton() {

View File

@ -60,7 +60,8 @@
"buttons": { "buttons": {
"episodes": "Episodes", "episodes": "Episodes",
"source": "Source", "source": "Source",
"captions": "Captions" "captions": "Captions",
"download": "Download"
}, },
"popouts": { "popouts": {
"sources": "Sources", "sources": "Sources",

View File

@ -0,0 +1,7 @@
export function normalizeTitle(title: string): string {
return title
.trim()
.toLowerCase()
.replace(/['":]/g, "")
.replace(/[^a-zA-Z0-9]+/g, "_");
}

View File

@ -1,10 +1,4 @@
function normalizeTitle(title: string): string { import { normalizeTitle } from "./normalizeTitle";
return title
.trim()
.toLowerCase()
.replace(/['":]/g, "")
.replace(/[^a-zA-Z0-9]+/g, "_");
}
export function compareTitle(a: string, b: string): boolean { export function compareTitle(a: string, b: string): boolean {
return normalizeTitle(a) === normalizeTitle(b); return normalizeTitle(a) === normalizeTitle(b);

View File

@ -30,6 +30,7 @@ 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 { DownloadAction } from "@/video/components/actions/DownloadAction";
type Props = VideoPlayerBaseProps; type Props = VideoPlayerBaseProps;
@ -141,6 +142,7 @@ export function VideoPlayer(props: Props) {
<div className="grid w-full grid-cols-[56px,1fr,56px] items-center"> <div className="grid w-full grid-cols-[56px,1fr,56px] items-center">
<div /> <div />
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<DownloadAction />
<CaptionsSelectionAction /> <CaptionsSelectionAction />
<SeriesSelectionAction /> <SeriesSelectionAction />
<SourceSelectionAction /> <SourceSelectionAction />
@ -157,6 +159,7 @@ export function VideoPlayer(props: Props) {
<div className="mx-2 h-6 w-px bg-white opacity-50" /> <div className="mx-2 h-6 w-px bg-white opacity-50" />
<ChromecastAction /> <ChromecastAction />
<AirplayAction /> <AirplayAction />
<DownloadAction />
<CaptionsSelectionAction /> <CaptionsSelectionAction />
<FullscreenAction /> <FullscreenAction />
</> </>

View File

@ -0,0 +1,41 @@
import { Icons } from "@/components/Icon";
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
import { useSource } from "@/video/state/logic/source";
import { MWStreamType } from "@/backend/helpers/streams";
import { normalizeTitle } from "@/utils/normalizeTitle";
import { useIsMobile } from "@/hooks/useIsMobile";
import { useTranslation } from "react-i18next";
import { useMeta } from "@/video/state/logic/meta";
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton";
interface Props {
className?: string;
}
export function DownloadAction(props: Props) {
const descriptor = useVideoPlayerDescriptor();
const sourceInterface = useSource(descriptor);
const { isMobile } = useIsMobile();
const { t } = useTranslation();
const meta = useMeta(descriptor);
const isHLS = sourceInterface.source?.type === MWStreamType.HLS;
const title = meta?.meta.meta.title;
return (
<a
href={isHLS ? undefined : sourceInterface.source?.url}
rel="noreferrer"
target="_blank"
download={title ? normalizeTitle(title) : undefined}
>
<VideoPlayerIconButton
className={props.className}
icon={Icons.DOWNLOAD}
disabled={isHLS}
text={isMobile ? (t("videoPlayer.buttons.download") as string) : ""}
/>
</a>
);
}

View File

@ -10,6 +10,7 @@ export interface VideoPlayerIconButtonProps {
active?: boolean; active?: boolean;
wide?: boolean; wide?: boolean;
noPadding?: boolean; noPadding?: boolean;
disabled?: boolean;
} }
export const VideoPlayerIconButton = forwardRef< export const VideoPlayerIconButton = forwardRef<
@ -21,13 +22,21 @@ export const VideoPlayerIconButton = forwardRef<
<button <button
type="button" type="button"
onClick={props.onClick} onClick={props.onClick}
className="group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110" className={[
"group pointer-events-auto p-2 text-white transition-transform duration-100 active:scale-110",
props.disabled
? "pointer-events-none cursor-not-allowed opacity-50"
: "",
].join(" ")}
> >
<div <div
className={[ className={[
"flex items-center justify-center rounded-full bg-denim-600 bg-opacity-0 transition-colors duration-100 group-hover:bg-opacity-50 group-active:bg-denim-500 group-active:bg-opacity-100", "flex items-center justify-center rounded-full bg-denim-600 bg-opacity-0 transition-colors duration-100",
props.active ? "!bg-denim-500 !bg-opacity-100" : "", props.active ? "!bg-denim-500 !bg-opacity-100" : "",
!props.noPadding ? (props.wide ? "py-2 px-4" : "p-2") : "", !props.noPadding ? (props.wide ? "py-2 px-4" : "p-2") : "",
!props.disabled
? "group-hover:bg-opacity-50 group-active:bg-denim-500 group-active:bg-opacity-100"
: "",
].join(" ")} ].join(" ")}
> >
<Icon icon={props.icon} className={props.iconSize ?? "text-2xl"} /> <Icon icon={props.icon} className={props.iconSize ?? "text-2xl"} />