commit
2e90601c1c
|
@ -86,6 +86,7 @@ const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
|||
};
|
||||
|
||||
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
||||
user-select: none;
|
||||
display: grid;
|
||||
grid-template-columns: ${({ columns, groups }) =>
|
||||
getTemplateColumns(columns, groups)};
|
||||
|
@ -100,6 +101,7 @@ const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
|||
`;
|
||||
|
||||
const DateContainer = styled.div<{ span: number }>`
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -181,6 +183,28 @@ const PhotoFrame = ({
|
|||
const startTime = Date.now();
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const listRef = useRef(null);
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(true);
|
||||
}
|
||||
};
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
return () => {
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInSearchMode) {
|
||||
|
@ -204,6 +228,12 @@ const PhotoFrame = ({
|
|||
setFetching({});
|
||||
}, [files, search, deleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
setRangeStart(null);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const updateUrl = (index: number) => (url: string) => {
|
||||
files[index] = {
|
||||
...files[index],
|
||||
|
@ -276,10 +306,16 @@ const PhotoFrame = ({
|
|||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSelect = (id: number) => (checked: boolean) => {
|
||||
const handleSelect = (id: number, index?: number) => (checked: boolean) => {
|
||||
if (selected.collectionID !== activeCollection) {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
}
|
||||
if (rangeStart || rangeStart === 0) {
|
||||
setRangeStart(null);
|
||||
} else if (checked) {
|
||||
setRangeStart(index);
|
||||
}
|
||||
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
[id]: checked,
|
||||
|
@ -287,19 +323,50 @@ const PhotoFrame = ({
|
|||
collectionID: activeCollection,
|
||||
}));
|
||||
};
|
||||
const onHoverOver = (index: number) => () => {
|
||||
setCurrentHover(index);
|
||||
};
|
||||
|
||||
const handleRangeSelect = (index: number) => () => {
|
||||
if (rangeStart !== index) {
|
||||
let leftEnd = -1;
|
||||
let rightEnd = -1;
|
||||
if (index < rangeStart) {
|
||||
leftEnd = index;
|
||||
rightEnd = rangeStart;
|
||||
} else {
|
||||
leftEnd = rangeStart;
|
||||
rightEnd = index;
|
||||
}
|
||||
for (let i = leftEnd; i <= rightEnd; i++) {
|
||||
handleSelect(filteredData[i].id)(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
const getThumbnail = (file: File[], index: number) => (
|
||||
<PreviewCard
|
||||
key={`tile-${file[index].id}`}
|
||||
key={`tile-${file[index].id}-selected-${
|
||||
selected[file[index].id] ?? false
|
||||
}`}
|
||||
file={file[index]}
|
||||
updateUrl={updateUrl(file[index].dataIndex)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
selectable={!isSharedCollection}
|
||||
onSelect={handleSelect(file[index].id)}
|
||||
onSelect={handleSelect(file[index].id, index)}
|
||||
selected={
|
||||
selected.collectionID === activeCollection &&
|
||||
selected[file[index].id]
|
||||
}
|
||||
selectOnClick={selected.count > 0}
|
||||
onHover={onHoverOver(index)}
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={
|
||||
isShiftKeyPressed && (rangeStart || rangeStart === 0)
|
||||
}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart + 1 && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart - 1)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,13 +15,18 @@ interface IProps {
|
|||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect?: (checked: boolean) => void;
|
||||
onHover?: () => void;
|
||||
onRangeSelect?: () => void;
|
||||
isRangeSelectActive?: boolean;
|
||||
selectOnClick?: boolean;
|
||||
isInsSelectRange?: boolean;
|
||||
}
|
||||
|
||||
const Check = styled.input`
|
||||
const Check = styled.input<{ active: boolean }>`
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
@ -34,7 +39,7 @@ const Check = styled.input`
|
|||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #fff;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: #ddd;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
vertical-align: bottom;
|
||||
|
@ -43,18 +48,19 @@ const Check = styled.input`
|
|||
line-height: 16px;
|
||||
transition: background-color 0.3s ease;
|
||||
pointer-events: inherit;
|
||||
color: #aaa;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border-right: 2px solid #fff;
|
||||
border-bottom: 2px solid #fff;
|
||||
border-right: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
transform: translate(-18px, 8px);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease;
|
||||
position: absolute;
|
||||
pointer-events: inherit;
|
||||
transform: translate(-18px, 10px) rotate(45deg);
|
||||
}
|
||||
|
||||
/** checked */
|
||||
|
@ -65,15 +71,50 @@ const Check = styled.input`
|
|||
color: #fff;
|
||||
}
|
||||
&:checked::after {
|
||||
opacity: 1;
|
||||
transform: translate(-18px, 10px) rotate(45deg);
|
||||
content: '';
|
||||
border-right: 2px solid #ddd;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
${(props) => props.active && 'opacity: 0.5 '};
|
||||
&:checked {
|
||||
opacity: 1;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HoverOverlay = styled.div<{ checked: boolean }>`
|
||||
opacity: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
outline: none;
|
||||
height: 40%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
!props.checked &&
|
||||
'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'};
|
||||
`;
|
||||
|
||||
export const InSelectRangeOverLay = styled.div<{ active: boolean }>`
|
||||
opacity: ${(props) => (!props.active ? 0 : 1)});
|
||||
left: 0;
|
||||
top: 0;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
position: absolute;
|
||||
${(props) => props.active && 'background:rgba(81, 205, 124, 0.25)'};
|
||||
`;
|
||||
|
||||
const Cont = styled.div<{ disabled: boolean; selected: boolean }>`
|
||||
background: #222;
|
||||
display: flex;
|
||||
|
@ -107,6 +148,9 @@ const Cont = styled.div<{ disabled: boolean; selected: boolean }>`
|
|||
}
|
||||
|
||||
&:hover ${Check} {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:hover ${HoverOverlay} {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
@ -123,6 +167,10 @@ export default function PreviewCard(props: IProps) {
|
|||
selected,
|
||||
onSelect,
|
||||
selectOnClick,
|
||||
onHover,
|
||||
onRangeSelect,
|
||||
isRangeSelectActive,
|
||||
isInsSelectRange,
|
||||
} = props;
|
||||
const isMounted = useRef(true);
|
||||
useLayoutEffect(() => {
|
||||
|
@ -167,24 +215,36 @@ export default function PreviewCard(props: IProps) {
|
|||
|
||||
const handleClick = () => {
|
||||
if (selectOnClick) {
|
||||
onSelect?.(!selected);
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect();
|
||||
} else {
|
||||
onSelect?.(!selected);
|
||||
}
|
||||
} else if (file?.msrc || imgSrc) {
|
||||
onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect?.();
|
||||
}
|
||||
onSelect?.(e.target.checked);
|
||||
};
|
||||
|
||||
const longPressCallback = () => {
|
||||
onSelect(!selected);
|
||||
};
|
||||
|
||||
const handleHover = () => {
|
||||
if (selectOnClick) {
|
||||
onHover();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Cont
|
||||
id={`thumb-${file?.id}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleHover}
|
||||
disabled={!forcedEnable && !file?.msrc && !imgSrc}
|
||||
selected={selected}
|
||||
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
|
||||
|
@ -193,11 +253,16 @@ export default function PreviewCard(props: IProps) {
|
|||
type="checkbox"
|
||||
checked={selected}
|
||||
onChange={handleSelect}
|
||||
active={isRangeSelectActive && isInsSelectRange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{(file?.msrc || imgSrc) && <img src={file?.msrc || imgSrc} />}
|
||||
{file?.metadata.fileType === 1 && <PlayCircleOutline />}
|
||||
<HoverOverlay checked={selected} />
|
||||
<InSelectRangeOverLay
|
||||
active={isRangeSelectActive && isInsSelectRange}
|
||||
/>
|
||||
</Cont>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue