mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
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:
@@ -1,4 +1,5 @@
|
||||
### ✨ 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))
|
||||
|
||||
### 🎨 Improvements
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { MouseEvent } from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -14,6 +14,7 @@ interface IImageCardProps {
|
||||
selected: boolean | undefined;
|
||||
zoomIndex: number;
|
||||
onSelectedChanged: (selected: boolean, shiftKey: boolean) => void;
|
||||
onPreview?: (ev: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const ImageCard: React.FC<IImageCardProps> = (
|
||||
@@ -119,6 +120,13 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
||||
alt={props.image.title ?? ""}
|
||||
src={props.image.paths.thumbnail ?? ""}
|
||||
/>
|
||||
{props.onPreview ? (
|
||||
<div className="preview-button">
|
||||
<Button onClick={props.onPreview}>
|
||||
<Icon icon="search" />
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
<RatingBanner rating={props.image.rating} />
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useState, MouseEvent } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -30,56 +30,10 @@ interface IImageWallProps {
|
||||
onChangePage: (page: number) => void;
|
||||
currentPage: number;
|
||||
pageCount: number;
|
||||
handleImageOpen: (index: number) => void;
|
||||
}
|
||||
|
||||
const ImageWall: React.FC<IImageWallProps> = ({
|
||||
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 ImageWall: React.FC<IImageWallProps> = ({ images, handleImageOpen }) => {
|
||||
const thumbs = images.map((image, index) => (
|
||||
<div
|
||||
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 {
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
persistState?: PersistanceLevel;
|
||||
@@ -237,23 +301,8 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderImageCard(
|
||||
image: SlimImageDataFragment,
|
||||
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 selectChange(id: string, selected: boolean, shiftKey: boolean) {
|
||||
onSelectChange(id, selected, shiftKey);
|
||||
}
|
||||
|
||||
function renderImages(
|
||||
@@ -266,26 +315,18 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
if (!result.data || !result.data.findImages) {
|
||||
return;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
{result.data.findImages.images.map((image) =>
|
||||
renderImageCard(image, selectedIds, filter.zoomIndex)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return (
|
||||
<ImageWall
|
||||
<ImageListImages
|
||||
filter={filter}
|
||||
images={result.data.findImages.images}
|
||||
onChangePage={onChangePage}
|
||||
currentPage={filter.currentPage}
|
||||
onSelectChange={selectChange}
|
||||
pageCount={pageCount}
|
||||
selectedIds={selectedIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent(
|
||||
result: FindImagesQueryResult,
|
||||
|
||||
@@ -216,6 +216,50 @@ textarea.text-input {
|
||||
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
|
||||
to the react-select controls */
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
|
||||
Reference in New Issue
Block a user