Add lightbox preview button to image card (#2275)

* Add lightbox preview button to image card
* Always show preview button on touch screen
This commit is contained in:
WithoutPants
2022-02-03 10:41:56 +11:00
committed by GitHub
parent def9ad88b0
commit e48b2ba3e8
4 changed files with 180 additions and 86 deletions

View File

@@ -1,4 +1,5 @@
### ✨ New Features ### ✨ New Features
* Added button to image card to view image in Lightbox. ([#2275](https://github.com/stashapp/stash/pull/2275))
* Added support for submitting performer/scene drafts to stash-box. ([#2234](https://github.com/stashapp/stash/pull/2234)) * Added support for submitting performer/scene drafts to stash-box. ([#2234](https://github.com/stashapp/stash/pull/2234))
### 🎨 Improvements ### 🎨 Improvements

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, { MouseEvent } from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@@ -14,6 +14,7 @@ interface IImageCardProps {
selected: boolean | undefined; selected: boolean | undefined;
zoomIndex: number; zoomIndex: number;
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void; onSelectedChanged: (selected: boolean, shiftKey: boolean) => void;
onPreview?: (ev: MouseEvent) => void;
} }
export const ImageCard: React.FC<IImageCardProps> = ( export const ImageCard: React.FC<IImageCardProps> = (
@@ -119,6 +120,13 @@ export const ImageCard: React.FC<IImageCardProps> = (
alt={props.image.title ?? ""} alt={props.image.title ?? ""}
src={props.image.paths.thumbnail ?? ""} src={props.image.paths.thumbnail ?? ""}
/> />
{props.onPreview ? (
<div className="preview-button">
<Button onClick={props.onPreview}>
<Icon icon="search" />
</Button>
</div>
) : undefined}
</div> </div>
<RatingBanner rating={props.image.rating} /> <RatingBanner rating={props.image.rating} />
</> </>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState, MouseEvent } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import _ from "lodash"; import _ from "lodash";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@@ -30,56 +30,10 @@ interface IImageWallProps {
onChangePage: (page: number) => void; onChangePage: (page: number) => void;
currentPage: number; currentPage: number;
pageCount: number; pageCount: number;
handleImageOpen: (index: number) => void;
} }
const ImageWall: React.FC<IImageWallProps> = ({ const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
images,
onChangePage,
currentPage,
pageCount,
}) => {
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
const handleLightBoxPage = useCallback(
(direction: number) => {
if (direction === -1) {
if (currentPage === 1) {
onChangePage(pageCount);
} else {
onChangePage(currentPage - 1);
}
} else if (direction === 1) {
if (currentPage === pageCount) {
// return to the first page
onChangePage(1);
} else {
onChangePage(currentPage + 1);
}
}
},
[onChangePage, currentPage, pageCount]
);
const handleClose = useCallback(() => {
setSlideshowRunning(false);
}, [setSlideshowRunning]);
const showLightbox = useLightbox({
images,
showNavigation: false,
pageCallback: pageCount > 1 ? handleLightBoxPage : undefined,
pageHeader: `Page ${currentPage} / ${pageCount}`,
slideshowEnabled: slideshowRunning,
onClose: handleClose,
});
const handleImageOpen = useCallback(
(index) => {
setSlideshowRunning(true);
showLightbox(index, true);
},
[showLightbox]
);
const thumbs = images.map((image, index) => ( const thumbs = images.map((image, index) => (
<div <div
role="link" role="link"
@@ -104,6 +58,116 @@ const ImageWall: React.FC<IImageWallProps> = ({
); );
}; };
interface IImageListImages {
images: SlimImageDataFragment[];
filter: ListFilterModel;
selectedIds: Set<string>;
onChangePage: (page: number) => void;
pageCount: number;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
const ImageListImages: React.FC<IImageListImages> = ({
images,
filter,
selectedIds,
onChangePage,
pageCount,
onSelectChange,
}) => {
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
const handleLightBoxPage = useCallback(
(direction: number) => {
const { currentPage } = filter;
if (direction === -1) {
if (currentPage === 1) {
onChangePage(pageCount);
} else {
onChangePage(currentPage - 1);
}
} else if (direction === 1) {
if (currentPage === pageCount) {
// return to the first page
onChangePage(1);
} else {
onChangePage(currentPage + 1);
}
}
},
[onChangePage, filter, pageCount]
);
const handleClose = useCallback(() => {
setSlideshowRunning(false);
}, [setSlideshowRunning]);
const showLightbox = useLightbox({
images,
showNavigation: false,
pageCallback: pageCount > 1 ? handleLightBoxPage : undefined,
pageHeader: `Page ${filter.currentPage} / ${pageCount}`,
slideshowEnabled: slideshowRunning,
onClose: handleClose,
});
const handleImageOpen = useCallback(
(index) => {
setSlideshowRunning(true);
showLightbox(index, true);
},
[showLightbox]
);
function onPreview(index: number, ev: MouseEvent) {
handleImageOpen(index);
ev.preventDefault();
}
function renderImageCard(
index: number,
image: SlimImageDataFragment,
zoomIndex: number
) {
return (
<ImageCard
key={image.id}
image={image}
zoomIndex={zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(image.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(image.id, selected, shiftKey)
}
onPreview={(ev) => onPreview(index, ev)}
/>
);
}
if (filter.displayMode === DisplayMode.Grid) {
return (
<div className="row justify-content-center">
{images.map((image, index) =>
renderImageCard(index, image, filter.zoomIndex)
)}
</div>
);
}
if (filter.displayMode === DisplayMode.Wall) {
return (
<ImageWall
images={images}
onChangePage={onChangePage}
currentPage={filter.currentPage}
pageCount={pageCount}
handleImageOpen={handleImageOpen}
/>
);
}
// should not happen
return <></>;
};
interface IImageList { interface IImageList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
persistState?: PersistanceLevel; persistState?: PersistanceLevel;
@@ -237,23 +301,8 @@ export const ImageList: React.FC<IImageList> = ({
); );
} }
function renderImageCard( function selectChange(id: string, selected: boolean, shiftKey: boolean) {
image: SlimImageDataFragment, onSelectChange(id, selected, shiftKey);
selectedIds: Set<string>,
zoomIndex: number
) {
return (
<ImageCard
key={image.id}
image={image}
zoomIndex={zoomIndex}
selecting={selectedIds.size > 0}
selected={selectedIds.has(image.id)}
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
onSelectChange(image.id, selected, shiftKey)
}
/>
);
} }
function renderImages( function renderImages(
@@ -266,25 +315,17 @@ export const ImageList: React.FC<IImageList> = ({
if (!result.data || !result.data.findImages) { if (!result.data || !result.data.findImages) {
return; return;
} }
if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="row justify-content-center"> <ImageListImages
{result.data.findImages.images.map((image) => filter={filter}
renderImageCard(image, selectedIds, filter.zoomIndex) images={result.data.findImages.images}
)} onChangePage={onChangePage}
</div> onSelectChange={selectChange}
); pageCount={pageCount}
} selectedIds={selectedIds}
if (filter.displayMode === DisplayMode.Wall) { />
return ( );
<ImageWall
images={result.data.findImages.images}
onChangePage={onChangePage}
currentPage={filter.currentPage}
pageCount={pageCount}
/>
);
}
} }
function renderContent( function renderContent(

View File

@@ -216,6 +216,50 @@ textarea.text-input {
width: 100%; width: 100%;
} }
.preview-button {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
text-align: center;
width: 100%;
button.btn,
button.btn:not(:disabled):not(.disabled):hover,
button.btn:not(:disabled):not(.disabled):focus,
button.btn:not(:disabled):not(.disabled):active {
background: none;
border: none;
box-shadow: none;
}
.fa-icon {
color: $text-color;
filter: drop-shadow(2px 4px 6px black);
height: 5em;
opacity: 0;
transition: opacity 0.5s;
width: 5em;
&:hover {
opacity: 0.8;
}
}
@media (hover: none), (pointer: coarse) {
// always show preview button when hovering not supported
align-items: flex-end;
justify-content: right;
.fa-icon {
height: 3em;
opacity: 0.8;
width: 3em;
}
}
}
/* this is a bit of a hack, because we can't supply direct class names /* this is a bit of a hack, because we can't supply direct class names
to the react-select controls */ to the react-select controls */
/* stylelint-disable selector-class-pattern */ /* stylelint-disable selector-class-pattern */