Add checkbox controls to Wall View and Tagger for Scenes, Scene Markers, Images, and Galleries (#6476)

This commit is contained in:
RyanAtNight 2026-01-11 16:06:57 -08:00 committed by GitHub
parent c9fa3b76d9
commit 579fc66275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 378 additions and 40 deletions

View File

@ -153,7 +153,15 @@ export const GalleryList: React.FC<IGalleryList> = PatchComponent(
<div className="row">
<div className={`GalleryWall zoom-${filter.zoomIndex}`}>
{result.data.findGalleries.galleries.map((gallery) => (
<GalleryWallCard key={gallery.id} gallery={gallery} />
<GalleryWallCard
key={gallery.id}
gallery={gallery}
selected={selectedIds.has(gallery.id)}
onSelectedChanged={(selected, shiftKey) =>
onSelectChange(gallery.id, selected, shiftKey)
}
selecting={selectedIds.size > 0}
/>
))}
</div>
</div>

View File

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { Form } from "react-bootstrap";
import { useIntl } from "react-intl";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
@ -8,6 +9,7 @@ import { useGalleryLightbox } from "src/hooks/Lightbox/hooks";
import { galleryTitle } from "src/core/galleries";
import { RatingSystem } from "../Shared/Rating/RatingSystem";
import { GalleryPreviewScrubber } from "./GalleryPreviewScrubber";
import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect";
import cx from "classnames";
const CLASSNAME = "GalleryWallCard";
@ -18,6 +20,9 @@ const CLASSNAME_IMG_CONTAIN = `${CLASSNAME}-img-contain`;
interface IProps {
gallery: GQL.SlimGalleryDataFragment;
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
type Orientation = "landscape" | "portrait";
@ -26,7 +31,12 @@ function getOrientation(width: number, height: number): Orientation {
return width > height ? "landscape" : "portrait";
}
const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
const GalleryWallCard: React.FC<IProps> = ({
gallery,
selected,
onSelectedChanged,
selecting,
}) => {
const intl = useIntl();
const [coverOrientation, setCoverOrientation] =
React.useState<Orientation>("landscape");
@ -34,6 +44,12 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
React.useState<Orientation>("landscape");
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
const { dragProps } = useDragMoveSelect({
selecting: selecting || false,
selected: selected || false,
onSelectedChanged: onSelectedChanged,
});
const cover = gallery?.paths.cover;
function onCoverLoad(e: React.SyntheticEvent<HTMLImageElement, Event>) {
@ -58,6 +74,14 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")]
: performerNames;
function handleCardClick(event: React.MouseEvent) {
if (selecting && onSelectedChanged) {
onSelectedChanged(!selected, event.shiftKey);
return;
}
showLightboxStart();
}
async function showLightboxStart() {
if (gallery.image_count === 0) {
return;
@ -69,15 +93,32 @@ const GalleryWallCard: React.FC<IProps> = ({ gallery }) => {
const imgClassname =
imageOrientation !== coverOrientation ? CLASSNAME_IMG_CONTAIN : "";
let shiftKey = false;
return (
<>
<section
className={`${CLASSNAME} ${CLASSNAME}-${coverOrientation}`}
onClick={showLightboxStart}
onKeyPress={showLightboxStart}
className={`${CLASSNAME} ${CLASSNAME}-${coverOrientation} wall-item`}
onClick={handleCardClick}
onKeyPress={() => showLightboxStart()}
role="button"
tabIndex={0}
{...dragProps}
>
{onSelectedChanged && (
<Form.Control
type="checkbox"
className="wall-item-check mousetrap"
checked={selected}
onChange={() => onSelectedChanged(!selected, shiftKey)}
onClick={(
event: React.MouseEvent<HTMLInputElement, MouseEvent>
) => {
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
)}
<RatingSystem value={gallery.rating100} disabled withoutContext />
<img
loading="lazy"

View File

@ -35,6 +35,9 @@ interface IImageWallProps {
pageCount: number;
handleImageOpen: (index: number) => void;
zoomIndex: number;
selectedIds?: Set<string>;
onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
const zoomWidths = [280, 340, 480, 640];
@ -49,6 +52,9 @@ const ImageWall: React.FC<IImageWallProps> = ({
images,
zoomIndex,
handleImageOpen,
selectedIds,
onSelectChange,
selecting,
}) => {
const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui;
@ -121,9 +127,26 @@ const ImageWall: React.FC<IImageWallProps> = ({
? props.photo.height
: targetRowHeight(containerRef.current?.offsetWidth ?? 0) *
maxHeightFactor;
return <ImageWallItem {...props} maxHeight={maxHeight} />;
const imageId = props.photo.key;
if (!imageId) {
return null;
}
return (
<ImageWallItem
{...props}
maxHeight={maxHeight}
selected={selectedIds?.has(imageId)}
onSelectedChanged={
onSelectChange
? (selected, shiftKey) =>
onSelectChange(imageId, selected, shiftKey)
: undefined
}
selecting={selecting}
/>
);
},
[targetRowHeight]
[targetRowHeight, selectedIds, onSelectChange, selecting]
);
return (
@ -258,6 +281,9 @@ const ImageListImages: React.FC<IImageListImages> = ({
pageCount={pageCount}
handleImageOpen={handleImageOpen}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
selecting={!!selectedIds && selectedIds.size > 0}
/>
);
}

View File

@ -1,32 +1,50 @@
import React from "react";
import { Form } from "react-bootstrap";
import type { RenderImageProps } from "react-photo-gallery";
import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect";
interface IExtraProps {
maxHeight: number;
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
export const ImageWallItem: React.FC<RenderImageProps & IExtraProps> = (
props: RenderImageProps & IExtraProps
) => {
const { dragProps } = useDragMoveSelect({
selecting: props.selecting || false,
selected: props.selected || false,
onSelectedChanged: props.onSelectedChanged,
});
const height = Math.min(props.maxHeight, props.photo.height);
const zoomFactor = height / props.photo.height;
const width = props.photo.width * zoomFactor;
type style = Record<string, string | number | undefined>;
var imgStyle: style = {
var divStyle: style = {
margin: props.margin,
display: "block",
position: "relative",
};
if (props.direction === "column") {
imgStyle.position = "absolute";
imgStyle.left = props.left;
imgStyle.top = props.top;
divStyle.position = "absolute";
divStyle.left = props.left;
divStyle.top = props.top;
}
var handleClick = function handleClick(
event: React.MouseEvent<Element, MouseEvent>
) {
if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, event.shiftKey);
event.preventDefault();
event.stopPropagation();
return;
}
if (props.onClick) {
props.onClick(event, { index: props.index });
}
@ -35,19 +53,39 @@ export const ImageWallItem: React.FC<RenderImageProps & IExtraProps> = (
const video = props.photo.src.includes("preview");
const ImagePreview = video ? "video" : "img";
let shiftKey = false;
return (
<ImagePreview
loop={video}
muted={video}
playsInline={video}
autoPlay={video}
key={props.photo.key}
style={imgStyle}
src={props.photo.src}
width={width}
height={height}
alt={props.photo.alt}
<div
className="wall-item"
style={divStyle}
onClick={handleClick}
/>
{...dragProps}
>
{props.onSelectedChanged && (
<Form.Control
type="checkbox"
className="wall-item-check mousetrap"
checked={props.selected}
onChange={() => props.onSelectedChanged!(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
)}
<ImagePreview
loop={video}
muted={video}
playsInline={video}
autoPlay={video}
key={props.photo.key}
src={props.photo.src}
width={width}
height={height}
alt={props.photo.alt}
onClick={handleClick}
/>
</div>
);
};

View File

@ -235,11 +235,20 @@ const SceneList: React.FC<{
scenes={scenes}
sceneQueue={queue}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
if (filter.displayMode === DisplayMode.Tagger) {
return <Tagger scenes={scenes} queue={queue} />;
return (
<Tagger
scenes={scenes}
queue={queue}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
return null;

View File

@ -101,6 +101,8 @@ export const SceneMarkerList: React.FC<ISceneMarkerList> = PatchComponent(
<MarkerWallPanel
markers={result.data.findSceneMarkers.scene_markers}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}

View File

@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import Gallery, {
GalleryI,
@ -10,6 +11,7 @@ import { objectTitle } from "src/core/files";
import { Link, useHistory } from "react-router-dom";
import { TruncatedText } from "../Shared/TruncatedText";
import TextUtils from "src/utils/text";
import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect";
import cx from "classnames";
import NavUtils from "src/utils/navigation";
import { markerTitle } from "src/core/markers";
@ -35,11 +37,20 @@ interface IMarkerPhoto {
interface IExtraProps {
maxHeight: number;
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
export const MarkerWallItem: React.FC<
RenderImageProps<IMarkerPhoto> & IExtraProps
> = (props: RenderImageProps<IMarkerPhoto> & IExtraProps) => {
const { dragProps } = useDragMoveSelect({
selecting: props.selecting || false,
selected: props.selected || false,
onSelectedChanged: props.onSelectedChanged,
});
const { configuration } = useConfigurationContext();
const playSound = configuration?.interface.soundOnPreview ?? false;
const showTitle = configuration?.interface.wallShowTitle ?? false;
@ -63,6 +74,12 @@ export const MarkerWallItem: React.FC<
}
var handleClick = function handleClick(event: React.MouseEvent) {
if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, event.shiftKey);
event.preventDefault();
event.stopPropagation();
return;
}
if (props.onClick) {
props.onClick(event, { index: props.index });
}
@ -75,16 +92,32 @@ export const MarkerWallItem: React.FC<
const title = wallItemTitle(marker);
const tagNames = marker.tags.map((p) => p.name);
let shiftKey = false;
return (
<div
className={cx("wall-item", { "show-title": showTitle })}
role="button"
onClick={handleClick}
{...dragProps}
style={{
...divStyle,
width,
height,
}}
>
{props.onSelectedChanged && (
<Form.Control
type="checkbox"
className="wall-item-check mousetrap"
checked={props.selected}
onChange={() => props.onSelectedChanged!(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
)}
<ImagePreview
loading="lazy"
loop={video}
@ -124,6 +157,9 @@ export const MarkerWallItem: React.FC<
interface IMarkerWallProps {
markers: GQL.SceneMarkerDataFragment[];
zoomIndex: number;
selectedIds?: Set<string>;
onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
@ -163,7 +199,13 @@ const breakpointZoomHeights = [
{ minWidth: 1400, heights: [160, 240, 300, 480] },
];
const MarkerWall: React.FC<IMarkerWallProps> = ({ markers, zoomIndex }) => {
const MarkerWall: React.FC<IMarkerWallProps> = ({
markers,
zoomIndex,
selectedIds,
onSelectChange,
selecting,
}) => {
const history = useHistory();
const containerRef = React.useRef<HTMLDivElement>(null);
@ -233,6 +275,7 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers, zoomIndex }) => {
const renderImage = useCallback(
(props: RenderImageProps<IMarkerPhoto>) => {
const markerId = props.photo.marker.id;
return (
<MarkerWallItem
{...props}
@ -240,10 +283,18 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers, zoomIndex }) => {
targetRowHeight(containerRef.current?.offsetWidth ?? 0) *
maxHeightFactor
}
selected={selectedIds?.has(markerId)}
onSelectedChanged={
onSelectChange
? (selected, shiftKey) =>
onSelectChange(markerId, selected, shiftKey)
: undefined
}
selecting={selecting}
/>
);
},
[targetRowHeight]
[targetRowHeight, selectedIds, onSelectChange, selecting]
);
return (
@ -266,11 +317,24 @@ const MarkerWall: React.FC<IMarkerWallProps> = ({ markers, zoomIndex }) => {
interface IMarkerWallPanelProps {
markers: GQL.SceneMarkerDataFragment[];
zoomIndex: number;
selectedIds?: Set<string>;
onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const MarkerWallPanel: React.FC<IMarkerWallPanelProps> = ({
markers,
zoomIndex,
selectedIds,
onSelectChange,
}) => {
return <MarkerWall markers={markers} zoomIndex={zoomIndex} />;
const selecting = !!selectedIds && selectedIds.size > 0;
return (
<MarkerWall
markers={markers}
zoomIndex={zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
selecting={selecting}
/>
);
};

View File

@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { SceneQueue } from "src/models/sceneQueue";
import Gallery, {
@ -12,6 +13,7 @@ import { Link, useHistory } from "react-router-dom";
import { TruncatedText } from "../Shared/TruncatedText";
import TextUtils from "src/utils/text";
import { useIntl } from "react-intl";
import { useDragMoveSelect } from "../Shared/GridCard/dragMoveSelect";
import cx from "classnames";
interface IScenePhoto {
@ -22,6 +24,9 @@ interface IScenePhoto {
interface IExtraProps {
maxHeight: number;
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
export const SceneWallItem: React.FC<
@ -29,6 +34,12 @@ export const SceneWallItem: React.FC<
> = (props: RenderImageProps<IScenePhoto> & IExtraProps) => {
const intl = useIntl();
const { dragProps } = useDragMoveSelect({
selecting: props.selecting || false,
selected: props.selected || false,
onSelectedChanged: props.onSelectedChanged,
});
const { configuration } = useConfigurationContext();
const playSound = configuration?.interface.soundOnPreview ?? false;
const showTitle = configuration?.interface.wallShowTitle ?? false;
@ -52,6 +63,12 @@ export const SceneWallItem: React.FC<
}
var handleClick = function handleClick(event: React.MouseEvent) {
if (props.selecting && props.onSelectedChanged) {
props.onSelectedChanged(!props.selected, event.shiftKey);
event.preventDefault();
event.stopPropagation();
return;
}
if (props.onClick) {
props.onClick(event, { index: props.index });
}
@ -68,16 +85,32 @@ export const SceneWallItem: React.FC<
? [...performerNames.slice(0, -2), performerNames.slice(-2).join(" & ")]
: performerNames;
let shiftKey = false;
return (
<div
className={cx("wall-item", { "show-title": showTitle })}
role="button"
onClick={handleClick}
{...dragProps}
style={{
...divStyle,
width,
height,
}}
>
{props.onSelectedChanged && (
<Form.Control
type="checkbox"
className="wall-item-check mousetrap"
checked={props.selected}
onChange={() => props.onSelectedChanged!(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
)}
<ImagePreview
loading="lazy"
loop={video}
@ -132,6 +165,9 @@ interface ISceneWallProps {
scenes: GQL.SlimSceneDataFragment[];
sceneQueue?: SceneQueue;
zoomIndex: number;
selectedIds?: Set<string>;
onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void;
selecting?: boolean;
}
// HACK: typescript doesn't allow Gallery to accept a parameter for some reason
@ -148,6 +184,9 @@ const SceneWall: React.FC<ISceneWallProps> = ({
scenes,
sceneQueue,
zoomIndex,
selectedIds,
onSelectChange,
selecting,
}) => {
const history = useHistory();
@ -223,6 +262,7 @@ const SceneWall: React.FC<ISceneWallProps> = ({
const renderImage = useCallback(
(props: RenderImageProps<IScenePhoto>) => {
const sceneId = props.photo.scene.id;
return (
<SceneWallItem
{...props}
@ -230,10 +270,18 @@ const SceneWall: React.FC<ISceneWallProps> = ({
targetRowHeight(containerRef.current?.offsetWidth ?? 0) *
maxHeightFactor
}
selected={selectedIds?.has(sceneId)}
onSelectedChanged={
onSelectChange
? (selected, shiftKey) =>
onSelectChange(sceneId, selected, shiftKey)
: undefined
}
selecting={selecting}
/>
);
},
[targetRowHeight]
[targetRowHeight, selectedIds, onSelectChange, selecting]
);
return (
@ -257,14 +305,26 @@ interface ISceneWallPanelProps {
scenes: GQL.SlimSceneDataFragment[];
sceneQueue?: SceneQueue;
zoomIndex: number;
selectedIds?: Set<string>;
onSelectChange?: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const SceneWallPanel: React.FC<ISceneWallPanelProps> = ({
scenes,
sceneQueue,
zoomIndex,
selectedIds,
onSelectChange,
}) => {
const selecting = !!selectedIds && selectedIds.size > 0;
return (
<SceneWall scenes={scenes} sceneQueue={sceneQueue} zoomIndex={zoomIndex} />
<SceneWall
scenes={scenes}
sceneQueue={sceneQueue}
zoomIndex={zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
selecting={selecting}
/>
);
};

View File

@ -284,6 +284,10 @@ button.collapse-button {
opacity: 0;
width: 1.2rem;
&:checked {
opacity: 0.75;
}
@media (hover: none), (pointer: coarse) {
// always show card controls when hovering not supported
opacity: 0.25;
@ -297,10 +301,6 @@ button.collapse-button {
.card-check {
padding-left: 15px;
&:checked {
opacity: 0.75;
}
@media (hover: none), (pointer: coarse) {
// and make it bigger when hovering not supported
width: 1.5rem;
@ -314,6 +314,34 @@ button.collapse-button {
}
}
.search-item-check,
.wall-item-check {
height: 1.2rem;
width: 1.2rem;
}
// Wall item checkbox styles
.wall-item-check {
left: 0.5rem;
opacity: 0;
position: absolute;
top: 0.5rem;
z-index: 10;
&:checked {
opacity: 0.75;
}
@media (hover: none) {
opacity: 0.25;
}
}
.wall-item:hover .wall-item-check {
opacity: 0.75;
transition: opacity 0.5s;
}
.TruncatedText {
-webkit-box-orient: vertical;
display: -webkit-box;

View File

@ -22,7 +22,17 @@ const Scene: React.FC<{
queue?: SceneQueue;
index: number;
showLightboxImage: (imagePath: string) => void;
}> = ({ scene, searchResult, queue, index, showLightboxImage }) => {
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}> = ({
scene,
searchResult,
queue,
index,
showLightboxImage,
selected,
onSelectedChanged,
}) => {
const intl = useIntl();
const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } =
useContext(TaggerStateContext);
@ -71,6 +81,8 @@ const Scene: React.FC<{
showLightboxImage={showLightboxImage}
queue={queue}
index={index}
selected={selected}
onSelectedChanged={onSelectedChanged}
>
{searchResult && searchResult.results?.length ? (
<SceneSearchResults scenes={searchResult.results} target={scene} />
@ -82,9 +94,16 @@ const Scene: React.FC<{
interface ITaggerProps {
scenes: GQL.SlimSceneDataFragment[];
queue?: SceneQueue;
selectedIds: Set<string>;
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
}
export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
export const Tagger: React.FC<ITaggerProps> = ({
scenes,
queue,
selectedIds,
onSelectChange,
}) => {
const {
sources,
setCurrentSource,
@ -103,6 +122,8 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
const intl = useIntl();
const hasSelection = selectedIds.size > 0;
function handleSourceSelect(e: React.ChangeEvent<HTMLSelectElement>) {
setCurrentSource(sources!.find((s) => s.id === e.currentTarget.value));
}
@ -211,7 +232,12 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
return;
}
if (scenes.length === 0) {
// Use selected scenes if any, otherwise all scenes
const scenesToScrape = hasSelection
? scenes.filter((s) => selectedIds.has(s.id))
: scenes;
if (scenesToScrape.length === 0) {
return;
}
@ -232,15 +258,20 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
);
}
// Change button text based on selection state
const buttonTextId = hasSelection
? "component_tagger.verb_scrape_selected"
: "component_tagger.verb_scrape_all";
return (
<div className="ml-1">
<OperationButton
disabled={loading}
operation={async () => {
await doMultiSceneFragmentScrape(scenes.map((s) => s.id));
await doMultiSceneFragmentScrape(scenesToScrape.map((s) => s.id));
}}
>
{intl.formatMessage({ id: "component_tagger.verb_scrape_all" })}
{intl.formatMessage({ id: buttonTextId })}
</OperationButton>
{multiError && (
<>
@ -276,6 +307,10 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
index={i}
showLightboxImage={showLightboxImage}
queue={queue}
selected={selectedIds.has(s.id)}
onSelectedChanged={(selected, shiftKey) =>
onSelectChange(s.id, selected, shiftKey)
}
/>
))}
</div>

View File

@ -116,6 +116,8 @@ interface ITaggerScene {
showLightboxImage: (imagePath: string) => void;
queue?: SceneQueue;
index?: number;
selected?: boolean;
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
@ -129,6 +131,8 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
showLightboxImage,
queue,
index,
selected,
onSelectedChanged,
}) => {
const { config } = useContext(TaggerStateContext);
const [queryString, setQueryString] = useState<string>("");
@ -235,10 +239,28 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
history.push(link);
}
let shiftKey = false;
return (
<div key={scene.id} className="mt-3 search-item">
<div className="row">
<div className="col col-lg-6 overflow-hidden align-items-center d-flex flex-column flex-sm-row">
{onSelectedChanged && (
<div className="col-auto d-flex align-items-start pt-2 pr-2">
<Form.Control
type="checkbox"
className="search-item-check mousetrap"
checked={selected}
onChange={() => onSelectedChanged(!selected, shiftKey)}
onClick={(
event: React.MouseEvent<HTMLInputElement, MouseEvent>
) => {
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>
</div>
)}
<div className="col-12 col-lg overflow-hidden align-items-center d-flex flex-column flex-sm-row">
<div className="scene-card mr-3">
<Link to={url}>
<ScenePreview
@ -256,7 +278,7 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
<TruncatedText text={objectTitle(scene)} lineCount={2} />
</Link>
</div>
<div className="col-md-6 my-1">
<div className="col-12 col-lg my-1">
<div>
{renderQueryForm()}
{scrapeSceneFragment ? (

View File

@ -56,6 +56,10 @@
}
}
.search-item-check {
cursor: pointer;
}
.search-result {
background-color: rgba(61, 80, 92, 0.3);
padding: 1rem 0;

View File

@ -230,6 +230,7 @@
"verb_match_tag": "Match Tag",
"verb_matched": "Matched",
"verb_scrape_all": "Scrape All",
"verb_scrape_selected": "Scrape Selected",
"verb_submit_fp": "Submit {fpCount, plural, one{# Fingerprint} other{# Fingerprints}}",
"verb_toggle_config": "{toggle} {configuration}",
"verb_toggle_unmatched": "{toggle} unmatched scenes"