Skip to content

Commit e48b2ba

Browse files
authored
Add lightbox preview button to image card (stashapp#2275)
* Add lightbox preview button to image card * Always show preview button on touch screen
1 parent def9ad8 commit e48b2ba

File tree

4 files changed

+156
-62
lines changed

4 files changed

+156
-62
lines changed

ui/v2.5/src/components/Changelog/versions/v0130.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
### ✨ New Features
2+
* Added button to image card to view image in Lightbox. ([#2275](https://github.com/stashapp/stash/pull/2275))
23
* Added support for submitting performer/scene drafts to stash-box. ([#2234](https://github.com/stashapp/stash/pull/2234))
34

45
### 🎨 Improvements

ui/v2.5/src/components/Images/ImageCard.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react";
1+
import React, { MouseEvent } from "react";
22
import { Button, ButtonGroup } from "react-bootstrap";
33
import cx from "classnames";
44
import * as GQL from "src/core/generated-graphql";
@@ -14,6 +14,7 @@ interface IImageCardProps {
1414
selected: boolean | undefined;
1515
zoomIndex: number;
1616
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void;
17+
onPreview?: (ev: MouseEvent) => void;
1718
}
1819

1920
export const ImageCard: React.FC<IImageCardProps> = (
@@ -119,6 +120,13 @@ export const ImageCard: React.FC<IImageCardProps> = (
119120
alt={props.image.title ?? ""}
120121
src={props.image.paths.thumbnail ?? ""}
121122
/>
123+
{props.onPreview ? (
124+
<div className="preview-button">
125+
<Button onClick={props.onPreview}>
126+
<Icon icon="search" />
127+
</Button>
128+
</div>
129+
) : undefined}
122130
</div>
123131
<RatingBanner rating={props.image.rating} />
124132
</>

ui/v2.5/src/components/Images/ImageList.tsx

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useState } from "react";
1+
import React, { useCallback, useState, MouseEvent } from "react";
22
import { useIntl } from "react-intl";
33
import _ from "lodash";
44
import { useHistory } from "react-router-dom";
@@ -30,17 +30,55 @@ interface IImageWallProps {
3030
onChangePage: (page: number) => void;
3131
currentPage: number;
3232
pageCount: number;
33+
handleImageOpen: (index: number) => void;
3334
}
3435

35-
const ImageWall: React.FC<IImageWallProps> = ({
36+
const ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
37+
const thumbs = images.map((image, index) => (
38+
<div
39+
role="link"
40+
tabIndex={index}
41+
key={image.id}
42+
onClick={() => handleImageOpen(index)}
43+
onKeyPress={() => handleImageOpen(index)}
44+
>
45+
<img
46+
src={image.paths.thumbnail ?? ""}
47+
loading="lazy"
48+
className="gallery-image"
49+
alt={image.title ?? TextUtils.fileNameFromPath(image.path)}
50+
/>
51+
</div>
52+
));
53+
54+
return (
55+
<div className="gallery">
56+
<div className="flexbin">{thumbs}</div>
57+
</div>
58+
);
59+
};
60+
61+
interface IImageListImages {
62+
images: SlimImageDataFragment[];
63+
filter: ListFilterModel;
64+
selectedIds: Set<string>;
65+
onChangePage: (page: number) => void;
66+
pageCount: number;
67+
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
68+
}
69+
70+
const ImageListImages: React.FC<IImageListImages> = ({
3671
images,
72+
filter,
73+
selectedIds,
3774
onChangePage,
38-
currentPage,
3975
pageCount,
76+
onSelectChange,
4077
}) => {
4178
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
4279
const handleLightBoxPage = useCallback(
4380
(direction: number) => {
81+
const { currentPage } = filter;
4482
if (direction === -1) {
4583
if (currentPage === 1) {
4684
onChangePage(pageCount);
@@ -56,7 +94,7 @@ const ImageWall: React.FC<IImageWallProps> = ({
5694
}
5795
}
5896
},
59-
[onChangePage, currentPage, pageCount]
97+
[onChangePage, filter, pageCount]
6098
);
6199

62100
const handleClose = useCallback(() => {
@@ -67,7 +105,7 @@ const ImageWall: React.FC<IImageWallProps> = ({
67105
images,
68106
showNavigation: false,
69107
pageCallback: pageCount > 1 ? handleLightBoxPage : undefined,
70-
pageHeader: `Page ${currentPage} / ${pageCount}`,
108+
pageHeader: `Page ${filter.currentPage} / ${pageCount}`,
71109
slideshowEnabled: slideshowRunning,
72110
onClose: handleClose,
73111
});
@@ -80,28 +118,54 @@ const ImageWall: React.FC<IImageWallProps> = ({
80118
[showLightbox]
81119
);
82120

83-
const thumbs = images.map((image, index) => (
84-
<div
85-
role="link"
86-
tabIndex={index}
87-
key={image.id}
88-
onClick={() => handleImageOpen(index)}
89-
onKeyPress={() => handleImageOpen(index)}
90-
>
91-
<img
92-
src={image.paths.thumbnail ?? ""}
93-
loading="lazy"
94-
className="gallery-image"
95-
alt={image.title ?? TextUtils.fileNameFromPath(image.path)}
121+
function onPreview(index: number, ev: MouseEvent) {
122+
handleImageOpen(index);
123+
ev.preventDefault();
124+
}
125+
126+
function renderImageCard(
127+
index: number,
128+
image: SlimImageDataFragment,
129+
zoomIndex: number
130+
) {
131+
return (
132+
<ImageCard
133+
key={image.id}
134+
image={image}
135+
zoomIndex={zoomIndex}
136+
selecting={selectedIds.size > 0}
137+
selected={selectedIds.has(image.id)}
138+
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
139+
onSelectChange(image.id, selected, shiftKey)
140+
}
141+
onPreview={(ev) => onPreview(index, ev)}
96142
/>
97-
</div>
98-
));
143+
);
144+
}
99145

100-
return (
101-
<div className="gallery">
102-
<div className="flexbin">{thumbs}</div>
103-
</div>
104-
);
146+
if (filter.displayMode === DisplayMode.Grid) {
147+
return (
148+
<div className="row justify-content-center">
149+
{images.map((image, index) =>
150+
renderImageCard(index, image, filter.zoomIndex)
151+
)}
152+
</div>
153+
);
154+
}
155+
if (filter.displayMode === DisplayMode.Wall) {
156+
return (
157+
<ImageWall
158+
images={images}
159+
onChangePage={onChangePage}
160+
currentPage={filter.currentPage}
161+
pageCount={pageCount}
162+
handleImageOpen={handleImageOpen}
163+
/>
164+
);
165+
}
166+
167+
// should not happen
168+
return <></>;
105169
};
106170

107171
interface IImageList {
@@ -237,23 +301,8 @@ export const ImageList: React.FC<IImageList> = ({
237301
);
238302
}
239303

240-
function renderImageCard(
241-
image: SlimImageDataFragment,
242-
selectedIds: Set<string>,
243-
zoomIndex: number
244-
) {
245-
return (
246-
<ImageCard
247-
key={image.id}
248-
image={image}
249-
zoomIndex={zoomIndex}
250-
selecting={selectedIds.size > 0}
251-
selected={selectedIds.has(image.id)}
252-
onSelectedChanged={(selected: boolean, shiftKey: boolean) =>
253-
onSelectChange(image.id, selected, shiftKey)
254-
}
255-
/>
256-
);
304+
function selectChange(id: string, selected: boolean, shiftKey: boolean) {
305+
onSelectChange(id, selected, shiftKey);
257306
}
258307

259308
function renderImages(
@@ -266,25 +315,17 @@ export const ImageList: React.FC<IImageList> = ({
266315
if (!result.data || !result.data.findImages) {
267316
return;
268317
}
269-
if (filter.displayMode === DisplayMode.Grid) {
270-
return (
271-
<div className="row justify-content-center">
272-
{result.data.findImages.images.map((image) =>
273-
renderImageCard(image, selectedIds, filter.zoomIndex)
274-
)}
275-
</div>
276-
);
277-
}
278-
if (filter.displayMode === DisplayMode.Wall) {
279-
return (
280-
<ImageWall
281-
images={result.data.findImages.images}
282-
onChangePage={onChangePage}
283-
currentPage={filter.currentPage}
284-
pageCount={pageCount}
285-
/>
286-
);
287-
}
318+
319+
return (
320+
<ImageListImages
321+
filter={filter}
322+
images={result.data.findImages.images}
323+
onChangePage={onChangePage}
324+
onSelectChange={selectChange}
325+
pageCount={pageCount}
326+
selectedIds={selectedIds}
327+
/>
328+
);
288329
}
289330

290331
function renderContent(

ui/v2.5/src/index.scss

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,50 @@ textarea.text-input {
216216
width: 100%;
217217
}
218218

219+
.preview-button {
220+
align-items: center;
221+
display: flex;
222+
height: 100%;
223+
justify-content: center;
224+
position: absolute;
225+
text-align: center;
226+
width: 100%;
227+
228+
button.btn,
229+
button.btn:not(:disabled):not(.disabled):hover,
230+
button.btn:not(:disabled):not(.disabled):focus,
231+
button.btn:not(:disabled):not(.disabled):active {
232+
background: none;
233+
border: none;
234+
box-shadow: none;
235+
}
236+
237+
.fa-icon {
238+
color: $text-color;
239+
filter: drop-shadow(2px 4px 6px black);
240+
height: 5em;
241+
opacity: 0;
242+
transition: opacity 0.5s;
243+
width: 5em;
244+
245+
&:hover {
246+
opacity: 0.8;
247+
}
248+
}
249+
250+
@media (hover: none), (pointer: coarse) {
251+
// always show preview button when hovering not supported
252+
align-items: flex-end;
253+
justify-content: right;
254+
255+
.fa-icon {
256+
height: 3em;
257+
opacity: 0.8;
258+
width: 3em;
259+
}
260+
}
261+
}
262+
219263
/* this is a bit of a hack, because we can't supply direct class names
220264
to the react-select controls */
221265
/* stylelint-disable selector-class-pattern */

0 commit comments

Comments
 (0)