mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
refactored media components
This commit is contained in:
parent
b551fc44ea
commit
d757b74fd0
9 changed files with 6 additions and 210 deletions
149
src/components/common/AudioPlayer/index.tsx
Normal file
149
src/components/common/AudioPlayer/index.tsx
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Icon } from '~/components/common/Icon';
|
||||
import { InputText } from '~/components/input/InputText';
|
||||
import { PlayerState } from '~/constants/player';
|
||||
import { IFile } from '~/types';
|
||||
import { useAudioPlayer } from '~/utils/providers/AudioPlayerProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type Props = {
|
||||
file: IFile;
|
||||
isEditing?: boolean;
|
||||
onDelete?: (id: IFile['id']) => void;
|
||||
onTitleChange?: (file_id: IFile['id'], title: string) => void;
|
||||
};
|
||||
|
||||
const AudioPlayer = memo(
|
||||
({ file, onDelete, isEditing, onTitleChange }: Props) => {
|
||||
const {
|
||||
toPercent,
|
||||
file: currentFile,
|
||||
setFile,
|
||||
play,
|
||||
status,
|
||||
progress,
|
||||
pause,
|
||||
} = useAudioPlayer();
|
||||
|
||||
const onPlay = useCallback(
|
||||
async (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (file.id !== currentFile?.id) {
|
||||
setFile(file);
|
||||
setTimeout(() => void play(), 0);
|
||||
return;
|
||||
}
|
||||
|
||||
status === PlayerState.PLAYING ? pause() : await play();
|
||||
},
|
||||
[play, pause, setFile, file, currentFile, status],
|
||||
);
|
||||
|
||||
const onSeek = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation();
|
||||
const { clientX, target } = event;
|
||||
const { left, width } = target.getBoundingClientRect();
|
||||
toPercent(((clientX - left) / width) * 100);
|
||||
},
|
||||
[toPercent],
|
||||
);
|
||||
|
||||
const onDropClick = useCallback(() => {
|
||||
if (!onDelete) return;
|
||||
|
||||
onDelete(file.id);
|
||||
}, [file, onDelete]);
|
||||
|
||||
const title = useMemo(
|
||||
() =>
|
||||
(file.metadata &&
|
||||
(file.metadata.title ||
|
||||
[file.metadata.id3artist, file.metadata.id3title]
|
||||
.filter((el) => el)
|
||||
.join(' - '))) ||
|
||||
file.orig_name ||
|
||||
'',
|
||||
[file.metadata, file.orig_name],
|
||||
);
|
||||
|
||||
const onRename = useCallback(
|
||||
(val: string) => {
|
||||
if (!onTitleChange) return;
|
||||
|
||||
onTitleChange(file.id, val);
|
||||
},
|
||||
[onTitleChange, file.id],
|
||||
);
|
||||
|
||||
const stopPropagation = useCallback(
|
||||
(event) => {
|
||||
if (!isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
},
|
||||
[isEditing],
|
||||
);
|
||||
|
||||
const playing = currentFile?.id === file.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.wrap, {
|
||||
[styles.playing]: playing,
|
||||
})}
|
||||
>
|
||||
{onDelete && (
|
||||
<div className={styles.drop} onMouseDown={onDropClick}>
|
||||
<Icon icon="close" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={styles.playpause}
|
||||
onClick={onPlay}
|
||||
onMouseDown={stopPropagation}
|
||||
>
|
||||
{playing && status === PlayerState.PLAYING ? (
|
||||
<Icon icon="pause" />
|
||||
) : (
|
||||
<Icon icon="play" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className={styles.input}>
|
||||
<InputText
|
||||
placeholder={title}
|
||||
handler={onRename}
|
||||
value={file.metadata && file.metadata.title}
|
||||
onMouseDown={stopPropagation}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>{title || ''}</div>
|
||||
|
||||
<div className={styles.progress} onClick={onSeek}>
|
||||
<div
|
||||
className={styles.bar}
|
||||
style={{
|
||||
width: `${progress.progress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export { AudioPlayer };
|
138
src/components/common/AudioPlayer/styles.module.scss
Normal file
138
src/components/common/AudioPlayer/styles.module.scss
Normal file
|
@ -0,0 +1,138 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: $comment_height;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
flex: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.playpause {
|
||||
flex: 0 0 $comment_height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: $gray_50;
|
||||
stroke: $gray_50;
|
||||
transition: fill 250ms, stroke 250ms;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 0 $gap * 2 0 $gap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
text-align: left;
|
||||
transition: all 0.5s;
|
||||
font: $font_18_semibold;
|
||||
|
||||
.playing & {
|
||||
top: 20px;
|
||||
opacity: 1;
|
||||
font-size: 12px;
|
||||
padding-right: 140px;
|
||||
color: $gray_75;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
transition: opacity 0.5s;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.playing & {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
touch-action: auto;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: $gray_90;
|
||||
width: 100%;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
background: $primary_gradient;
|
||||
position: absolute;
|
||||
height: 10px;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
border-radius: 5px;
|
||||
min-width: 10px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
|
||||
.drop {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #222222;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
border-radius: 12px;
|
||||
z-index: 2;
|
||||
transition: background-color 250ms, opacity 0.25s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
padding: 0 48px 0 0;
|
||||
}
|
109
src/components/common/PinchZoom/index.tsx
Normal file
109
src/components/common/PinchZoom/index.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
interface Props {
|
||||
children: (props: {
|
||||
setRef: (ref: HTMLElement | null) => void;
|
||||
}) => ReactElement;
|
||||
}
|
||||
|
||||
const getDistance = (event: TouchEvent) => {
|
||||
return Math.hypot(
|
||||
event.touches[0].pageX - event.touches[1].pageX,
|
||||
event.touches[0].pageY - event.touches[1].pageY,
|
||||
);
|
||||
};
|
||||
|
||||
interface Start {
|
||||
x: number;
|
||||
y: number;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
const PinchZoom: FC<Props> = ({ children }) => {
|
||||
const start = useRef<Start>({ x: 0, y: 0, distance: 0 });
|
||||
const [ref, setRef] = useState<HTMLElement | null>(null);
|
||||
const imageElementScale = useRef(1);
|
||||
|
||||
const onTouchStart = useCallback((event: TouchEvent) => {
|
||||
if (event.touches.length !== 2) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault(); // Prevent page scroll
|
||||
|
||||
// Calculate where the fingers have started on the X and Y axis
|
||||
start.current.x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
|
||||
start.current.y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
|
||||
start.current.distance = getDistance(event);
|
||||
}, []);
|
||||
|
||||
const onTouchMove = useCallback(
|
||||
(event) => {
|
||||
if (event.touches.length !== 2 || !ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault(); // Prevent page scroll
|
||||
|
||||
// Safari provides event.scale as two fingers move on the screen
|
||||
// For other browsers just calculate the scale manually
|
||||
const scale = event.scale ?? getDistance(event) / start.current.distance;
|
||||
imageElementScale.current = Math.min(Math.max(1, scale), 4);
|
||||
|
||||
// Calculate how much the fingers have moved on the X and Y axis
|
||||
const deltaX =
|
||||
((event.touches[0].pageX + event.touches[1].pageX) / 2 -
|
||||
start.current.x) *
|
||||
2; // x2 for accelarated movement
|
||||
const deltaY =
|
||||
((event.touches[0].pageY + event.touches[1].pageY) / 2 -
|
||||
start.current.y) *
|
||||
2; // x2 for accelarated movement
|
||||
|
||||
// Transform the image to make it grow and move with fingers
|
||||
const transform = `translate3d(${deltaX}px, ${deltaY}px, 0) scale(${imageElementScale})`;
|
||||
ref.style.transform = transform;
|
||||
ref.style.zIndex = '9999';
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
const onTouchEnd = useCallback(
|
||||
(event) => {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset image to it's original format
|
||||
ref.style.transform = '';
|
||||
ref.style.zIndex = '';
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.addEventListener('touchstart', onTouchStart);
|
||||
ref.addEventListener('touchmove', onTouchMove);
|
||||
ref.addEventListener('touchend', onTouchEnd);
|
||||
|
||||
return () => {
|
||||
ref.removeEventListener('touchstart', onTouchStart);
|
||||
ref.removeEventListener('touchmove', onTouchMove);
|
||||
ref.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
}, [onTouchEnd, onTouchMove, onTouchStart, ref]);
|
||||
|
||||
return children({ setRef });
|
||||
};
|
||||
|
||||
export { PinchZoom };
|
Loading…
Add table
Add a link
Reference in a new issue