mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 21:04:37 +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
|
### ✨ 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
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,26 +315,18 @@ 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)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (filter.displayMode === DisplayMode.Wall) {
|
|
||||||
return (
|
|
||||||
<ImageWall
|
|
||||||
images={result.data.findImages.images}
|
images={result.data.findImages.images}
|
||||||
onChangePage={onChangePage}
|
onChangePage={onChangePage}
|
||||||
currentPage={filter.currentPage}
|
onSelectChange={selectChange}
|
||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
|
selectedIds={selectedIds}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function renderContent(
|
function renderContent(
|
||||||
result: FindImagesQueryResult,
|
result: FindImagesQueryResult,
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
Reference in New Issue
Block a user