mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-19 16:51:40 +03:00
Refactor Carousel
This commit is contained in:
parent
0b8460cd55
commit
dc37100442
2 changed files with 160 additions and 116 deletions
src
|
@ -2,7 +2,13 @@ import './status.css';
|
||||||
|
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import mem from 'mem';
|
import mem from 'mem';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import {
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'preact/hooks';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import useResizeObserver from 'use-resize-observer';
|
import useResizeObserver from 'use-resize-observer';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
@ -15,6 +21,7 @@ import htmlContentLength from '../utils/html-content-length';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import useDebouncedCallback from '../utils/useDebouncedCallback';
|
||||||
import visibilityIconsMap from '../utils/visibility-icons-map';
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
||||||
|
|
||||||
import Avatar from './avatar';
|
import Avatar from './avatar';
|
||||||
|
@ -132,16 +139,6 @@ function Status({
|
||||||
};
|
};
|
||||||
|
|
||||||
const [showMediaModal, setShowMediaModal] = useState(false);
|
const [showMediaModal, setShowMediaModal] = useState(false);
|
||||||
const carouselFocusItem = useRef(null);
|
|
||||||
const prevShowMediaModal = useRef(showMediaModal);
|
|
||||||
useEffect(() => {
|
|
||||||
if (showMediaModal !== false) {
|
|
||||||
carouselFocusItem.current?.node?.scrollIntoView({
|
|
||||||
behavior: prevShowMediaModal.current === false ? 'auto' : 'smooth',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
prevShowMediaModal.current = showMediaModal;
|
|
||||||
}, [showMediaModal]);
|
|
||||||
|
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
return (
|
return (
|
||||||
|
@ -157,7 +154,6 @@ function Status({
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
|
|
||||||
const carouselRef = useRef(null);
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const spoilerContentRef = useRef(null);
|
const spoilerContentRef = useRef(null);
|
||||||
|
@ -563,111 +559,13 @@ function Status({
|
||||||
</div>
|
</div>
|
||||||
{showMediaModal !== false && (
|
{showMediaModal !== false && (
|
||||||
<Modal>
|
<Modal>
|
||||||
<div
|
<Carousel
|
||||||
ref={carouselRef}
|
mediaAttachments={mediaAttachments}
|
||||||
class="carousel"
|
index={showMediaModal}
|
||||||
onClick={(e) => {
|
onClose={() => {
|
||||||
if (
|
setShowMediaModal(false);
|
||||||
e.target.classList.contains('carousel-item') ||
|
|
||||||
e.target.classList.contains('media')
|
|
||||||
) {
|
|
||||||
setShowMediaModal(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
tabindex="0"
|
/>
|
||||||
>
|
|
||||||
{mediaAttachments?.map((media, i) => {
|
|
||||||
const { blurhash } = media;
|
|
||||||
const rgbAverageColor = blurhash
|
|
||||||
? getBlurHashAverageColor(blurhash)
|
|
||||||
: null;
|
|
||||||
return (
|
|
||||||
<InView
|
|
||||||
class="carousel-item"
|
|
||||||
style={{
|
|
||||||
backgroundColor:
|
|
||||||
rgbAverageColor &&
|
|
||||||
`rgba(${rgbAverageColor.join(',')}, .5)`,
|
|
||||||
}}
|
|
||||||
tabindex="0"
|
|
||||||
key={media.id}
|
|
||||||
ref={i === showMediaModal ? carouselFocusItem : null}
|
|
||||||
// InView options
|
|
||||||
root={carouselRef.current}
|
|
||||||
threshold={1}
|
|
||||||
onChange={(inView) => {
|
|
||||||
if (inView) {
|
|
||||||
setShowMediaModal(i);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Media media={media} showOriginal />
|
|
||||||
</InView>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div class="carousel-top-controls">
|
|
||||||
<span />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="carousel-button plain2"
|
|
||||||
onClick={() => setShowMediaModal(false)}
|
|
||||||
>
|
|
||||||
<Icon icon="x" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{mediaAttachments?.length > 1 && (
|
|
||||||
<div class="carousel-controls">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="carousel-button plain2"
|
|
||||||
hidden={showMediaModal === 0}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowMediaModal(
|
|
||||||
(showMediaModal - 1 + mediaAttachments.length) %
|
|
||||||
mediaAttachments.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-left" />
|
|
||||||
</button>
|
|
||||||
<span class="carousel-dots">
|
|
||||||
{mediaAttachments?.map((media, i) => (
|
|
||||||
<button
|
|
||||||
key={media.id}
|
|
||||||
type="button"
|
|
||||||
disabled={i === showMediaModal}
|
|
||||||
class={`plain carousel-dot ${
|
|
||||||
i === showMediaModal ? 'active' : ''
|
|
||||||
}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowMediaModal(i);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
•
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="carousel-button plain2"
|
|
||||||
hidden={showMediaModal === mediaAttachments.length - 1}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowMediaModal(
|
|
||||||
(showMediaModal + 1) % mediaAttachments.length,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-right" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
{!!showEdited && (
|
{!!showEdited && (
|
||||||
|
@ -1203,4 +1101,127 @@ function StatusButton({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Carousel({ mediaAttachments, index = 0, onClose = () => {} }) {
|
||||||
|
const carouselRef = useRef(null);
|
||||||
|
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(index);
|
||||||
|
const carouselFocusItem = useRef(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
carouselFocusItem.current?.node?.scrollIntoView();
|
||||||
|
}, []);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
carouselFocusItem.current?.node?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}, [currentIndex]);
|
||||||
|
|
||||||
|
const onSnap = useDebouncedCallback((inView, i) => {
|
||||||
|
if (inView) {
|
||||||
|
setCurrentIndex(i);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={carouselRef}
|
||||||
|
class="carousel"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (
|
||||||
|
e.target.classList.contains('carousel-item') ||
|
||||||
|
e.target.classList.contains('media')
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
{mediaAttachments?.map((media, i) => {
|
||||||
|
const { blurhash } = media;
|
||||||
|
const rgbAverageColor = blurhash
|
||||||
|
? getBlurHashAverageColor(blurhash)
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<InView
|
||||||
|
class="carousel-item"
|
||||||
|
style={{
|
||||||
|
backgroundColor:
|
||||||
|
rgbAverageColor && `rgba(${rgbAverageColor.join(',')}, .5)`,
|
||||||
|
}}
|
||||||
|
tabindex="0"
|
||||||
|
key={media.id}
|
||||||
|
ref={i === currentIndex ? carouselFocusItem : null} // InView options
|
||||||
|
root={carouselRef.current}
|
||||||
|
threshold={1}
|
||||||
|
onChange={(inView) => onSnap(inView, i)}
|
||||||
|
>
|
||||||
|
<Media media={media} showOriginal />
|
||||||
|
</InView>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div class="carousel-top-controls">
|
||||||
|
<span />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-button plain2"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
>
|
||||||
|
<Icon icon="x" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{mediaAttachments?.length > 1 && (
|
||||||
|
<div class="carousel-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-button plain2"
|
||||||
|
hidden={currentIndex === 0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setCurrentIndex(
|
||||||
|
(currentIndex - 1 + mediaAttachments.length) %
|
||||||
|
mediaAttachments.length,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-left" />
|
||||||
|
</button>
|
||||||
|
<span class="carousel-dots">
|
||||||
|
{mediaAttachments?.map((media, i) => (
|
||||||
|
<button
|
||||||
|
key={media.id}
|
||||||
|
type="button"
|
||||||
|
disabled={i === currentIndex}
|
||||||
|
class={`plain carousel-dot ${
|
||||||
|
i === currentIndex ? 'active' : ''
|
||||||
|
}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setCurrentIndex(i);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
•
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-button plain2"
|
||||||
|
hidden={currentIndex === mediaAttachments.length - 1}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setCurrentIndex((currentIndex + 1) % mediaAttachments.length);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-right" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default Status;
|
export default Status;
|
||||||
|
|
23
src/utils/useDebouncedCallback.js
Normal file
23
src/utils/useDebouncedCallback.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { useCallback, useRef } from 'preact/hooks';
|
||||||
|
|
||||||
|
export default function useDebouncedCallback(
|
||||||
|
callback,
|
||||||
|
delay,
|
||||||
|
dependencies = [],
|
||||||
|
) {
|
||||||
|
const timeout = useRef();
|
||||||
|
|
||||||
|
const comboDeps = dependencies
|
||||||
|
? [callback, delay, ...dependencies]
|
||||||
|
: [callback, delay];
|
||||||
|
|
||||||
|
return useCallback((...args) => {
|
||||||
|
if (timeout.current != null) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout.current = setTimeout(() => {
|
||||||
|
callback(...args);
|
||||||
|
}, delay);
|
||||||
|
}, comboDeps);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue