diff --git a/ui/v2.5/src/components/Changelog/versions/v0130.md b/ui/v2.5/src/components/Changelog/versions/v0130.md index 82c480ea1..9a9c42a56 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0130.md +++ b/ui/v2.5/src/components/Changelog/versions/v0130.md @@ -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 diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 0014b434c..76b490682 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -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 = ( @@ -119,6 +120,13 @@ export const ImageCard: React.FC = ( alt={props.image.title ?? ""} src={props.image.paths.thumbnail ?? ""} /> + {props.onPreview ? ( +
+ +
+ ) : undefined} diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index f95624eb1..f08f7fde4 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -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 = ({ - images, - onChangePage, - currentPage, - pageCount, -}) => { - const [slideshowRunning, setSlideshowRunning] = useState(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 = ({ images, handleImageOpen }) => { const thumbs = images.map((image, index) => (
= ({ ); }; +interface IImageListImages { + images: SlimImageDataFragment[]; + filter: ListFilterModel; + selectedIds: Set; + onChangePage: (page: number) => void; + pageCount: number; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; +} + +const ImageListImages: React.FC = ({ + images, + filter, + selectedIds, + onChangePage, + pageCount, + onSelectChange, +}) => { + const [slideshowRunning, setSlideshowRunning] = useState(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 ( + 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 ( +
+ {images.map((image, index) => + renderImageCard(index, image, filter.zoomIndex) + )} +
+ ); + } + if (filter.displayMode === DisplayMode.Wall) { + return ( + + ); + } + + // should not happen + return <>; +}; + interface IImageList { filterHook?: (filter: ListFilterModel) => ListFilterModel; persistState?: PersistanceLevel; @@ -237,23 +301,8 @@ export const ImageList: React.FC = ({ ); } - function renderImageCard( - image: SlimImageDataFragment, - selectedIds: Set, - zoomIndex: number - ) { - return ( - 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,25 +315,17 @@ export const ImageList: React.FC = ({ if (!result.data || !result.data.findImages) { return; } - if (filter.displayMode === DisplayMode.Grid) { - return ( -
- {result.data.findImages.images.map((image) => - renderImageCard(image, selectedIds, filter.zoomIndex) - )} -
- ); - } - if (filter.displayMode === DisplayMode.Wall) { - return ( - - ); - } + + return ( + + ); } function renderContent( diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index a67e1e504..0a7399a80 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -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 */