mirror of
https://github.com/stashapp/stash.git
synced 2026-02-04 01:52:43 -06:00
Add checkbox controls to Wall View and Tagger for Scenes, Scene Markers, Images, and Galleries (#6476)
This commit is contained in:
parent
c9fa3b76d9
commit
579fc66275
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -56,6 +56,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.search-item-check {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-result {
|
||||
background-color: rgba(61, 80, 92, 0.3);
|
||||
padding: 1rem 0;
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user