Skip to content
8 changes: 8 additions & 0 deletions .changeset/pink-wasps-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@alfalab/core-components-gallery': minor
---

Добавлена поддержка скачивания видео для мобильных браузеров.
Расширены возможности управления видео в галерее. Заменен цвет для плейсхолдеров и заглушек, что улучшает видимость на темном фоне галереи.
Исправлены отступы у навигации.
При свайпе до закрытия, галерея следует за свайпом
50 changes: 45 additions & 5 deletions packages/gallery/src/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const Gallery: FC<GalleryProps> = ({
const [mutedVideo, setMutedVideo] = useState<boolean>(DEFAULT_MUTED_VIDEO);
const [playingVideo, setPlayingVideo] = useState<boolean>(DEFAULT_PLAYING_VIDEO);
const [hideNavigation, setHideNavigation] = useState<boolean>(DEFAULT_HIDE_NAVIGATION);
const [swipeY, setSwipeY] = useState<number>(0);

const isDesktop = useIsDesktop();

Expand Down Expand Up @@ -208,32 +209,67 @@ export const Gallery: FC<GalleryProps> = ({
}, [handleKeyDown]);

useEffect(() => {
let startY: number;
let startX = 0;
let startY = 0;
let lockedDirection: 'horizontal' | 'vertical' | null = null;
const directionLockThreshold = 10;

const abortController = new AbortController();
const { signal } = abortController;

document.addEventListener(
'touchstart',
(e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
lockedDirection = null;
},
{ signal },
);

document.addEventListener(
'touchmove',
(e) => {
const endY = e.changedTouches[0].clientY;
const deltaY = startY - endY;
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;
const deltaX = currentX - startX;
const deltaY = currentY - startY;

if (!lockedDirection) {
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);

if (absX > absY && absX > directionLockThreshold) {
lockedDirection = 'horizontal';
} else if (absY > absX && absY > directionLockThreshold) {
lockedDirection = 'vertical';
} else {
return;
}
}

// Если свайп вниз, закрываем галерею
if (deltaY < SWIPE_THRESHOLD) {
if (lockedDirection === 'horizontal') {
return;
}

// vertical lock
setSwipeY(deltaY);
if (deltaY < SWIPE_THRESHOLD || deltaY > -SWIPE_THRESHOLD) {
onClose();
}
},
{ signal },
);

document.addEventListener(
'touchend',
() => {
lockedDirection = null;
setSwipeY(0);
},
{ signal },
);

return () => {
abortController.abort();
};
Expand Down Expand Up @@ -281,6 +317,10 @@ export const Gallery: FC<GalleryProps> = ({
onUnmount={onUnmount}
>
<div
data-content-area={true}
style={{
transform: `translateY(${swipeY}px)`,
}}
className={cn(styles.container, {
[styles.mobile]: !isDesktop,
})}
Expand Down
221 changes: 158 additions & 63 deletions packages/gallery/src/components/buttons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { FC, MutableRefObject } from 'react';
import cn from 'classnames';

import { IconButton, IconButtonProps } from '@alfalab/core-components-icon-button';
import { useIsDesktop } from '@alfalab/core-components-mq';
import { TooltipDesktop } from '@alfalab/core-components-tooltip/desktop';
import { ArrowLeftMIcon } from '@alfalab/icons-glyph/ArrowLeftMIcon';
import { ArrowsInwardMIcon } from '@alfalab/icons-glyph/ArrowsInwardMIcon';
Expand All @@ -14,29 +15,43 @@ import { ShareMIcon } from '@alfalab/icons-glyph/ShareMIcon';
import { SoundCrossMIcon } from '@alfalab/icons-glyph/SoundCrossMIcon';
import { SoundMIcon } from '@alfalab/icons-glyph/SoundMIcon';

import { downloadFile, isVideo } from '../../utils';

import styles from './index.module.css';

type Props = Omit<IconButtonProps, 'icon' | 'colors'> & {
buttonRef?: MutableRefObject<HTMLButtonElement | null>;
download?: string | boolean;
};

export const Fullscreen: FC<Props> = ({ buttonRef, ...restProps }) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Открыть в полноэкранном режиме'
fallbackPlacements={['bottom-end']}
>
export const Fullscreen: FC<Props> = ({ buttonRef, ...restProps }) => {
const isDesktop = useIsDesktop();

const iconButton = (
<IconButton
{...restProps}
ref={buttonRef}
icon={ArrowsOutwardMIcon}
aria-label='Открыть в полноэкранном режиме'
className={styles.iconButton}
/>
</TooltipDesktop>
);
);

return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Открыть в полноэкранном режиме'
fallbackPlacements={['bottom-end']}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const BackArrow: FC<Props> = ({ buttonRef, ...restProps }) => (
<IconButton
Expand Down Expand Up @@ -68,90 +83,170 @@ export const Pause: FC<Props> = ({ buttonRef, className, ...restProps }) => (
/>
);

export const ExitFullscreen: FC<Props> = ({ buttonRef, ...restProps }) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Выйти из полноэкранного режима'
fallbackPlacements={['bottom-end']}
>
export const ExitFullscreen: FC<Props> = ({ buttonRef, ...restProps }) => {
const isDesktop = useIsDesktop();

const iconButton = (
<IconButton
{...restProps}
ref={buttonRef}
icon={ArrowsInwardMIcon}
aria-label='Выйти из полноэкранного режима'
className={styles.iconButton}
/>
</TooltipDesktop>
);
);

return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Выйти из полноэкранного режима'
fallbackPlacements={['bottom-end']}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const MuteVideo: FC<Props> = ({ buttonRef, className, ...restProps }) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Выключить звук'
fallbackPlacements={['bottom-end']}
targetClassName={className}
>
export const MuteVideo: FC<Props> = ({ buttonRef, className, ...restProps }) => {
const isDesktop = useIsDesktop();

const iconButton = (
<IconButton
{...restProps}
ref={buttonRef}
icon={SoundMIcon}
aria-label='Выключить звук'
className={styles.iconButton}
/>
</TooltipDesktop>
);
);

return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Выключить звук'
fallbackPlacements={['bottom-end']}
targetClassName={className}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const UnmuteVideo: FC<Props> = ({ buttonRef, className, ...restProps }) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Включить звук'
fallbackPlacements={['bottom-end']}
targetClassName={className}
>
export const UnmuteVideo: FC<Props> = ({ buttonRef, className, ...restProps }) => {
const isDesktop = useIsDesktop();

const iconButton = (
<IconButton
{...restProps}
ref={buttonRef}
icon={SoundCrossMIcon}
aria-label='Включить звук'
className={styles.iconButton}
/>
</TooltipDesktop>
);
);

export const Download: FC<Props> = (props) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Скачать'
fallbackPlacements={['bottom-end']}
>
<IconButton
{...props}
icon={PointerDownMIcon}
aria-label='Скачать'
className={styles.iconButton}
/>
</TooltipDesktop>
);
return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Включить звук'
fallbackPlacements={['bottom-end']}
targetClassName={className}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const Download: FC<Props & { href?: string }> = ({ href, ...props }) => {
const isDesktop = useIsDesktop();

const handleMobileVideoDownload = async (e: React.MouseEvent) => {
e.preventDefault();

if (!href) return;

const fileName = props.download?.toString() || 'video';
const fileType = isVideo(href) ? 'video/*' : undefined;

await downloadFile({
url: href,
fileName,
fileType,
});
};

export const Share: FC<Props> = (props) => (
<TooltipDesktop
trigger='hover'
position='bottom'
content='Поделиться'
fallbackPlacements={['bottom-end']}
>
const iconButtonProps = {
...props,
icon: PointerDownMIcon,
'aria-label': 'Скачать' as const,
className: styles.iconButton,
};

const iconButton =
!isDesktop && isVideo(href) ? (
<IconButton {...iconButtonProps} onClick={handleMobileVideoDownload} />
) : (
<IconButton {...iconButtonProps} href={href} />
);

return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Скачать'
fallbackPlacements={['bottom-end']}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const Share: FC<Props> = (props) => {
const isDesktop = useIsDesktop();

const iconButton = (
<IconButton
{...props}
icon={ShareMIcon}
aria-label='Скачать'
aria-label='Поделиться'
className={styles.iconButton}
/>
</TooltipDesktop>
);
);

return isDesktop ? (
<TooltipDesktop
view='hint'
colors='inverted'
trigger='hover'
position='bottom'
content='Поделиться'
fallbackPlacements={['bottom-end']}
>
{iconButton}
</TooltipDesktop>
) : (
iconButton
);
};

export const Exit: FC<Props> = (props) => (
<IconButton {...props} icon={CrossMIcon} aria-label='Закрыть' className={styles.iconButton} />
Expand Down
Loading