From 67456210516667b37422b95480fb38dd2dc42f8a Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 1 Jul 2025 13:11:51 +0300 Subject: [PATCH 1/5] fix(gallery): paddings fix --- packages/gallery/src/components/buttons/index.tsx | 2 ++ .../gallery/src/components/header-info-block/Component.tsx | 6 +++--- packages/gallery/src/components/header/index.module.css | 2 +- .../gallery/src/components/navigation-bar/index.module.css | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/gallery/src/components/buttons/index.tsx b/packages/gallery/src/components/buttons/index.tsx index 846756a7dc..4afe76d703 100644 --- a/packages/gallery/src/components/buttons/index.tsx +++ b/packages/gallery/src/components/buttons/index.tsx @@ -23,6 +23,8 @@ type Props = Omit & { export const Fullscreen: FC = ({ buttonRef, ...restProps }) => ( = ({ filename, descriptio
{head} - + {tail}
diff --git a/packages/gallery/src/components/header/index.module.css b/packages/gallery/src/components/header/index.module.css index d34676b912..e851f7bb6c 100644 --- a/packages/gallery/src/components/header/index.module.css +++ b/packages/gallery/src/components/header/index.module.css @@ -5,6 +5,6 @@ justify-content: space-between; flex-shrink: 0; width: 100%; - padding: var(--gap-16) var(--gap-32); + padding: var(--gap-16) var(--gap-24) var(--gap-16) var(--gap-32); box-sizing: border-box; } diff --git a/packages/gallery/src/components/navigation-bar/index.module.css b/packages/gallery/src/components/navigation-bar/index.module.css index 73b8ccce14..159e6202db 100644 --- a/packages/gallery/src/components/navigation-bar/index.module.css +++ b/packages/gallery/src/components/navigation-bar/index.module.css @@ -8,7 +8,7 @@ flex-shrink: 0; overflow-x: auto; box-sizing: border-box; - padding: var(--gap-12) var(--gap-24); + padding: var(--gap-12) var(--gap-32); scrollbar-width: none; background-color: var(--color-static-neutral-0-inverted); From 6dcfb8de5b8f84ea084eabcaf73b9fa631731b3a Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 2 Jul 2025 17:13:58 +0300 Subject: [PATCH 2/5] =?UTF-8?q?feat(gallery):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B8=D0=B4=D0=B5=D0=BE=20=D0=B8=20?= =?UTF-8?q?=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=D0=B2=D0=B0?= =?UTF-8?q?=D0=B9=D0=BF=D0=BE=D0=B2=20=D0=B2=20=D0=B3=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/pink-wasps-serve.md | 8 + packages/gallery/src/Component.tsx | 17 +- .../gallery/src/components/buttons/index.tsx | 223 +++++++++++++----- .../components/image-preview/Component.tsx | 2 +- .../components/image-preview/index.module.css | 4 +- .../src/components/image-viewer/component.tsx | 1 + .../src/components/image-viewer/hooks.ts | 5 +- .../components/image-viewer/index.module.css | 4 +- .../src/components/image-viewer/single.tsx | 1 + .../src/components/image-viewer/slide.tsx | 8 +- .../image-viewer/video/index.module.css | 10 + .../components/image-viewer/video/index.tsx | 76 +++++- .../navigation-bar/index.module.css | 2 +- .../gallery/src/docs/Component.stories.tsx | 8 +- packages/gallery/src/types.ts | 6 +- packages/gallery/src/utils/index.ts | 1 + packages/gallery/src/utils/modern-download.ts | 92 ++++++++ 17 files changed, 374 insertions(+), 94 deletions(-) create mode 100644 .changeset/pink-wasps-serve.md create mode 100644 packages/gallery/src/utils/modern-download.ts diff --git a/.changeset/pink-wasps-serve.md b/.changeset/pink-wasps-serve.md new file mode 100644 index 0000000000..daf9057f5b --- /dev/null +++ b/.changeset/pink-wasps-serve.md @@ -0,0 +1,8 @@ +--- +'@alfalab/core-components-gallery': minor +--- + +Добавлена поддержка скачивания видео для мобильных браузеров. +Расширены возможности управления видео в галерее. Заменен цвет для плейсхолдеров и заглушек, что улучшает видимость на темном фоне галереи. +Исправлены отступы у навигации. +При свайпе до закрытия, галерея следует за свайпом diff --git a/packages/gallery/src/Component.tsx b/packages/gallery/src/Component.tsx index c7566a6876..1c9c982153 100644 --- a/packages/gallery/src/Component.tsx +++ b/packages/gallery/src/Component.tsx @@ -88,6 +88,7 @@ export const Gallery: FC = ({ const [mutedVideo, setMutedVideo] = useState(DEFAULT_MUTED_VIDEO); const [playingVideo, setPlayingVideo] = useState(DEFAULT_PLAYING_VIDEO); const [hideNavigation, setHideNavigation] = useState(DEFAULT_HIDE_NAVIGATION); + const [swipeY, setSwipeY] = useState(0); const isDesktop = useIsDesktop(); @@ -226,14 +227,23 @@ export const Gallery: FC = ({ const endY = e.changedTouches[0].clientY; const deltaY = startY - endY; - // Если свайп вниз, закрываем галерею - if (deltaY < SWIPE_THRESHOLD) { + setSwipeY(-deltaY); + + if (deltaY < SWIPE_THRESHOLD || deltaY > -SWIPE_THRESHOLD) { onClose(); } }, { signal }, ); + document.addEventListener( + 'touchend', + () => { + setSwipeY(0); + }, + { signal }, + ); + return () => { abortController.abort(); }; @@ -281,6 +291,9 @@ export const Gallery: FC = ({ onUnmount={onUnmount} >
& { @@ -21,15 +24,10 @@ type Props = Omit & { download?: string | boolean; }; -export const Fullscreen: FC = ({ buttonRef, ...restProps }) => ( - +export const Fullscreen: FC = ({ buttonRef, ...restProps }) => { + const isDesktop = useIsDesktop(); + + const iconButton = ( = ({ buttonRef, ...restProps }) => ( aria-label='Открыть в полноэкранном режиме' className={styles.iconButton} /> - -); + ); + + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; export const BackArrow: FC = ({ buttonRef, ...restProps }) => ( = ({ buttonRef, className, ...restProps }) => ( /> ); -export const ExitFullscreen: FC = ({ buttonRef, ...restProps }) => ( - +export const ExitFullscreen: FC = ({ buttonRef, ...restProps }) => { + const isDesktop = useIsDesktop(); + + const iconButton = ( = ({ buttonRef, ...restProps }) => ( aria-label='Выйти из полноэкранного режима' className={styles.iconButton} /> - -); + ); + + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; -export const MuteVideo: FC = ({ buttonRef, className, ...restProps }) => ( - +export const MuteVideo: FC = ({ buttonRef, className, ...restProps }) => { + const isDesktop = useIsDesktop(); + + const iconButton = ( = ({ buttonRef, className, ...restProps }) => aria-label='Выключить звук' className={styles.iconButton} /> - -); + ); + + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; -export const UnmuteVideo: FC = ({ buttonRef, className, ...restProps }) => ( - +export const UnmuteVideo: FC = ({ buttonRef, className, ...restProps }) => { + const isDesktop = useIsDesktop(); + + const iconButton = ( = ({ buttonRef, className, ...restProps }) = aria-label='Включить звук' className={styles.iconButton} /> - -); + ); -export const Download: FC = (props) => ( - - - -); + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; + +export const Download: FC = ({ 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) => ( - + const iconButtonProps = { + ...props, + icon: PointerDownMIcon, + 'aria-label': 'Скачать' as const, + className: styles.iconButton, + }; + + const iconButton = + !isDesktop && isVideo(href) ? ( + + ) : ( + + ); + + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; + +export const Share: FC = (props) => { + const isDesktop = useIsDesktop(); + + const iconButton = ( - -); + ); + + return isDesktop ? ( + + {iconButton} + + ) : ( + iconButton + ); +}; export const Exit: FC = (props) => ( diff --git a/packages/gallery/src/components/image-preview/Component.tsx b/packages/gallery/src/components/image-preview/Component.tsx index ca9036568b..4e18b780e5 100644 --- a/packages/gallery/src/components/image-preview/Component.tsx +++ b/packages/gallery/src/components/image-preview/Component.tsx @@ -151,7 +151,7 @@ export const ImagePreview: FC = ({ image, active = false, index, onSelect return (
diff --git a/packages/gallery/src/components/image-preview/index.module.css b/packages/gallery/src/components/image-preview/index.module.css index 816353df2b..693fd7dd1f 100644 --- a/packages/gallery/src/components/image-preview/index.module.css +++ b/packages/gallery/src/components/image-preview/index.module.css @@ -59,7 +59,7 @@ background-color: var(--color-static-neutral-100-inverted); & .active { - background-color: var(--color-static-neutral-300-inverted); + background-color: var(--color-static-neutral-translucent-inverted-100); } } @@ -67,7 +67,7 @@ display: flex; justify-content: center; align-items: center; - background-color: var(--color-static-neutral-300-inverted); + background-color: var(--color-static-neutral-translucent-inverted-100); opacity: 0.3; &.active { diff --git a/packages/gallery/src/components/image-viewer/component.tsx b/packages/gallery/src/components/image-viewer/component.tsx index 22d20bd6a8..0047179db8 100644 --- a/packages/gallery/src/components/image-viewer/component.tsx +++ b/packages/gallery/src/components/image-viewer/component.tsx @@ -135,6 +135,7 @@ export const ImageViewer: FC = () => { src={currentImage?.src} alt={currentImage ? getImageAlt(currentImage, currentSlideIndex) : ''} className={styles.fullScreenImage} + data-content-area='true' /> )} diff --git a/packages/gallery/src/components/image-viewer/hooks.ts b/packages/gallery/src/components/image-viewer/hooks.ts index dded28d23e..bb6395bd93 100644 --- a/packages/gallery/src/components/image-viewer/hooks.ts +++ b/packages/gallery/src/components/image-viewer/hooks.ts @@ -42,9 +42,10 @@ export const useHandleImageViewer = () => { const isPlaceholder = Boolean(eventTarget.closest(`.${styles.placeholder}`)); - const isImg = eventTarget.tagName === 'IMG'; + const isContentArea = Boolean(eventTarget.closest('[data-content-area]')); - if (!isImg && !isPlaceholder && !isArrow && !isMobile) { + // Закрываем галерею только при клике вне элементов контента и только на desktop + if (!isPlaceholder && !isArrow && !isContentArea && !isMobile) { onClose(); } }, diff --git a/packages/gallery/src/components/image-viewer/index.module.css b/packages/gallery/src/components/image-viewer/index.module.css index 76763622e1..3317c57e0a 100644 --- a/packages/gallery/src/components/image-viewer/index.module.css +++ b/packages/gallery/src/components/image-viewer/index.module.css @@ -32,7 +32,7 @@ height: 100%; max-height: calc(100vh - 80px); - padding: var(--gap-32); + padding: var(--gap-0) var(--gap-32) var(--gap-32); box-sizing: border-box; &.mobile { @@ -129,7 +129,7 @@ width: 100%; height: 100%; border-radius: var(--border-radius-8); - background-color: var(--color-static-neutral-300-inverted); + background-color: var(--color-static-neutral-translucent-inverted-100); } .brokenImgWrapper { diff --git a/packages/gallery/src/components/image-viewer/single.tsx b/packages/gallery/src/components/image-viewer/single.tsx index 236a5f07f7..64fba357c2 100644 --- a/packages/gallery/src/components/image-viewer/single.tsx +++ b/packages/gallery/src/components/image-viewer/single.tsx @@ -39,6 +39,7 @@ export const Single: FC = () => { src={currentImage?.src} alt={currentImage ? getImageAlt(currentImage, currentSlideIndex) : ''} className={styles.fullScreenImage} + data-content-area='true' /> ) : (
= ({ if (isVideo(image.src)) { return ( - + ); @@ -114,6 +119,7 @@ export const Slide: FC = ({ style={{ maxHeight: `${containerHeight}px`, }} + data-content-area='true' data-test-id={slideVisible ? TestIds.ACTIVE_IMAGE : undefined} /> diff --git a/packages/gallery/src/components/image-viewer/video/index.module.css b/packages/gallery/src/components/image-viewer/video/index.module.css index 22143519f7..f420ab8da6 100644 --- a/packages/gallery/src/components/image-viewer/video/index.module.css +++ b/packages/gallery/src/components/image-viewer/video/index.module.css @@ -6,6 +6,16 @@ height: 100%; width: auto; position: relative; + min-width: 300px; + min-height: 200px; +} + +.videoWrapper.loading { + width: 100%; + aspect-ratio: 16/9; + background-color: var(--color-static-neutral-translucent-100-inverted); + border-radius: var(--border-radius-24); + align-items: center; } .video { diff --git a/packages/gallery/src/components/image-viewer/video/index.tsx b/packages/gallery/src/components/image-viewer/video/index.tsx index 8c9e3e8563..b536ee68c2 100644 --- a/packages/gallery/src/components/image-viewer/video/index.tsx +++ b/packages/gallery/src/components/image-viewer/video/index.tsx @@ -5,6 +5,7 @@ import React, { useContext, useEffect, useRef, + useState, } from 'react'; import cn from 'classnames'; import Hls from 'hls.js'; @@ -26,8 +27,11 @@ type Props = { }; export const Video = ({ url, index, className, isActive }: Props) => { + const [videoLoaded, setVideoLoaded] = useState(false); const playerRef = useRef(null); const timer = useRef>(); + const abortController = useRef(new AbortController()); + const [videoError, setVideoError] = useState(false); const { setImageMeta, @@ -49,6 +53,47 @@ export const Video = ({ url, index, className, isActive }: Props) => { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [index]); + useEffect(() => () => abortController.current.abort(), []); + + useEffect(() => { + const video = playerRef.current; + + if (!video) { + return; + } + + const { signal } = abortController.current; + + const handleCanPlay = () => { + setVideoLoaded(true); + setVideoError(false); + setImageMeta( + { + player: playerRef, + width: video.videoWidth || 1920, + height: video.videoHeight || 1080, + loaded: true, + }, + index, + ); + }; + + const handleError = () => { + setVideoError(true); + setVideoLoaded(false); + setImageMeta({ player: { current: null }, broken: true, loaded: true }, index); + }; + + const handleLoadStart = () => { + setVideoLoaded(false); + setVideoError(false); + }; + + video.addEventListener('canplay', handleCanPlay, { signal }); + video.addEventListener('error', handleError, { signal }); + video.addEventListener('loadstart', handleLoadStart, { signal }); + }, [setImageMeta, index]); + useEffect(() => { const hls = new Hls(); @@ -60,6 +105,7 @@ export const Video = ({ url, index, className, isActive }: Props) => { hls.recoverMediaError(); break; case Hls.ErrorTypes.NETWORK_ERROR: + setVideoError(true); setImageMeta({ player: { current: null }, broken: true }, index); break; default: @@ -102,8 +148,12 @@ export const Video = ({ url, index, className, isActive }: Props) => { }, [isActive, playingVideo]); useEffect(() => { + const { signal } = abortController.current; + const handleSpacePress = (e: KeyboardEvent) => { if ((e.key === ' ' || e.code === 'Space') && isActive) { + e.preventDefault(); + e.stopPropagation(); if (playingVideo) { setPlayingVideo(false); } else { @@ -112,11 +162,7 @@ export const Video = ({ url, index, className, isActive }: Props) => { } }; - document.addEventListener('keyup', handleSpacePress); - - return () => { - document.removeEventListener('keyup', handleSpacePress); - }; + document.addEventListener('keydown', handleSpacePress, { signal }); }, [isActive, playingVideo, setPlayingVideo]); const handleWrapperClick = (e: MouseEvent) => { @@ -133,11 +179,9 @@ export const Video = ({ url, index, className, isActive }: Props) => { setHideNavigation(true); }, 3000); } - - return; + } else { + setPlayingVideo(!playingVideo); } - - setPlayingVideo(!playingVideo); }; const handleBottomButtonClick = useCallback( @@ -178,8 +222,14 @@ export const Video = ({ url, index, className, isActive }: Props) => { }; return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -
+
- {isDesktop && !playingVideo && ( + {isDesktop && !playingVideo && videoLoaded && (
@@ -200,7 +250,7 @@ export const Video = ({ url, index, className, isActive }: Props) => {
)} {isDesktop && } - {isDesktop && image?.bottomButton && ( + {isDesktop && image?.bottomButton && videoLoaded && ( {}, - timeout: 2, + timeout, }, }, { diff --git a/packages/gallery/src/types.ts b/packages/gallery/src/types.ts index 194dc1ec7c..4ebf0f400c 100644 --- a/packages/gallery/src/types.ts +++ b/packages/gallery/src/types.ts @@ -43,10 +43,12 @@ export type ImageMeta = height: number; broken?: boolean; player?: never; + loaded?: boolean; } | { - width?: never; - height?: never; + width?: number; + height?: number; broken?: boolean; player: RefObject; + loaded?: boolean; }; diff --git a/packages/gallery/src/utils/index.ts b/packages/gallery/src/utils/index.ts index 5081f80f62..c180cf6ae0 100644 --- a/packages/gallery/src/utils/index.ts +++ b/packages/gallery/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './split-filename'; export * from './utils'; export * from './constants'; +export * from './modern-download'; diff --git a/packages/gallery/src/utils/modern-download.ts b/packages/gallery/src/utils/modern-download.ts new file mode 100644 index 0000000000..064a64b759 --- /dev/null +++ b/packages/gallery/src/utils/modern-download.ts @@ -0,0 +1,92 @@ +const downloadWithBlob = (blob: Blob, fileName: string): void => { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + setTimeout(() => URL.revokeObjectURL(url), 100); +}; + +const downloadWithLink = (url: string, fileName: string): void => { + const link = document.createElement('a'); + + link.href = url; + link.download = fileName; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + +export interface DownloadOptions { + url: string; + fileName?: string; + fileType?: string; +} + +export const downloadFile = async ({ + url, + fileName = 'download', + fileType, +}: DownloadOptions): Promise => { + if ('share' in navigator && navigator?.canShare) { + const response = await fetch(url); + const blob = await response.blob(); + + const file = new File([blob], fileName, { type: blob.type || fileType }); + + if (navigator?.canShare?.({ files: [file] })) { + await navigator.share({ + files: [file], + title: `Скачать ${fileName}`, + }); + + return; + } + } + + if ('WritableStream' in window && 'ReadableStream' in window) { + const response = await fetch(url); + + if (!response.ok) throw new Error('Network error'); + + const contentLength = response.headers.get('content-length'); + const isLargeFile = contentLength && parseInt(contentLength, 10) > 10 * 1024 * 1024; // 10MB + + if (isLargeFile && response.body) { + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + + const readAllChunks = async (): Promise => { + const { done, value } = await reader.read(); + + if (!done) { + chunks.push(value); + + return readAllChunks(); + } + + return Promise.resolve(); + }; + + await readAllChunks(); + const blob = new Blob(chunks); + + downloadWithBlob(blob, fileName); + + return; + } + + const blob = await response.blob(); + + downloadWithBlob(blob, fileName); + + return; + } + + downloadWithLink(url, fileName); +}; From d81c475934a3eedd999444431812f863d10a6688 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 2 Jul 2025 17:57:18 +0300 Subject: [PATCH 3/5] =?UTF-8?q?fix(gallery):=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=86=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=D1=84=D0=BE=D0=BD=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B3=D0=BB=D1=83=D1=88=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gallery/src/components/image-preview/index.module.css | 4 ++-- packages/gallery/src/components/image-viewer/index.module.css | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gallery/src/components/image-preview/index.module.css b/packages/gallery/src/components/image-preview/index.module.css index 693fd7dd1f..9da4a97a76 100644 --- a/packages/gallery/src/components/image-preview/index.module.css +++ b/packages/gallery/src/components/image-preview/index.module.css @@ -59,7 +59,7 @@ background-color: var(--color-static-neutral-100-inverted); & .active { - background-color: var(--color-static-neutral-translucent-inverted-100); + background-color: var(--color-static-neutral-translucent-100-inverted); } } @@ -67,7 +67,7 @@ display: flex; justify-content: center; align-items: center; - background-color: var(--color-static-neutral-translucent-inverted-100); + background-color: var(--color-static-neutral-translucent-100-inverted); opacity: 0.3; &.active { diff --git a/packages/gallery/src/components/image-viewer/index.module.css b/packages/gallery/src/components/image-viewer/index.module.css index 3317c57e0a..d9102ed8ba 100644 --- a/packages/gallery/src/components/image-viewer/index.module.css +++ b/packages/gallery/src/components/image-viewer/index.module.css @@ -129,7 +129,7 @@ width: 100%; height: 100%; border-radius: var(--border-radius-8); - background-color: var(--color-static-neutral-translucent-inverted-100); + background-color: var(--color-static-neutral-translucent-100-inverted); } .brokenImgWrapper { From d9c7fba7959eb46235eeb701916d4ead5a670ebe Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 18 Aug 2025 18:48:49 +0300 Subject: [PATCH 4/5] =?UTF-8?q?feat(gallery):=20=D0=B8=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA=D0=B0=20=D0=BA=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=D0=B8=20=D1=81=D0=B2=D0=B0=D0=B9=D0=BF?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/gallery/src/Component.tsx | 35 +++++++++++++++++--- packages/gallery/src/utils/split-filename.ts | 10 ++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/packages/gallery/src/Component.tsx b/packages/gallery/src/Component.tsx index 1c9c982153..da22dd0e05 100644 --- a/packages/gallery/src/Component.tsx +++ b/packages/gallery/src/Component.tsx @@ -209,14 +209,20 @@ export const Gallery: FC = ({ }, [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 }, ); @@ -224,11 +230,30 @@ export const Gallery: FC = ({ 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; + } + } - setSwipeY(-deltaY); + if (lockedDirection === 'horizontal') { + return; + } + // vertical lock + setSwipeY(deltaY); if (deltaY < SWIPE_THRESHOLD || deltaY > -SWIPE_THRESHOLD) { onClose(); } @@ -239,6 +264,7 @@ export const Gallery: FC = ({ document.addEventListener( 'touchend', () => { + lockedDirection = null; setSwipeY(0); }, { signal }, @@ -291,6 +317,7 @@ export const Gallery: FC = ({ onUnmount={onUnmount} >
0) { - head = filename.slice(0, splitPosition); - tail = filename.slice(splitPosition); + if (dotPosition > 0) { + head = filename.slice(0, dotPosition); + tail = filename.slice(dotPosition); } return [head, tail]; From e13e2870f558cc258f3d9fc6a139b87397d391c2 Mon Sep 17 00:00:00 2001 From: Artem Date: Fri, 29 Aug 2025 15:21:52 +0300 Subject: [PATCH 5/5] =?UTF-8?q?fix(gallery):=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=20SlideInner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/gallery/src/components/image-viewer/slide.tsx | 2 +- packages/gallery/src/utils/split-filename.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/gallery/src/components/image-viewer/slide.tsx b/packages/gallery/src/components/image-viewer/slide.tsx index 40e13eb37c..873ed9f8b6 100644 --- a/packages/gallery/src/components/image-viewer/slide.tsx +++ b/packages/gallery/src/components/image-viewer/slide.tsx @@ -53,7 +53,7 @@ const SlideInner: FC = ({ children, broken, loading, isVideoVie ); return ( -
+
{broken ?
{content}
: content}
diff --git a/packages/gallery/src/utils/split-filename.ts b/packages/gallery/src/utils/split-filename.ts index 18a17f1cc8..c2b613279a 100644 --- a/packages/gallery/src/utils/split-filename.ts +++ b/packages/gallery/src/utils/split-filename.ts @@ -1,12 +1,16 @@ +const SEPARATION_POSITION_SHIFT = 3; + export function splitFilename(filename: string): [string, string] { const dotPosition = filename.lastIndexOf('.'); let head = filename; let tail = ''; - if (dotPosition > 0) { - head = filename.slice(0, dotPosition); - tail = filename.slice(dotPosition); + const splitPosition = dotPosition - SEPARATION_POSITION_SHIFT; + + if (splitPosition > 0) { + head = filename.slice(0, splitPosition); + tail = filename.slice(splitPosition); } return [head, tail];