1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 12:26:40 +07:00

play video in embed instead of new window on desktops

This commit is contained in:
Fedor Katurov 2025-02-26 16:48:54 +07:00
parent 5056047546
commit 06cf7050a9
8 changed files with 184 additions and 44 deletions

View file

@ -36,6 +36,7 @@
"react-lazyload": "^3.2.0", "react-lazyload": "^3.2.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-popper": "^2.2.3", "react-popper": "^2.2.3",
"react-resize-detector": "^12.0.2",
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-sticky-box": "^1.0.2", "react-sticky-box": "^1.0.2",

View file

@ -0,0 +1,28 @@
import classNames from 'classnames';
import { useResizeDetector } from 'react-resize-detector';
import styles from './styles.module.scss';
interface Props {
id: string;
title: string;
className?: string;
}
export const CommentVideoFrame = ({ id, title, className }: Props) => {
const { ref, width = 0, height = 0 } = useResizeDetector();
return (
<div className={classNames(styles.wrap, className)} ref={ref}>
<iframe
width={width}
height={height}
src={`https://www.youtube.com/embed/${id}?autoplay=1`}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
frameBorder="0"
allowFullScreen
title={title}
/>
</div>
);
};

View file

@ -0,0 +1,8 @@
@import '~/styles/variables';
.wrap {
width: 100%;
aspect-ratio: calc(16 / 9);
overflow: hidden;
border-radius: $radius;
}

View file

@ -1,15 +1,21 @@
import { FC, memo, useMemo } from 'react'; import { FC, memo, useCallback, useMemo } from 'react';
import { Icon } from '~/components/common/Icon'; import { Icon } from '~/components/common/Icon';
import { ICommentBlockProps } from '~/constants/comment'; import { ICommentBlockProps } from '~/constants/comment';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata'; import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata';
import { getYoutubeThumb } from '~/utils/dom'; import { getYoutubeThumb } from '~/utils/dom';
import { useVideoPlayer } from '~/utils/providers/VideoPlayerProvider';
import { CommentVideoFrame } from './components/CommentVideoFrame';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
type Props = ICommentBlockProps & {}; type Props = ICommentBlockProps & {};
const CommentEmbedBlock: FC<Props> = memo(({ block }) => { const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
const { isTablet } = useWindowSize();
const { url, setUrl } = useVideoPlayer();
const id = useMemo(() => { const id = useMemo(() => {
const match = block.content.match( const match = block.content.match(
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/, /https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/,
@ -18,7 +24,7 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
return (match && match[1]) || ''; return (match && match[1]) || '';
}, [block.content]); }, [block.content]);
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]); const address = `https://youtube.com/watch?v=${id}`;
const preview = useMemo( const preview = useMemo(
() => getYoutubeThumb(block.content), () => getYoutubeThumb(block.content),
@ -28,21 +34,46 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
const metadata = useYoutubeMetadata(id); const metadata = useYoutubeMetadata(id);
const title = metadata?.metadata?.title || ''; const title = metadata?.metadata?.title || '';
const onClick = useCallback(() => {
if (isTablet) {
window.open(address, '_blank');
return;
}
setUrl(address);
}, [isTablet, setUrl, address]);
const closeVideo = useCallback(() => setUrl(''), [setUrl]);
return ( return (
<div className={styles.embed}> <div className={styles.embed}>
<a href={url} target="_blank" rel="noreferrer" /> {url === address ? (
<div className={styles.video}>
<div className={styles.preview}> <div className={styles.close} onClick={closeVideo}>
<div style={{ backgroundImage: `url("${preview}")` }}> <Icon icon="close" />
<div className={styles.backdrop}> </div>
<div className={styles.play}> <div className={styles.animation}>
<Icon icon="play" size={32} /> <CommentVideoFrame id={id} title={title} />
</div>
<div className={styles.title}>{title}</div>
</div> </div>
</div> </div>
</div> ) : (
<div
className={styles.preview}
role="button"
onClick={onClick}
tabIndex={-1}
>
<div style={{ backgroundImage: `url("${preview}")` }}>
<div className={styles.backdrop}>
<div className={styles.play}>
<Icon icon="play" size={32} />
</div>
<div className={styles.title}>{title}</div>
</div>
</div>
</div>
)}
</div> </div>
); );
}); });

View file

@ -1,8 +1,8 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
.embed { .embed {
padding: 0 $gap; padding: 0 0;
height: $comment_height; min-height: $comment_height;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
background: 50% 50% no-repeat; background: 50% 50% no-repeat;
@ -69,6 +69,7 @@
justify-content: stretch; justify-content: stretch;
box-sizing: border-box; box-sizing: border-box;
z-index: 2; z-index: 2;
cursor: pointer;
& > div { & > div {
width: 100%; width: 100%;
@ -98,3 +99,47 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@keyframes appear {
0% {
grid-template-columns: 0fr;
opacity: 0;
}
50% {
grid-template-columns: 1fr;
}
100% {
opacity: 1;
}
}
.video {
width: 100%;
position: relative;
padding: $gap / 2;
}
.close {
display: flex;
align-items: center;
justify-content: center;
background: var(--color_danger);
width: 64px;
height: 24px;
position: absolute;
bottom: calc(100% - #{$gap / 2});
right: 24px;
border-radius: $radius $radius 0 0;
z-index: 10;
cursor: pointer;
}
.animation {
background-color: var(--content_bg_darker);
display: grid;
animation: appear 0.5s forwards;
width: 100%;
border-radius: $radius;
}

View file

@ -15,6 +15,7 @@ import { useCommentContext } from '~/utils/context/CommentContextProvider';
import { useNodeContext } from '~/utils/context/NodeContextProvider'; import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useUserContext } from '~/utils/context/UserContextProvider'; import { useUserContext } from '~/utils/context/UserContextProvider';
import { canEditComment, canLikeComment } from '~/utils/node'; import { canEditComment, canLikeComment } from '~/utils/node';
import { VideoPlayerProvider } from '~/utils/providers/VideoPlayerProvider';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
@ -84,38 +85,40 @@ const NodeComments: FC<Props> = observer(({ order }) => {
}, [isLoading]); }, [isLoading]);
return ( return (
<div className={styles.wrap}> <VideoPlayerProvider>
{order === 'DESC' && more} <div className={styles.wrap}>
{order === 'DESC' && more}
{groupped.map((group, index) => ( {groupped.map((group, index) => (
<> <>
{isFirstGroupWithNewComment(group, groupped[index - 1]) && ( {isFirstGroupWithNewComment(group, groupped[index - 1]) && (
<a <a
id={NEW_COMMENT_ANCHOR_NAME} id={NEW_COMMENT_ANCHOR_NAME}
className={styles.newCommentAnchor} className={styles.newCommentAnchor}
/>
)}
<Comment
nodeId={node.id!}
key={group.ids.join()}
group={group}
highlighted={
node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID
}
onLike={onLike}
canLike={canLikeComment(group, user)}
canEdit={canEditComment(group, user)}
onDelete={onDeleteComment}
onShowImageModal={onShowImageModal}
isSame={group.user.id === user.id}
saveComment={onSaveComment}
/> />
)} </>
))}
<Comment {order === 'ASC' && more}
nodeId={node.id!} </div>
key={group.ids.join()} </VideoPlayerProvider>
group={group}
highlighted={
node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID
}
onLike={onLike}
canLike={canLikeComment(group, user)}
canEdit={canEditComment(group, user)}
onDelete={onDeleteComment}
onShowImageModal={onShowImageModal}
isSame={group.user.id === user.id}
saveComment={onSaveComment}
/>
</>
))}
{order === 'ASC' && more}
</div>
); );
}); });

View file

@ -0,0 +1,17 @@
import { createContext, ReactNode, useContext, useState } from 'react';
const Context = createContext({
url: '',
setUrl: (val: string) => {},
});
/** Provides context for comment video playing */
export const VideoPlayerProvider = ({ children }: { children: ReactNode }) => {
const [url, setUrl] = useState('');
return (
<Context.Provider value={{ url, setUrl }}>{children}</Context.Provider>
);
};
export const useVideoPlayer = () => useContext(Context);

View file

@ -2626,6 +2626,13 @@ react-popper@^2.2.3:
react-fast-compare "^3.0.1" react-fast-compare "^3.0.1"
warning "^4.0.2" warning "^4.0.2"
react-resize-detector@^12.0.2:
version "12.0.2"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-12.0.2.tgz#5e65f906a85835d246de57dbf608bf22ab333cad"
integrity sha512-aAI4WxWAysWLhA8wKDpsS+PnnxQ0lWCkTlk2t+2ijalWvoSa7vPxmcKRLURkH+PU84QE4KP4dO58oVP3ypWkKA==
dependencies:
lodash "^4.17.21"
react-router-dom@^5.1.2: react-router-dom@^5.1.2:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.0.tgz#da1bfb535a0e89a712a93b97dd76f47ad1f32363"