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:
parent
5056047546
commit
06cf7050a9
8 changed files with 184 additions and 44 deletions
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
@import '~/styles/variables';
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: calc(16 / 9);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: $radius;
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
17
src/utils/providers/VideoPlayerProvider.tsx
Normal file
17
src/utils/providers/VideoPlayerProvider.tsx
Normal 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);
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue